Skip to content

fix(runtime): the /meta generic branch refuses an item-less envelope, like its own object branch - #18655

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-18401-meta-generic-branch-itemless-success
Sep 17, 2026
Merged

os-support-ai merged 3 commits into
mainfrom
claude/issue-18401-meta-generic-branch-itemless-success

Conversation

@os-support-ai

@os-support-ai os-support-ai commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #18401

handleMetadataRequest's generic :type/:name branch returned protocol.getMetaItem's answer straight through with deps.success(data). That producer answers a miss with the protection envelope wrapped around an absent item — { type, name, item: undefined, lock, editable, deletable, resettable }, because resolveLockState(undefined, false) is unconditional — never with undefined. So a name with nothing behind it was announced as a 200, and JSON.stringify at the transport then dropped the item member: the declared GetMetaItemResponseSchema envelope minus its required member, served as a hit.

The object branch of the same function already refused that exact shape and answered 404. One function, two opposite answers to "does absence mean success?", selected by which type you asked for.

What changed

packages/runtime/src/domains/meta.ts — the generic branch now runs the same hit test its sibling runs (data?.item != null) before answering success. An item-less answer falls through to the MetadataService resolver exactly as the object branch falls through to the ObjectQL registry, and only a read no resolver can satisfy reaches the branch's own pre-existing deps.error('Not found', 404).

No new refusal dialect. The refusal this change routes to is the one this block already emitted, the ADR-0112 nested { success:false, error:{ code, message, httpStatus } } every other refusal in this file speaks. Sibling card #18402 — which asks how GET /meta/:type/:name spells its refusals, in carriers under packages/rest/src/ — is therefore neither answered nor pre-empted here. #18402 is not addressed by this PR and remains open.

Premises, measured before relying on them

premise reading
the generic branch is the only branch in the file serving success on an item-less getMetaItem envelope holds. Three getMetaItem call sites in the file: the two object-branch ones were already guarded (data?.item != null), this one was not. Control that could fail: the same sweep finds and names the two guarded siblings, and the ablation below moves the count 3 → 2 on exactly the line under test.
the object branch's refusal is the right one to copy holds, and it is not copied so much as reused. It is deps.error('Not found', 404) through apiErrorResponse / buildApiError, i.e. the ADR-0112 nested envelope with code derived from the status as RESOURCE_NOT_FOUND. The generic branch already ended at the identical call, so the change adds no second refusal.
no existing caller depends on the item-less success holds, measured. Full @objectstack/runtime suite green (265 files / 3660 tests). Repo-wide, the only handleMetadata consumers outside the package are a vi.fn() transport stub (packages/adapters/hono/src/__mocks__/runtime.ts) and an authz-conformance ledger row that names the handler for its anonymous-deny gate, neither behavioural on this shape; every dogfood /meta item probe addresses the object branch or /published, both untouched.

Clause-②: no

The heading above carries the key inside a ## prefix, which CLAUSE2_KEY_LINE does not accept — so the gate read it as a near miss. The bare line above it is the declaration; the reading it declares is unchanged.

Clause-②: no — declared from the measured diff

reading measurement
new exports 0 added export lines across the whole diff.
ERROR_CODE_LEDGER + StandardErrorCode 0 occurrences in the diff. No code is minted: the only SCREAMING_SNAKE literal added is 'RESOURCE_NOT_FOUND' inside a test assertion, an existing member that standardErrorCodeForHttpStatus(404) already derives.
accept-set changes none. No schema key, no closed set, no query-parameter allowlist. The change narrows what the handler treats as a hit on its own read; the set of requests it accepts is byte-identical.
check-widening-tells --declaration no --diff exit 0, and honestly: it reports both files as NOT MEASURED ("no declared surface covers it") — consistent with the path limb, since the contract surface it knows is packages/spec, which this diff does not touch.
check-clause2-carriers --pair recorded in the report comment on the card; run once this PR had a number.

⇒ no needs:contract-review. What a caller observes does change — a request previously answered 200 with an item-less body is now answered 404 — but no declaration moves, which is the axis this gate is about. A request that resolves to a real item is byte-identical to before, protection envelope included.

Tests

packages/runtime/src/domains/meta-item-absent-404.test.ts, six pins: the 404 with its code and status; the wire body measured against GetMetaItemResponseSchema in both directions; the two branches answering absence the same way (the finding itself); the fall-through to the MetadataService still happening; and two controls that must stay green — a real hit on each branch, every value read from inside the answer.

Ablation — the fix reverted to the unguarded pass-through, proven on disk before reading anything (if (data?.item != null) occurrences 3 → 2, raw pass-through 1 → 2, blob 4e5e86dde51f48acab), restore by git checkout HEAD -- verified byte-identical back to 4e5e86dde with git diff HEAD empty:

ABLATED_TEST_EXIT=1
  × §1 refuses an absent name with 404 RESOURCE_NOT_FOUND instead of the item-less 200
  × §2 the item-less envelope never reaches the wire as a 200
  × §3 the object branch and the generic branch answer absence THE SAME WAY (the finding)
  × §4 an item-less protocol answer FALLS THROUGH to the MetadataService
  Tests  4 failed | 2 passed (6)        ← the two controls held
restored →  Tests  6 passed (6)

Commands, all foreground, heavy ones through scripts/pm/os-verify-lock.sh:

  • pnpm --filter '@objectstack/runtime...' build --concurrency=2 — VERDICT command-exit 0
  • pnpm --filter @objectstack/runtime typecheck — green (tsc --noEmit plus check:test-typecheck: 27 files / 191 errors / 69 pinned signatures held, the new file carrying none of them)
  • pnpm --filter @objectstack/runtime test265 files / 3660 tests passed
  • pnpm lint repo-wide (eslint . --no-inline-config, full population, no narrowing) — exit 0 at 7a8e5f3a4
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 59 families (52, then 7 more once the changeset existed); reconciled with --ran carrying every exit code: 59 accounted, 57 run, 0 unrun.
    • 56 green.
    • check:dual-build-cjs-loads and check:type-check-debt first exited 3 (PREREQUISITE NOT MET, nothing measured); both re-ran green after a full turbo run build over ./packages/* and ./packages/*/* (72/72 tasks).
    • check:cross-package-test-inputs exits 1 — proven not this diff's, twice: see Acceptance notes.

Changeset

skip-changeset is not available. @objectstack/runtime is published (private: false, files[] = dist, README.md, CHANGELOG.md) and the changed hit test is in the shipped bytes: the marker occurs 3 times in the built packages/runtime/dist/index.js, matching the post-fix source count of 3 against a pre-fix 2, with a positive control (Save not supported) hitting. So: a patch changeset, carrying the Clause-② line.

Acceptance notes

To file (class a — a reproducible defect, dedupe words below). pnpm check:cross-package-test-inputs returns two opposite verdicts for one commit, decided by whether the box has built packages/spec. Isolated to a single variable in this worktree, same tree, same commit:

packages/spec/dist present (115 entries) → FAIL, exit 1
  "@objectstack/cli descends a directory tree from packages/spec/dist/ …
   rooted in packages/cli/test/init-created-files-summary.e2e.test.ts"
packages/spec/dist parked aside       → OK,   exit 0
  "29 package(s) read outside themselves, all declared … 13 walked root(s) judged"

The walk-radius limb enumerates the real filesystem under the walked root, so the radius is empty on an unbuilt tree and populated on a built one. It reds for exactly the developer who followed AGENTS.md's "rebuild before you believe it" and greens for the one who did not. Independently, an ablation putting this diff back to origin/main (proven on disk) left the same FAIL, so it is not this PR's. Dedupe words: cross-package-test-inputs walked radius · packages/spec/dist walk root · init-created-files-summary.e2e walk · ACCEPTED_WALK_RADII build state · gate verdict depends on dist presence.

noted, not filed. The /published branch of this same file guards its getPublished hit with data === undefined, so a producer answering null would pass through as a 200 — a different producer and a different envelope from the one this card is about, and no producer returning null was measured, so it is undrilled drift rather than a defect. Successor: none — no queued card or open PR touches that branch (#18402's carriers are in packages/rest/src/).


Generated by Claude Code

… like its own object branch

`handleMetadataRequest`'s generic `:type/:name` branch returned
`protocol.getMetaItem`'s answer straight through with `deps.success(data)`.
That producer answers a miss with the protection envelope wrapped around an
absent item (`{ type, name, item: undefined, lock, editable, deletable,
resettable }`) rather than with `undefined`, so a name with nothing behind it
was announced as a 200 whose body — after `JSON.stringify` drops the member —
is the declared envelope minus its required `item`.

The `object` branch of the same function already refuses that exact shape and
404s, so one function answered "does absence mean success?" both ways
depending on which type you asked for. `GetMetaItemResponseSchema` declares
`item` required, and the REST twin of this door refuses the same shape.

Apply the sibling branch's hit test, and let the miss fall through to the
MetadataService resolver and then to the branch's own existing 404. No new
refusal dialect is introduced.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
…ing through it

`check:test-typecheck` is an exact, shrink-only ratchet and the new pin's file
is inside the checked zone, so the sibling files' `res.response?.` spelling is
not available to it. A local narrowing helper is the stronger form anyway: the
negative assertions in this file (`toBeUndefined()`, `not.toBe(200)`) pass
vacuously against an unhandled result, which is the one outcome that must not
read as a pass here.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
`@objectstack/runtime` publishes `dist/`, and the built `dist/index.js` carries
the changed hit test (marker count 3, matching the post-fix source, against a
positive control that hits), so published bytes move and `skip-changeset` is
not available.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 1 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/system-context.mdx (via handleMetadataRequest (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 99fcb4ac104d44625136df690b51a2ff30a99d7cpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 1b10c0fcac5d3905da411984167d1f5b0c40cb12 — the merge of head 7a8e5f3a43af2efd9155b094d5bb517d2ce81ef1 into base 99fcb4ac104d44625136df690b51a2ff30a99d7c, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1b10c0fcac5d3905da411984167d1f5b0c40cb12 && git checkout 1b10c0fcac5d3905da411984167d1f5b0c40cb12
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 99fcb4ac104d44625136df690b51a2ff30a99d7c 7a8e5f3a43af2efd9155b094d5bb517d2ce81ef1 && git checkout -B drift-repro 99fcb4ac104d44625136df690b51a2ff30a99d7c && git merge --no-ff 7a8e5f3a43af2efd9155b094d5bb517d2ce81ef1

node scripts/docs-audit/affected-docs.mjs --json 99fcb4ac104d44625136df690b51a2ff30a99d7c

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 99fcb4ac104d44625136df690b51a2ff30a99d7c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 17, 2026
@os-support-ai
os-support-ai marked this pull request as ready for review September 17, 2026 14:00
@os-support-ai
os-support-ai added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit c17ff70 Sep 17, 2026
42 of 43 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-18401-meta-generic-branch-itemless-success branch September 17, 2026 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants