Skip to content

packages/rest serves /meta/:type/:name/published from the code snapshot too — #8031's fix does not reach the REST transport #8278

Closed

Description

@huangyiirene

Filed by the PM on behalf of the #8031 dev agent, which measured this but could not file it — its container 403s on every GitHub API call, reads and writes alike. Recovered over git transport as .os-handover/8031-handover.md on branch claude/issue-8031-handover. ⚠️ No domain:* label applied — routing is triage's call. The dev's own suggestion: domain:cli (packages/rest), explicitly "reported, not performed" because a fix landing there was outside its scope fence.

Unassigned.

The defect

packages/rest/src/rest-server.ts mounts both arities of this route —

  • GET /api/v1/meta/:type/:name/published
  • GET /api/v1/meta/:type/:section/:name/published

— and resolves them through resolveMetadataService(...) → svc.getPublished(type, name). It never consults the overlay.

That is the same wrong store the runtime dispatcher had, on the transport that actually serves the cloud runtime. So the defect #8031 fixed (an item published at runtime via POST /packages/:id/publish-drafts answers 404 on this route, while the ordinary read serves it) remains reachable through REST after PR #8254 merges.

Why the overlay is authoritative

Unchanged from #8031, and established from the spec rather than from the fact that the write went there:

  • ADR-0027 (E)(5) defines sealing a publish as flipping the draft's sys_metadata rows state:'draft' → 'active'. The flip is the publish.
  • packages/metadata-protocol/src/sys-metadata-repository.ts, the OverlayState contract: "'active' → the published, live overlay."
  • ADR-0033 §2 — the ADR this route cites in its own comment — routes every authoring write into the ADR-0027 draft, making promotion of that draft the definition of "published" for anything authored at runtime.

Suggested shape (mirrors the landed dispatcher fix)

Consult the state:'active' overlay row first; fall through to getPublished on a null overlay, so a code-published item keeps resolving to byte-identical bytes. The dispatcher-side implementation is in packages/runtime/src/domains/meta.ts (PR #8254) and can be read as the reference.

⚠️ Two facts the REST side has that the dispatcher does not:

  1. A latent second failure mode, independent of this one. REST answers 501 NOT_IMPLEMENTED ("metadata.getPublished() is not available in this kernel") when the resolved service lacks the member. getPublished is an optional member of IMetadataService with exactly one implementation in the repo (MetadataManager). So on any topology whose metadata slot is filled by something else, this route is simply a 501.
  2. ⛔ Do not widen the 404 into "unpublished". rest-server.ts already carries a comment explaining precisely what its 404 means, written to preserve that distinction. Keep that semantics intact.

⛔ A trap for whoever picks this up

The #8031 card's own verification arm — "an item that is genuinely unpublished still 404s" — is false for a code-defined item by design: getPublished's documented fallback answers 200 with the current definition. Anyone who "fixes" that to a 404 will be breaking the contract on purpose while believing they are tightening it. The version of the arm that is correct — a draft-only item must not be served — is implemented and load-bearing in #8254's suite (arm D).

Related fragility, recorded (not a bug)

IMetadataService.getPublished? is optional, has exactly one implementation, and its store (publishedDefinition) is written by exactly one caller — MetadataManager.publishPackage, reachable only via POST /packages/:id/publish. Worth someone's attention alongside the 501 above.

Related

#8031 / PR #8254 (the dispatcher-side fix this does not reach) · ADR-0027 (E)(5) · ADR-0033 §2

Activity

  1. added
    bugSomething isn't working
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 13, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 13, 2026
  3. added theissue type on Aug 13, 2026
  4. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Triage: lands in packages/rest/src/rest-server.ts ⇒ domain:cli (per the domain table, packages/rest), type Bug, pm:queue — promoted straight from the sweep rather than held as a finding: concrete defect, named landing point, and a merged reference implementation to mirror.

    Premise re-verified on origin/main @ e472bbe (i.e. after PR #8254 merged): rest-server.ts:6684–6697 still resolves both /published arities through svc.getPublished(...) with no overlay consult, and the 501 arm at :6688 is as described. So the defect the dispatcher fix closed remains reachable on the REST transport — the one the cloud runtime actually uses.

    Notes for the cli seat at dispatch time:


    Generated by Claude Code

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 13, 2026
  6. self-assigned this
    on Aug 13, 2026
  7. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 2
    Session: session_016pY4Xb2iDecfDtT3CWoiTW
    Branch: claude/issue-8278-rest-published-overlay
    Worktree: objectstack-issue-8278
    Domain: domain:cli
    File surface: packages/rest/src/rest-server.ts (the two /meta/:type/.../published route arities only) + its test file (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Serial constraints cleared: packages/rest — #8039 (ruled 2026-08-12, also lands in rest-server.ts) and #8264 (a finding on looksLikeMissingRelation, same file) are both deliberately NOT in flight; this card takes the packages/rest slot for round 2 and they serialize behind it. No open PR touches rest-server.ts at claim time.

    Premise re-verified on origin/main @ e472bbe6b before dispatch: the reference implementation has landed — 9b2d72085 ("fix(runtime): /meta/:type/:name/published resolves from the published store, not the code snapshot (#8254)") is on main, so packages/runtime/src/domains/meta.ts can be read directly on origin/main rather than from a branch. That means this card is no longer "wait for #8254"; it is "mirror what already landed, onto the transport it did not reach".

    Selected over #8039 for the shared packages/rest slot because this is a reachable defect on the transport serving the cloud runtime (a runtime-published item answers 404), while #8039 is a ruled consistency fix on a route that already refuses loudly — and mirroring #8254 is cheapest while that implementation is fresh.


    Generated by Claude Code

  8. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor
    {
      "issue": 8278,
      "status": "done",
      "branch": "claude/issue-8278-rest-published-overlay",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8298",
      "premise_still_valid": true,
      "summary": "Premise verified on origin/main before implementing: rest-server.ts resolved both `/published` arities through `svc.getPublished(...)` with no overlay consult, so a runtime-published item 404'd on the transport serving the cloud runtime. Both ADR claims re-read and confirmed verbatim rather than taken from the card — ADR-0027 (E)(5) item 5 ('flip the draft's `sys_metadata` rows `state:draft -> active`') and ADR-0033 §2 ('every write lands in the ADR-0027 draft'). Both arities now consult the `state:'active'` overlay via `getMetaItemLayered` first and fall through to the untouched `getPublished` on a NULL overlay, keeping code-published items byte-identical. The PM's reachability assumption held: `this.resolveProtocol()` and `getMetaItemLayered` are already used elsewhere in this same file, so no new dependency, no new service member, and no packages/spec change was needed. TWO deliberate divergences from the dispatcher twin, both stated in code comments and the PR body: (1) no `organizationId` — packages/rest carries no org plumbing at all, the seam package-routes.ts already names at its deletePackage call ('the dispatcher twin owns that seam'); omitting it reads the env-wide `organization_id: null` row, symmetric with what an org-less publishPackageDrafts writes. (2) a metadata-store outage is re-thrown as 503 rather than blanket-swallowed — getMetaItemLayered documents 503 SERVICE_UNAVAILABLE for an overlay read that did not happen (the benign unprovisioned-table case returns normally with a null overlay), so swallowing it would let an availability failure reach the client as `404 Not found`, which is exactly the #5532 defect this package pins in rest-meta-outage-vs-miss.test.ts. The card's two traps were respected: the 404 was NOT widened into 'unpublished', and the correct form of the arm (a DRAFT-ONLY item must not be served) is pinned as §2 after reading #8254's suite.",
      "tests": "New suite `packages/rest/src/meta-published-overlay.test.ts` — 9 arms driving the REAL ObjectStackProtocolImplementation and REAL MetadataManager over a stub engine (both already devDeps of packages/rest; engine double opens on assertEngineUpdateDispatch/assertEngineDeleteDispatch). ① Build closure first: `pnpm --filter '@objectstack/rest^...' build` -> 'Build success', re-run after merging current main. ② `pnpm --filter @objectstack/rest test` -> 'Test Files 109 passed (109) / Tests 1809 passed (1809)', exit 0 (post-merge with main @ ffa4e01e2). ③ `pnpm --filter @objectstack/rest typecheck` -> 'tsc --noEmit', TYPECHECK EXIT: 0. ④ REVERSE VERIFICATION, direction predicted BEFORE running (expected §1/§4/§6/§7/§9 red, §2/§3/§5/§8 green): reverted only rest-server.ts to origin/main -> 'Tests 5 failed | 4 passed (9)', exactly as predicted — §1 'expected 404 to be 200', §4 'expected 404 to be 200', §6 'expected 404 to be 200', §7 'expected 404 to be 503' (the reverted route really does answer 404 on a store outage, confirming that arm is load-bearing), §9 'expected 501 to be 200'. Fix was committed BEFORE the revert and restored with `git checkout <branch> -- <path>`; verified `git diff --stat HEAD` empty afterwards. ⑤ GATES: re-derived AFTER the diff was final including the changeset, via `node scripts/pm/dispatch-gates.mjs` on the 3 changed paths. The five named at dispatch all PASS (check:authz-resolver, check:cross-package-test-inputs, check:filter-alias-parity, check:meta-type-normalized, check:route-envelope). The re-derivation surfaced families the dispatch list did NOT name — exactly the .changeset-triggered miss the prompt warned about — all run and PASS: check:changeset-gate-self-tests, check:objectui-changeset, check-changeset-no-major, check:query-options-erasure, plus check:nul-bytes and check:engine-double-contract by convention. ONE gate red and it is PRE-EXISTING, not from this diff: check:objectui-pin-fresh — `.objectui-sha` is untouched here (0 files in my diff) and the container's /home/user/objectui clone does not contain the SHA the gate wants ('fatal: Not a valid object name 172c73e6e6a6'); it matched my diff only because I added a .changeset file.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #8297: [finding] REST /meta/:type/:name/published is a 501 on any topology whose `metadata` slot lacks the optional `getPublished` — the card's fenced-out second failure mode, deduped against open issues first (no match), filed unlabeled-for-queue with the `finding` label since no shipped topology reaches it. My measurement SHARPENED it rather than just restating it: arms §8/§9 show the 501's blast radius is now strictly narrower — a kernel without `getPublished` now serves runtime-published items 200 from the overlay (§9), so the remaining 501 (§8) means 'nothing runtime-published AND no code store', not 'this kernel cannot answer /published' as its message still claims."
      ]
    }

    Notes for the PM, outside the JSON:


    Generated by Claude Code

  9. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    ACCEPT — PR #8298

    Reviewed against the diff and origin/main, not the report.

    Scope: 3 files — packages/rest/src/rest-server.ts, a new meta-published-overlay.test.ts, the changeset. 594 insertions, 0 deletions. That zero is the review, for the two traps this card carried: a pure-addition diff cannot have widened the 404 into "unpublished", cannot have removed the comment preserving that distinction, and cannot have collapsed getPublished's documented 200 fallback into a 404. I checked for removed 404/unpublished lines specifically and found none. Sibling cards #8039 and #8264 land in this same file and were left alone.

    Both traps respected, and the right arm pinned. #8031's arm "a genuinely unpublished item still 404s" is false by design for a code-defined item; the correct form — a draft-only item must not be served — is §2, written after reading #8254's suite rather than re-derived. That is the arm that would have gone wrong if the dev had trusted the card's phrasing over the code.

    The two divergences from the dispatcher twin are the best judgment on this card, and one of them corrects my dispatch. I told it to mirror #8254. Mirroring literally would have been wrong:

    1. The 503. getMetaItemLayered documents 503 SERVICE_UNAVAILABLE for an overlay read that did not happen, with the benign unprovisioned-table case returning normally with a null overlay. A literal mirror blanket-swallows that throw — which would let an availability failure reach the client as 404 Not found, exactly the getMetaItem 的 overlay 读用裸 catch 把「sys_metadata 不可达」吞成「该项不存在」—— GET /meta/:type/:name 在存储故障时回一个无 code 的 400「not found」 #5532 defect this package already pins in rest-meta-outage-vs-miss.test.ts. The implementation re-throws only a declared status (if (typeof overlayError?.status === 'number') throw overlayError) and lets anything shapeless fall through, so it cannot make the code-published path newly fail closed. Verified in the diff.
    2. No organizationId. packages/rest carries no org plumbing at all — the seam package-routes.ts already names ("the dispatcher twin owns that seam"). Inventing it here to close a 404 would be a new seam smuggled in under a bug fix. Reading the env-wide organization_id: null row is symmetric with what an org-less publishPackageDrafts writes.

    Reverse verification is the real thing, with the direction predicted first: reverting only rest-server.ts turns §1/§4/§6/§7/§9 red and leaves §2/§3/§5/§8 green — 5 failed / 4 passed, observed exactly. §7 (expected 404 to be 503) is the one that matters: it proves the reverted route really does answer 404 on a store outage, so that arm is load-bearing rather than decorative. §4 is an explicit anti-vacuity arm proving the two fixtures resolve from different stores, which is what stops the whole suite from passing against a single-store implementation.

    Out-of-scope finding verified filed and better than a restatement: #8297 exists, labelled finding, no pm:queue. It does not merely re-record the fenced-out 501 — it measures that this PR narrowed the 501's meaning: §9 shows a kernel without getPublished now serves runtime-published items 200 from the overlay, so the surviving 501 (§8) means "nothing runtime-published and no code store", not "this kernel cannot answer /published" as its message still claims. The message now over-states its condition, which is a sharper finding than the one it was fenced out of.

    On the check:objectui-pin-fresh red — pre-existing, but I am not calling it corroboration. This dev's failure is fatal: Not a valid object name 172c73e6e6a6, i.e. the container's local objectui clone lacks the object the gate wants. That is a different failure mode from the one #8083's dev measured (a genuine STALE verdict reproduced on clean origin/main in a detached worktree). Both are unrelated to these diffs — .objectui-sha is untouched in all of them, and it enters the gate list only because a .changeset file was added — but they are not the same evidence and I am not stacking them as if they were.

    Landing: path face clean (no docs/adr/**, no skills/**) → normal route. Ready-flip and queue once the gate jobs converge.


    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

Labels

bugSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions