test(metadata-protocol,rest): the cached /meta ETag folds the SUPPLIED organization — measured, pinned, not a fault - #17292
Conversation
… folds
`getMetaItemCached` folds `request.organizationId` — the SUPPLIED member —
into the ETag scope prefix, not the effective scope
`organizationIdForMetaRead` returns. Measured and pinned end to end:
§1 a type declared `allowOrgOverride: false` gates the supplied
organization away, serves the env-wide record, and still issues an
organization-named validator — byte-identical bodies, two validators.
§2 the "resolve an org row, fall back to env-wide" path the fold was
written against is present today, reachable on an overridable type
whose organization has no row, with the control that the harness can
resolve an org row when one exists.
§3 the served bytes are inside the same hash, so a validator can never
pin a caller to a representation it did not receive: unchanged
document 304s (the control that must fire), a changed document never
does, and one organization's validator is never honoured for another.
§3 is the half that makes §1 a cost rather than a fault, and the half a
hash change would break silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
The cached door computes `organizationIdForMetaRead(canonicalMetaUrlType( req.params.type), readCtx?.tenantId)` above the cached/uncached fork and spreads THAT into the `getMetaItemCached` request. Because the protocol folds the member as handed in when it computes the ETag, this reduction is what keeps the validator's declared scope equal to the scope the body was resolved under. Nothing pinned it. The sibling `rest-server-meta-read-org-scope.test.ts` records the gap in its own words -- "it pins that the arm still FOLDS, never that the fold happens at the door" -- and its measured ablation confirms it: swapping the door's predicate for a raw tenant leaves that file green, because the callee re-folds. The callee re-folds for the BODY, not for the validator. Observed through the ETag response header across two reads of one document differing only in the session's active organization: identical on a non-overridable type, and -- the control that must fire -- different on an overridable one. Plus the counterfactual, asking the protocol directly with the raw tenant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
…t as future The fold's rationale argued from "any FUTURE path that resolves an org row but falls back to the env-wide body". That path is present: `getMetaItem` resolves `(orgId ? findOverlay(orgId) : undefined) ?? findOverlay(null)`, so an organization with no row of its own is served the env-wide document under an org-named validator. Reading it as future is how the next author concludes the risk has not arrived. Also records the two facts a reader of this block needs and could not get from it: `request.organizationId` is the SUPPLIED member (the registry reduction happens below, inside `getMetaItem`), and the reason neither that nor the fallback is a correctness fault is that `content` is inside the same hash -- so a 304 can only ever pin a caller to bytes it received, and the residue is validator fragmentation rather than error. Comment only; no behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
…tmetaitemcached-etag-scope
`check:engine-double-contract` named it: the cached-ETag door pin declares a findOne double the pinned ledger does not record, so the ledger would not protect that file. Written by the gate's own `--write`, which reports 0 seams added, grown or lost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6a3d03423b69b6752f1001236084f31bf7fbf0b8 && git checkout 6a3d03423b69b6752f1001236084f31bf7fbf0b8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab450f49a154859630dee81aeb1ba86d9665100f dced2447e511834beee2f5524d8d8ba7cd487908 && git checkout -B drift-repro ab450f49a154859630dee81aeb1ba86d9665100f && git merge --no-ff dced2447e511834beee2f5524d8d8ba7cd487908
node scripts/docs-audit/affected-docs.mjs --json ab450f49a154859630dee81aeb1ba86d9665100f
|
Fixes #16525
This is an investigation card, and the answer is a third one — neither of the two branches the filing seat named. The ETag folds the SUPPLIED
organizationId, so branch 2 is syntactically correct. But the divergence it feared is unreachable at the only production door, and where it is reachable it is not a cache-correctness fault. No behaviour changes here. What lands is the measurement, made durable.(a) Which value is folded — measured
packages/metadata-protocol/src/protocol.ts:11956-11965(getMetaItemCached, declared at:11854):⇒ The SUPPLIED request member. The registry reduction —
organizationIdForMetaRead(packages/metadata-core/src/meta-write-org-scope.ts:184) — runs one layer BELOW this line, insidegetMetaItematpackages/metadata-protocol/src/protocol.ts:7818, and its result never travels back up to the hash.(b) Is the divergence reachable — measured, and the answer has two halves
Half one: by a direct call on the published verb, YES.
objectis declaredallowOrgOverride: false(packages/spec/src/kernel/metadata-plugin.zod.ts:724). Supply an organization for it and the gate answersundefined, the null-coalescing fallback atprotocol.ts:7922-7923falls to the env-wide record, and the validator still names the organization. Reproduced end to end inpackages/metadata-protocol/src/get-meta-item-cached-etag-scope.test.ts§1 — byte-identical bodies, two validators.Half two: through the REST cached door, NO — the door pre-gates.
packages/rest/src/rest-server.ts:6607-6611computesabove the cached/uncached fork and spreads THAT into the cached request at
:6656, which is handed togetMetaItemCachedat:6658. The protocol's own fold (canonicalizeMetaRequestTypeatprotocol.ts:266reachingcanonicalMetaTypeat:233, which iscanonicalMetaUrlType) is the same fold the door used, andorganizationIdForMetaReadis idempotent. ⇒ At that door supplied equals effective, andrest-server.ts:6658is the only non-test invocation of this verb in the repository (grep over every.ts/.tsx/.mjsoutsidedist/and outside*.test.ts).⭐ Why the reachable half is a COST and not a fault
content— the bytes actually being sent — is inside the same hash (protocol.ts:11956,:11961-11965). A304is therefore answered only on an exact match over those bytes, so a caller is only ever pinned to the representation it itself received. The harm the card names ("a validator claiming to describe an org-scoped representation that was never served") cannot reach anybody. What is left is validator fragmentation: N organizations reading one env-wide document hold N validators for identical content. Waste, not error.⛔ Not repaired, deliberately. Folding the effective value would change every published ETag that carries an organization — one forced miss per caller per deploy — and buys nothing at the door above, where the two values already agree. That is a decision, not a regression fix, and it is not this card's to take.
What lands
Two pins and one comment correction. No production code path changes.
packages/metadata-protocol/src/get-meta-item-cached-etag-scope.test.ts(new, 9 cases) — §1 the supplied-value characterization, §2 the fallback's reachability with the control that the harness can resolve an org row, §3 the content-in-hash invariant that makes §1 harmless.packages/rest/src/rest-server-meta-cached-etag-door-scope.test.ts(new, 7 cases) — the door's pre-gate, observed through theETagresponse header. Nothing pinned this before. The siblingrest-server-meta-read-org-scope.test.tssays so in its own header: "it pins that the arm still FOLDS, never that the fold happens at the door."packages/metadata-protocol/src/protocol.ts— comment only. The fold's rationale argued from "any future path that resolves an org row but falls back to the env-wide body". That path is present today (§2 above). Corrected to present tense, plus the two facts a reader of that block could not previously get from it: which value is folded, and thatcontentin the hash is what makes it safe.scripts/engine-double-contract.pinned.json— written bynode scripts/check-engine-double-contract.mjs --write, as that gate's own failure text instructed. It reports 0 seams added, grown or lost.Ablations — both fired, both restored
git diff HEADempty and by blob-hash equality against theHEADblob, not by an exit code.A — remove the door's pre-gate (
rest-server.ts:6607-6611replaced by a rawreadCtx?.tenantId):rest-server-meta-cached-etag-door-scope.test.ts(new)rest-server-meta-read-org-scope.test.ts(sibling)The two that reddened are exactly §1's non-overridable case and §3's counterfactual:
⭐ The control inside §1 — an overridable type, where the two validators must DIFFER — stayed green, so the ablation did not simply break the observation channel. And the sibling staying green at 41/41 is the measured confirmation of the gap its own header records: the callee re-folds for the BODY, not for the validator.
B — remove
contentfrom the hash (protocol.ts:11961-11965reduced to the scope alone):get-meta-item-cached-etag-scope.test.ts(new)Exactly one assertion carries that invariant, and §1/§2 stay green under it — which is the point: a hash "optimization" would leave every other assertion green while destroying the argument that makes the current shape safe.
Verification
Measured at
dced2447e5, exit codes captured by redirect-then-$?, never across a pipe.pnpm --filter @objectstack/metadata-protocol exec vitest runpnpm --filter @objectstack/rest exec vitest run --project localpnpm --workspace-concurrency=2 --filter @objectstack/metadata-protocol --filter @objectstack/rest run typecheckcheck:test-typecheckon rest, 0 files / 0 errorsnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack)check:engine-double-contractexit 1, then 0 after its own--writeremedy⊘ NOT MEASURED, both requiring a whole-repository build, which is CI's run and not this card's:
pnpm check:dual-build-cjs-loads— exit 3,PREREQUISITE NOT MET: 44 packages have nodist/. ⛔ Not a pass.pnpm check:type-check-debt— exit 3, same class. ⛔ Not a pass and not a finding.The gate list was derived on a fresh tree after merging
origin/main(the first derivation warnedSTALE TREE); re-deriving added exactly one family,pnpm check:tier-file-adoption, which was run and exits 0.Region fence (PR #17252,
protocol.tslines 4820-5100): clear. This branch's only hunks in that file are at@@ -11919and@@ -11922— about 6,800 lines away.Grade:
skip-changeset, measuredfiles[]for both packages is["dist","README.md","CHANGELOG.md"]. After building both:metadata-protocolfiles[]getMetaItemCachedinmetadata-protocol/distdistreadOrganizationIdinrest/dist⇒ Nothing published moves; the controls fire.
scripts/engine-double-contract.pinned.jsonis a repo-root script asset and ships in no package.Acceptance notes
The describe prose is already carded, and it is not a new finding.
GetMetaItemCachedRequestSchema.organizationId's describe (packages/spec/src/api/protocol.zod.ts:1816-1822) opens "Selects the org partition in the ADR-0005 overlay read order… exactly as on the uncached read", which does not say that a supplied organization is gated away entirely for a type declaringallowOrgOverride: false. ⛔ Not edited —packages/spec/belongs to the seat that filed this card.⭐ And ⛔ not filed either, because it is already row three of an open decision-box card. I read
packages/spec/src/api/protocol.zod.tsat the anchor shac383352cb752245899b6ca7e2dc7d233405113ee, and line:1815there is the first line of exactly this describe. Issue #16524 names:271/:441/:1815and is open, awaiting the maintainer. So the triage note on this card that "本卡第 1 条改的是GetMetaItemCachedRequestSchema自己的 describe,⛔ 不是那三处" is mistaken on that one point::1815is that describe.One correction worth carrying into that decision: the second half of this describe — "folded into the ETag, so a scope switch never returns a stale 304 from another scope's cached representation" — is measurably TRUE, and is now pinned (
get-meta-item-cached-etag-scope.test.ts§3, third case). Only its first sentence is imprecise. Issue #16524 remains open and untouched by this branch.Noted, not filed (successor named, per the discipline that an unfiled observation must say who will meet it):
🤖 Generated with Claude Code
https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Generated by Claude Code