Repository navigation
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
|
…ied org is always consulted (objectstack-ai#16524) (objectstack-ai#20177) Fixes objectstack-ai#16524 Clause-②: no ## What this changes Three published `.describe()` strings in `packages/spec/src/api/protocol.zod.ts`, the `organizationId` member of: | schema | was | now | |:--|:--|:--| | `GetMetaItemRequestSchema` | "Selects the org partition in the ADR-0005 overlay read order … Absent = environment-wide read: only env-level overlays apply and no org partition is consulted." | "When an org partition applies, this selects it in the ADR-0005 overlay read order … Supplying a value does not by itself guarantee an org partition is consulted; where none applies, and whenever it is absent, the read is environment-wide and only env-level overlays apply." | | `GetMetaItemLayeredRequestSchema` | "Selects the org partition … Absent = environment-wide read: `overlay` reports the env-level row only." | "When an org partition applies, this selects it … Supplying a value does not by itself guarantee an org partition is consulted; where none applies, and whenever it is absent, the read is environment-wide: `overlay` reports the env-level row only." | | `GetMetaItemCachedRequestSchema` | "Selects the org partition … exactly as on the uncached read. Also folded into the ETag, … Absent = environment-wide read." | "When an org partition applies, this selects it … exactly as on the uncached read. Also folded into the ETag, … Supplying a value does not by itself guarantee an org partition is consulted; where none applies, and whenever it is absent, the read is environment-wide." | Plus the regenerated `content/docs/references/api/protocol.mdx` (three table rows) and an `@objectstack/spec` **patch** changeset. Prose only: no key, export, accept set or runtime behaviour changes. A supplied `organizationId` is still accepted on all three requests, and the runtime still drops it where no org partition applies. ## Rulings carried - Ruling B (comment 5567936958), maintainer, verbatim: 「协议不对不是应该修改协议吗?」. The describes are protocol text, so they are corrected now. The wording qualifies the promise, states the negative, names neither `allowOrgOverride` nor the registry (no registry clause), and adds no key. Its shape matches the sibling `GetMetaItemsRequestSchema.organizationId` row reworded in PR objectstack-ai#16523, so the four meta-read rows now read the same way. - Scope ruling (comment 5807019811), maintainer, verbatim: 「16524 改文档,不需要按组织取」. This PR changes published text only: no runtime change, and no refusal of a supplied `organizationId`. - Measured correction (comment 5612706063): on `GetMetaItemCachedRequestSchema` only the opening sentence overstated. The ETag sentence ("Also folded into the ETag, so a scope switch never returns a stale 304 from another scope's cached representation.") is true, is pinned by PR objectstack-ai#17292, and is **kept byte-for-byte**. Its source line does not appear in this diff. ## Premise re-verified on today's tree Measured on `84880f9` (BASE); the merge of `369bcbed` touched no file below. - The gate is `organizationIdForMetaRead` (`packages/metadata-core/src/meta-write-org-scope.ts:184`). It returns `undefined` unless `declaresOrgOverride(type)` holds. - `getMetaItem` (`packages/metadata-protocol/src/protocol.ts`) reads `request.organizationId` only through the gate (`:8123`). A gated-away value resolves `findDraft(null)` (`:8161`) and `findOverlay(null)` (`:8227`-`:8228`), which are env-level. - `getMetaItemLayered` gates at `:8633`. `if (orgId)` at `:8728` skips the org overlay read entirely, and only `findOverlay(null)` runs. - `getMetaItemCached` delegates to `getMetaItem` (`:12199`), so the same drop applies. The ETag folds the *supplied* value (`:12294`), so the kept ETag sentence stays true. The runtime still behaves as the ruling describes, so no stop was needed. ## Verification All at head `3388f2920` (branch merged with `origin/main` `369bcbed`), unless noted. Every heavy run went through `scripts/pm/os-verify-lock.sh`, and each result is the `VERDICT command-exit` line. - `pnpm --filter @objectstack/spec build`, then `check:generated`: before `--fix`, exactly 1 of 15 artefacts was stale (`content/docs/references/**`). `--fix` regenerated that one, and the three `organizationId` rows in `protocol.mdx` are the only lines that moved. After the merge: "All 15 generated artifacts are up to date", exit 0. - `pnpm --filter @objectstack/spec test`: 540 files, 15820 passed and 2 todo, exit 0. `src/api/protocol.test.ts` alone: 178 passed. - `pnpm --filter @objectstack/spec typecheck` (tsc, scripts, and the test layer): exit 0. - Compiled `dist` read back: the three new descriptions are present, "allowOrgOverride" and "registry" each count 0 in them, and the ETag sentence occurs verbatim once. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 101 commands, the same list before and after the merge. `--ran` reconciliation reads "101 derived, 100 run, 1 NOT-MEASURED, 0 UNRUN". All 100 that ran exited 0. - NOT MEASURED: `pnpm check:dual-build-cjs-loads` (exit 3, PREREQUISITE NOT MET). It needs every publishable package built: 33 still had no `dist/` after the closure build below, and CI's `Build Core` runs it. As a declared narrowing, I loaded every `require` entry of `@objectstack/spec`, the only package this diff rebuilds, under CJS: 23 of 23 loaded. The gate's own `--list` names 18 spec entries, all inside those 23. Every other package's dist is unaffected by string edits in `packages/spec/src`. - For five gates the first run refused with PREREQUISITE NOT MET (unbuilt `lint` / `formula` / `client-react` / `objectql`): `check:skill-examples`, `check:doc-formula-expressions`, `check:doc-security-posture`, `check:docs-transcript-drift` and `check:lean-entry-closure`. After a turbo build of that 34-task closure they ran green. `check-plugin-teardown-shape --self-test` first refused on the shallow clone; it went green after a depth-1 fetch of its pinned fixture commit. - `check:dts-closure` and `check:sourcemap-no-sources-content` swept 1 built package, `@objectstack/spec`, which is the one this diff touches. - ESLint (narrowed, a measurement rather than a skip): `eslint --no-inline-config --format json` over the three touched paths returns 3 entries. `protocol.zod.ts` has 0 errors and 0 warnings. The `.md` and `.mdx` answer "no matching configuration", which means they are outside the population defined by `eslint.config.mjs`'s `files` globs. The config enables no type-aware linting (no `parserOptions.project`, zero `projectService`), so this diff cannot move a verdict on any untouched file. The repo-wide `pnpm lint` is left to CI. - No pin test is added. These strings are prose that no consumer parses, and pinning their wording would only freeze the wording. ## Acceptance notes - `packages/spec/src/api/protocol.test.ts:1599`-`1602`, a test comment, still says the implementation "accepts and HONOURS `organizationId` on all four read verbs: it selects the org partition … it decides which tenant's row is served". That is the same overstatement, in an unpublished test comment. It is outside this card's claimed file surface, so I left it. Carrier: none. - Draft PR objectstack-ai#19637 also regenerates `content/docs/references/api/protocol.mdx`. Whichever PR lands second regenerates that file; the file is routed to the `os-regen` merge driver. --- _Generated by [Claude Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
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