Skip to content

Multi-package artifact: the metadata service attributes every top-level object to the artifact's manifest.id while the registry owns it per package — crm_order is served twice on GET /api/v1/meta/object, listed under com.example.multi.core, and Studio's Data pillar for the App package shows the module's object #14599

Description

@hotlong

Found during the post-landing verification of #14439 (PR #14513) on main 7085f9053, booting examples/app-multi-package with objectstack dev --seed-admin. Verification-only finding — nothing fixed here. This is the "correctness edge … nothing exercises it today" that #14512 names, exercised: the two copies of one definition are attributed to different packages, and which owner a consumer sees depends on which door it went through.

Fixture

examples/app-multi-package — com.example.multi.core (type app, owns crm_account + app multi_crm) and com.example.multi.orders (type module, owns crm_order, lookup → crm_account), composed with composeStacks([ordersStack, coreStack], { manifest: 'preserve' }). The artifact's top-level manifest is core (the 'last' pick), packages[] carries both.

What the server answers (all authenticated as the seeded admin)

GET /api/v1/meta/object — crm_order appears twice, crm_account once. The two crm_order rows are not byte-identical (one carries _packageVersion, the other does not — two producers):

[{"name":"crm_order","label":"Order","_packageId":"com.example.multi.orders","_packageVersion":"1.0.0","_provenance":"package"},
 {"name":"crm_order","label":"Order","_packageId":"com.example.multi.orders","_packageVersion":null,"_provenance":"package"},
 {"name":"crm_account","label":"Account","_packageId":"com.example.multi.core","_packageVersion":null,"_provenance":"package"}]

GET /api/v1/meta/object?package=com.example.multi.core — returns the module's object as well as its own:

[{"name":"crm_order","_packageId":"com.example.multi.orders"},{"name":"crm_account","_packageId":"com.example.multi.core"}]

GET /api/v1/meta/object?package=com.example.multi.orders — correct: [{"name":"crm_order","_packageId":"com.example.multi.orders"}].

GET /api/v1/meta/object/crm_order/layers (same answer with ?package= either value) — the metadata service's copy says the owner is core:

{"type":"object","name":"crm_order","code":{"name":"crm_order","_packageId":"com.example.multi.core","_packageVersion":"1.0.0","_provenance":"package",…},"overlay":null,"effective":{…"_packageId":"com.example.multi.core"…},"lock":"none","provenance":"package","packageId":"com.example.multi.core"}

GET /api/v1/meta/object/crm_order?package=com.example.multi.core — the single-item door says the owner is orders: {"item":{"name":"crm_order","_packageId":"com.example.multi.orders","_packageVersion":"1.0.0",…},"packageId":"com.example.multi.orders",…}.

So the platform holds two answers to "who owns crm_order": the layers door says com.example.multi.core, the item door and the list rows say com.example.multi.orders. GET /api/v1/packages itself is right (core.objects = [crm_account], orders.objects = [crm_order], both writable: false).

Both boots of the same DB answered identically (rows do not accumulate; this is in-memory attribution, no sys_metadata rows exist for either object).

What Studio shows (both the vendored console at the pinned objectui d8ec8d6d and objectui main 7dedec6f)

The Data pillar for com.example.multi.core lists Order and Account (rail text Objects Order Account Read-only); the pillar for com.example.multi.orders lists only Order. ADR-0130 Consequences ("Studio's scope is the package, so package boundaries are the grouping Studio has never had (§1.3a)") does not hold for the App package: it shows the whole artifact. Screenshots are attached to the verification comment on #14439 (hmr-03-core-data.png, vendored-03-core-data.png).

Mechanism (read, not guessed)

  1. MetadataPlugin._parseAndRegisterArtifact iterates the flattened top level and stamps every item with the artifact's manifest.id — packages/metadata/src/plugin.ts:929 (manifestPackageId = metadata.manifest.id), :1005 (applyProtection(item, { packageId: manifestPackageId, … })), :1010 (manager.register(metaType, name, item, { notify: false })). For this artifact that id is com.example.multi.core, so the metadata service's crm_order is stamped core. This is the copy the layers door serves.
  2. The ObjectQL load path registers packages[] per package in topological order — packages/objectql/src/plugin.ts:453 (ql.registerApp(manifest, scope)) — so the registry's owner of crm_order is com.example.multi.orders (registry.ts:2695 stamps _packageId from getObjectOwner). This is the copy the item door and one of the list rows serve.
  3. registry.getAllObjects(packageId) filters by contributors (registry.ts:2688), and the metadata-service copy stamped core is re-ingested into the registry as core's contribution to crm_order (an overlay contribution — objectContributionKind, owner ≠ packageId), which is why ?package=com.example.multi.core returns crm_order while the row still reports the owner as orders.
  4. The list merge keys slots by ${packageId}${name} (packages/metadata-protocol/src/protocol.ts:1199), so the core-attributed copy and the orders-attributed copy land in two slots — the duplicate row.

getMetaItems for the list is runtime/src/domains/meta.ts → protocol.getMetaItems({ type, packageId }).

Expected

One owner per object across every door (com.example.multi.orders for crm_order), one row per object on GET /api/v1/meta/object, ?package=<id> returning exactly the objects GET /api/v1/packages says that package owns, and Studio's per-package Data rail matching that. #14512's option B (teach the metadata artifact door to read packages[] when present, and stop emitting the flattened copy) removes the cause at the root; a narrower fix is for the top-level iteration to stamp each item with its owning package (resolvable from packages[]) instead of the artifact's identity when packages[] is present. Single-package artifacts are unaffected either way (manifest.id is the owner there — D7).

Repro

pnpm exec turbo run build --filter='./examples/app-multi-package...'
cd examples/app-multi-package && ./node_modules/.bin/objectstack dev --seed-admin -p 4310 -d file:/tmp/mp/data.db
# sign in: POST /api/v1/auth/sign-in/email {"email":"admin@objectos.ai","password":"admin123"} → set-auth-token → Bearer
curl -H "Authorization: Bearer $T" 'http://localhost:4310/api/v1/meta/object' | jq '.items | map(select(.name|startswith("crm_"))) | map({name,_packageId,_packageVersion})'
curl -H "Authorization: Bearer $T" 'http://localhost:4310/api/v1/meta/object?package=com.example.multi.core' | jq '.items|map(.name)'
curl -H "Authorization: Bearer $T" 'http://localhost:4310/api/v1/meta/object/crm_order/layers' | jq '{code:.code._packageId, packageId}'

Related: #14512 (the format decision), #14439 / #14513 (the producer), #14122 (epic), ADR-0130 D4/D5 and Consequences §1.3a.

Activity

  1. self-assigned this
    on Sep 2, 2026
  2. hotlong commented on Sep 2, 2026

    @hotlong
    ContributorAuthor

    Claiming this card.

    Generated by Claude Code


    Generated by Claude Code

  3. hotlong commented on Sep 2, 2026

    @hotlong
    ContributorAuthor

    os-dev-report

    {
      "issue": 14599,
      "status": "done",
      "branch": "claude/issue-14599-metadata-door-reads-packages",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14643",
      "premise_still_valid": true,
      "summary": "MetadataPlugin._parseAndRegisterArtifact now reads both ADR-0130 D4 shapes: `packages` present -> each assembled package body's collections are registered stamped with THAT body's id/version; `packages` absent -> the single-`manifest` branch runs exactly as before (D7). Ordering and the entry gate are reused from the ObjectQL load path's own `resolveArtifactPackageOrder` (D5), so the two readers of one `packages[]` cannot disagree about order or about which artifacts load. SCOPE DEVIATION, deliberate and flagged: reuse was physically impossible from where that function lived — `@objectstack/objectql` depends on `@objectstack/metadata` — so `artifact-packages.ts` moved to `@objectstack/core`, which already owns `resolvePluginOrder` and is already a dependency of both readers (no package edge added); objectql re-exports all three names, so its published surface is unchanged, and runtime's three dispatcher-error-vocabulary `file:` anchors were repointed. A residual sweep keeps any top-level definition no package body repeats (`packages` composes by `concat`) — registered once under the artifact's own id and warned about — so nothing a booted instance sees today disappears. Producer untouched: composeStacks / os build keep emitting the flattened top level (#14512's decision).",
      "tests": "All at HEAD 423074c99, clean tree. MEASUREMENT FIRST: every live ARTIFACT_FIELD_TO_TYPE key (25) is a member of AssembledPackageBodySchema, so iterating bodies loses no collection; the only two absent (`workflows`, `ragPipelines`) are declared by NEITHER schema and are already-ledgered dead map entries. ACCEPTANCE on a real `objectstack dev --seed-admin` boot of examples/app-multi-package (all 6 criteria met, exact JSON quoted in the PR body): (1) GET /api/v1/meta/object -> exactly [{crm_account, com.example.multi.core}, {crm_order, com.example.multi.orders}]; (2) ?package=core -> [crm_account], ?package=orders -> [crm_order], matching GET /api/v1/packages' manifest.objects; (3) /meta/object/crm_order/layers -> code._packageId and packageId both com.example.multi.orders, and the item door asked under ?package=core agrees; (4) /meta/app -> multi_crm stamped com.example.multi.core (the fixture carries no other package-owned collections; /meta/view is count 0 on both legs); (5) GET /api/v1/packages unchanged, both rows writable:false, core.objects=[crm_account], orders.objects=[crm_order]; (6) single-package control on a real app-todo boot, fixed vs ablated -> `diff` EMPTY. UNIT PIN packages/metadata/src/plugin-artifact-packages-attribution.test.ts (new, 7 cases, fixture built by the REAL composeStacks producer) 7 passed; includes the D7 control as a literal manager.register sequence. DOGFOOD packages/qa/dogfood/test/multi-package-artifact.dogfood.test.ts extended +3 cases over real HTTP with the artifact door mounted via artifactSource (bootStack registers no MetadataPlugin, so the pre-existing block could not reach this defect) -> 8 passed. ABLATION, direction predicted RED before running, prediction exact: mutation `git checkout 2a2653619 -- packages/metadata/src/plugin.ts`; PROVED ON DISK — blob ac95906e -> 0b22c5f2, markers _registerArtifactBodyCollections 3->0, resolveArtifactPackageOrder 3->0, carriesPackages 4->0, deleted text `packageId: manifestPackageId` back at 3 hits; REBUILT BOTH LEGS (dogfood resolves metadata through dist) with ablation-dist-preflight.mjs — mutate leg `--absent` -> 'marker absent from all 24 built files', restore leg -> 'marker present in 6 built files'; OBSERVED unit 4 failed | 3 passed (the 4 predicted red; crm_order stamped com.example.multi.core instead of .orders), dogfood 3 failed | 5 passed (crm_order row duplicated), and a real ablated boot reproduced the card's exact 3-row JSON; RESTORED BY BYTES `git checkout HEAD -- <absolute path>` -> git hash-object == HEAD blob ac95906ed193bcaf1adf2eb112fa01ea91a8b622, whole-tree `git status --porcelain` empty, `git diff HEAD` empty, both pins re-run green (7/7, 8/8). SUITES: @objectstack/metadata 44 files/684 tests, @objectstack/core 47/1156, @objectstack/objectql 262/4518, @objectstack/runtime 209/3079 — all passed; objectql+runtime `typecheck` green (objectql check:test-typecheck included). GATES: union re-derived from the actual diff — `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` -> 43 commands, all run, exit codes captured before any pipe: 40 exit 0 (incl. check:dispatcher-error-vocabulary, check-system-context-census — anchors did not move, no --fix needed —, check:stack-collection-maps, check:test-source-alias, check:undeclared-dep-imports, check:type-source-resolution, check:published-files, check:empty-changeset, check:changeset-no-major), plus pnpm check:nul-bytes OK (7980 files). 3 exit 3 = the scripts' own PREREQUISITE-NOT-MET, recorded NOT MEASURED and not findings: check-test-completeness and check-half-states need a CI-supplied input; check:dual-build-cjs-loads needs a whole-repo pnpm build. CI status at report time: in_progress (draft PR just opened).",
      "mcp_calls": "6 — issue_read get, issue_read get_comments, add_issue_comment (claim), create_pull_request, pull_request_read (body read-back), add_issue_comment (this report). Card body + #14512's body and all 11 timeline items were read through the zero-quota public-repo payload channel; the repo-scoped REST probe answered 403 and `gh` is absent in this container.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Notes for the PM, outside the machine-read block:

    1. One deliberate scope deviation, flagged rather than taken silently. The dispatch said "keep the change to the metadata plugin" and "reuse resolveArtifactPackageOrder, do not re-derive an order". Those two conflict on a fact the dispatch did not have: @objectstack/objectql depends on @objectstack/metadata, so the door could not import that function from where it lived. Re-deriving was excluded twice over — by the direction, and by the module's own header (⛔ "Do not add a second sort here, in any form") — and the function is also the entry gate, so a second read would have made the two readers disagree about which artifacts are loadable, not just about order. Resolution: move it to @objectstack/core, which already owns resolvePluginOrder and is already a dependency of both readers, so no package edge is added; objectql re-exports all three names, so its published surface is unchanged. Diff spans core / objectql / metadata / runtime; changesets graded @objectstack/core minor, the other three patch.

    2. The dogfood harness could not reach this door, and that was measured, not assumed. bootStack registers AppPlugin and no MetadataPlugin, so the existing multi-package dogfood block measures the ObjectQL registry alone — a pin written on /meta/* through the plain harness would have been green on both ablation legs. The new block mounts the real artifact door via artifactSource over a build-shaped artifact (the shape showcase-object-extension-meta-read.dogfood.test.ts established), and the ablation confirms it is not a phantom: 3 red on the mutate leg.

    3. One behaviour beyond the dispatch's per-key rule, and why. The dispatch framed "keep reading the top level" per KEY; the measurement says every live key has a package-body counterpart, so per that rule the top level would be skipped entirely. The residual risk is per-ITEM, not per-key: packages composes by concat, so an artifact built from one stack that already carried packages and one that did not has top-level collections outside every body, and dropping them would be a silent regression. The sweep registers those once under the artifact's own id and logs it; for a normally composed artifact it registers nothing and logs nothing (pinned both ways).

    4. A dead-map-entry observation, deliberately NOT filed as a new issue. workflows and ragPipelines in ARTIFACT_FIELD_TO_TYPE are declared by neither ObjectStackDefinitionSchema nor AssembledPackageBodySchema — a definition carrying either is refused unrecognized_keys by the strict parse, so they can never match, exactly like the retired themes / roles / policies. This is already ledgered: scripts/check-stack-collection-maps.mjs carries an explicit waiver for these two keys naming Six independent enumerations of the stack-collection set, none answerable to stack.zod.ts — one is mis-aimed and two still list retired kinds #6242 row 4. Filing it would be a duplicate, so it is recorded here instead.

    5. Base has moved since the branch point (2a2653619 -> ca48cf377); the merge state reads blocked on a fresh draft, and CI convergence is yours per the standing split.

    Generated by Claude Code


    Generated by Claude Code

  4. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage — lane repair only. ⛔ No re-grading: bug / priority:p1 / pm:dispatched are unchanged and this card's dispatch is not disturbed.

    Added the missing domain:engine. This card was pm:dispatched at p1 with no domain:* at all, which is the worst shape in the sweep: a dispatched, top-priority card belonging to no lane. Nothing in the state machine would have surfaced it, and the seat that holds it had no label saying so.

    Named the fix surface rather than inferring from the title, per the anchoring rule. The manifest.id attribution the card describes lives in:

    • packages/objectql/src/artifact-packages.ts, registry.ts, plugin.ts
    • packages/metadata/src/ (the loader/registry side)

    Both packages/objectql and packages/metadata* are domain:engine under the merged lane (maintainer 2026-08-19; the former domain:metadata is retired from circulation). So the routing is the same whichever side the repair lands on, which is why I could close this gap without disturbing the in-flight work.

    ⚠️ One boundary worth stating for whoever holds it: the card's last symptom — "Studio's Data pillar for the App package shows the module's object" — is a client rendering of the same server defect. If any part of the fix turns out to be objectui-side, that half goes to repo:objectui as its own card; ⛔ do not cross the repo boundary from this one.

    If the holding seat reads the anchor differently, pm:retriage plus a dissent comment in the same stroke and I will re-route.


    Generated by Claude Code

  5. hotlong commented on Sep 2, 2026

    @hotlong
    ContributorAuthor

    Post-landing verification on main after #14643 — Studio grouping

    Re-run of the post-landing verification recorded on #14439 (14:17 UTC), now on main 655b106ce2804d2cdb9ed4386df795152535d372 (= the #14643 merge commit, on top of #14513). Verification only — no product code changed, no PR. Every defect the earlier run found on the objects door and in Studio's App-package rail is gone.

    Setup. Fresh worktree of origin/main at 655b106ce (git log --grep='#14643' → that HEAD), pnpm install, pnpm exec turbo run build --filter='./examples/app-multi-package...' (57/57 tasks). Boot from examples/app-multi-package: ./node_modules/.bin/objectstack dev --seed-admin -p 3457 -d file:<scratch>/dogfood-mp2/data.db — boot log 📦 Artifact: dist/objectstack.json; the artifact carries packages[] = [{manifest:{id:"com.example.multi.orders",…}}, {manifest:{id:"com.example.multi.core",…}}] and the flattened top level. GET /api/v1/health → 200 {"success":true,"data":{"status":"ok","version":"17.2.0",…}}. Sign-in POST /api/v1/auth/sign-in/email {"email":"admin@objectos.ai","password":"admin123"} → 200, set-auth-token present (77 chars), used as Authorization: Bearer. Two boots of the same DB (the second to mount the freshly built /_console) answered identically.

    Item 2 — server truth: PASS

    GET /api/v1/meta/object → 200, 75 items; crm_order once, crm_account once (fields/_diagnostics stripped):

    {"name":"crm_account","label":"Account","pluralLabel":"Accounts","isSystem":false,"datasource":"default","sharingModel":"private","nameField":"name","_packageId":"com.example.multi.core","_provenance":"package"}
    {"name":"crm_order","label":"Order","pluralLabel":"Orders","isSystem":false,"datasource":"default","sharingModel":"private","nameField":"name","_packageId":"com.example.multi.orders","_provenance":"package"}

    GET /api/v1/meta/object?package=com.example.multi.core → [{"name":"crm_account","_packageId":"com.example.multi.core"}] ✅ (the earlier run also returned crm_order here)
    GET /api/v1/meta/object?package=com.example.multi.orders → [{"name":"crm_order","_packageId":"com.example.multi.orders"}] ✅

    GET /api/v1/meta/object/crm_order/layers — the layers door now names orders (the earlier run said core):

    {"type":"object","name":"crm_order","code":{"name":"crm_order","_packageId":"com.example.multi.orders","_packageVersion":"1.0.0","_provenance":"package"},"overlay":null,"effective":{"_packageId":"com.example.multi.orders"},"lock":"none","provenance":"package","packageId":"com.example.multi.orders"}

    GET /api/v1/meta/object/crm_order?package=com.example.multi.core (item door, asked under the wrong package) — agrees:

    {"item":{"name":"crm_order","_packageId":"com.example.multi.orders","_packageVersion":"1.0.0","_provenance":"package"},"packageId":"com.example.multi.orders","provenance":"package","lock":"none"}

    (?package=com.example.multi.orders → same owner; crm_account/layers → code._packageId = packageId = com.example.multi.core.)

    GET /api/v1/meta/app → multi_crm owned by core: {"name":"multi_crm","label":"Multi-Package CRM","_packageId":"com.example.multi.core","_provenance":"package","navigation":[{"id":"nav_accounts","objectName":"crm_account"}]} ✅ (other rows: setup, account).

    GET /api/v1/packages → 200, total: 23; the two fixture rows verbatim except manifest.objects/manifest.apps reduced to names:

    {"manifest":{"id":"com.example.multi.core","namespace":"crm","defaultDatasource":"default","version":"1.0.0","type":"app","scope":"project","name":"Multi-Package Core","description":"The App half of a two-package release artifact (ADR-0130 D4)","objects":["crm_account"],"engines":{"protocol":"^17"},"apps":["multi_crm"]},"status":"installed","enabled":true,"installedAt":"2026-09-02T18:51:09.804Z","updatedAt":"2026-09-02T18:51:09.804Z","writable":false}
    {"manifest":{"id":"com.example.multi.orders","namespace":"crm","defaultDatasource":"default","version":"1.0.0","type":"module","scope":"project","name":"Multi-Package Orders","description":"The Module half of a two-package release artifact (ADR-0130 D4)","objects":["crm_order"],"dependencies":{"com.example.multi.core":"^1.0.0"},"engines":{"protocol":"^17"},"apps":null},"status":"installed","enabled":true,"installedAt":"2026-09-02T18:51:09.810Z","updatedAt":"2026-09-02T18:51:09.810Z","writable":false}

    GET /api/v1/packages/com.example.multi.core → {"id":"com.example.multi.core","writable":false,"objects":["crm_account"]}; …/com.example.multi.orders → {"id":"com.example.multi.orders","writable":false,"objects":["crm_order"]}.
    GET /api/v1/meta/package → 200, 23 items; fixture rows: {"manifest":{"id":"com.example.multi.core","type":"app","namespace":"crm","version":"1.0.0","objects":["crm_account"]},"status":"installed","enabled":true,"writable":false} and {"manifest":{"id":"com.example.multi.orders","type":"module","namespace":"crm","version":"1.0.0","objects":["crm_order"]},"status":"installed","enabled":true,"writable":false} ✅

    assertion result
    exactly one crm_order (owner orders) and one crm_account (owner core) on GET /meta/object ✅
    ?package=core → [crm_account]; ?package=orders → [crm_order] ✅
    layers door and item door (asked under ?package=core) both name com.example.multi.orders ✅
    /meta/app → multi_crm owned by core ✅
    /api/v1/packages both rows writable:false, core.objects=[crm_account], orders.objects=[crm_order] ✅
    /meta/package both rows writable:false ✅

    Item 3 — Studio: PASS (vendored console AND objectui HMR console)

    (a) Vendored /_console. Pin .objectui-sha = 67dadd602a3a891666ea1513c5de677140784b6a (objectui#7229). A fresh worktree has no console dist, so it was built at the pin with pnpm objectui:build (✓ @objectstack/console dist ready (50932 KB) from objectui@67dadd602a3a; pnpm check:console-sha → ✓ Console dist matches the objectui pin; no drift refusal, /_console/ → 200 after a restart by recorded PID on the same DB). The pin does not contain objectui#7331 (ebc05b4, 64 commits behind objectui main) — PIN-stale, not BUILD-stale; as #14597 records, on this fixture the pre-#7331 heuristic and the server writable verdict coincide.

    (b) Authoritative — objectui HMR console at objectui main bb459ea9cb734166a8bf2d84163ed684d885e95c (git merge-base --is-ancestor ebc05b4 … → true, so #7331 is in), detached worktree, apps/console: DEV_PROXY_TARGET=http://localhost:3457 VITE_SERVER_URL=http://localhost:3457 pnpm exec vite --port 5190 --strictPort (proxy self-check GET /api/v1/health via :5190 → 200), signed in on that origin. Every row below is identical on (a) and (b).

    Headless Chromium 141, viewport 1440×900, screenshots taken after networkidle + settle; DOM text recorded only alongside its screenshot.

    check result evidence (vendored-* / hmr-*)
    Studio front door (/studio) lists both packages ✅ both under "INSTALLED (READ-ONLY · BROWSABLE)", each card Read-only; "MY PACKAGES (WRITABLE): No writable packages yet" 02b-studio-front-door.png
    Package picker lists both, both read-only ✅ popover "PACKAGES (APPS)": Multi-Package Core com.example.multi.core Read-only, Multi-Package Orders com.example.multi.orders Read-only; 0 × Writable 04-core-switcher-open.png
    Header badge / rail footer read-only ✅ header button Multi-Package Core Read-only / Multi-Package Orders Read-only; rail footer lock + Read-only (no New object); Create app disabled on orders 03-core-data.png, 05-orders-data.png
    App package (com.example.multi.core) Data rail lists ONLY Account ✅ rail = ["Account"] (crm_account, 9 fields); no Order — the earlier run's Order Account leak is gone 03-core-data.png
    Module package (com.example.multi.orders) Data rail lists ONLY Order ✅ rail = ["Order"] (crm_order, 10 fields) 05-orders-data.png
    Cross-package lookup crm_order.account → crm_account renders in the module's object view ✅ Form tab shows Account LOOKUP; field inspector: Type Relation · Lookup, Related object crm_account, "every crm_account record is selectable" 07-orders-form-tab.png, 08-orders-account-lookup-inspector.png

    The client's own fetch('/api/v1/packages') from each origin returned writable:false on both fixture rows (*-findings.json).

    Note on the entry route: /_console/apps/com.objectstack.studio renders "App not available — it may still be publishing" on this fixture (02-studio-landing.png, same on both consoles). That is the console's correct answer here, not a defect: the fixture installs no com.objectstack.studio package (GET /api/v1/packages → 23 rows, none Studio; GET /api/v1/meta/app → multi_crm, setup, account only). The builder's own front door is /_console/studio, which is what the table measured.

    Observations (no issue filed)

    Verdicts

    Evidence: <scratch>/dogfood-mp2/evidence/{server,studio-vendored,studio-hmr} on the verification host (curl JSON per door, boot logs, vendored-0{1,2,2b,3,4,5,6,7,8}-*.png, hmr-0{1,2,2b,3,4,5,6,7,8}-*.png, *-findings.json). Servers stopped by recorded PID; worktrees removed.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions