fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type - #14767

Merged
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate
Sep 3, 2026
Merged

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type#14767
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate

Conversation

@os-musk

@os-muskos-musk commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Part of #14683

getMetaItems now applies the allowOrgOverride read gate itself, so a metadata sweep that reads more than one type per request is scoped per type instead of per request.

Clause-②: yes. No new exported symbol and no new key on a published payload, so the mechanical floor is not hit. The limb that holds is that this change alters the runtime meaning of a spec-described request member without touching the spec: GetMetaItemsRequestSchema.organizationId is described as "Selects the org partition in the ADR-0005 overlay read order…", and after this PR the org partition is not consulted at all for a registry-non-overridable type. The described contract and the served behaviour diverge, with the spec unchanged.

⚠️ Corrected from the first revision of this body, which named the limb as "which rows a caller gets back, with a tenancy dimension". That limb does not hold: a tenancy read-scope behaviour change sits on the human floor rather than the contract-review one, and the accept/refuse criterion is not met either — the same requests are accepted and the same refusals raised. needs:contract-review requested either way.


The change

One line of behaviour, in packages/metadata-protocol/src/protocol.ts:

request=canonicalizeMetaRequestType(request);const{ packageId }=request;constorgId=organizationIdForMetaRead(request.type,request.organizationId);

The two per-arm re-reads of request.organizationId (the active-overlay read and the previewDrafts read) are deleted and both now spend that one resolution — a gate threaded into only one arm would leave the draft preview resurrecting exactly what the active list had stopped serving.

Three placement facts, each load-bearing:

  • After the canonical fold, never before.declaresOrgOverride tolerates the MANIFEST plurals and not the URL-only ones (translations, email_templates have no manifest key). Handed a raw URL segment it answers env-wide for two genuinely org-overridable types — one item in two partitions, addressed by spelling.
  • One resolution, both arms.
  • No new dependency edge.organizationIdForMetaRead was already imported into protocol.ts (line 57) from @objectstack/metadata-core, already a workspace:* dependency of packages/metadata-protocol. Nothing moved between packages, no cycle. (Measured, not assumed — this was a declared stop condition.)

The harm class is RESURRECTION, not concealment

Carried forward from triage deliberately, because a fix written against the opposite premise would aim at the wrong failure.

SysMetadataRepository.history() filters organization_id by strict equality, so naming the tenant therehides an allowOrgOverride: false type's rows. On this path the two queryByOrg reads are UNIONed, so naming it can only add — and what it adds are the pre-#6190 phantoms: org-scoped rows of types with no per-org read channel, which loadMetaFromDb walks past and reportUnhydratableOrgScopedRows exists to warn about. Read back, they surface inside a clearance rendered before a destructive action, where a resurrected row is worse than an omission because it reads as evidence.


⭐ The idempotence proof (the tripwire)

Direction A was ruled conditional on showing that moving the predicate inside does not change the scope any already-gating call site receives. Verdict: it does not. No gating call site's scope moves.

The measured call-site population

Counted on this branch's own tree, not taken on faith:

#sitereaches getMetaItems?effect of this change
1rest-server.ts:3172/layersno — getMetaItemLayereduntouched
2rest-server.ts:4823GET /meta/:type listyes (:4839)unchanged — proof below
3rest-server.ts:5661 by-name readno — getMetaItem / getMetaItemCacheduntouched
4rest-server.ts:6517/historyno — historyMetaItemuntouched
5rest-server.ts:7085/diffno — diffMetaItemuntouched
6protocol.ts:11091 search sweep's page readyes (:11118)unchanged — page is non-overridable, both readings are undefined

organizationIdForMetaRead has FIVE call sites in rest-server.ts on origin/main, not six. The sixth is the page read inside packages/metadata-protocol itself. Both readings of "six" are now reconciled: five in that file, six in the tree. (PR #14677 would make it six in the file; it has not landed.)

The argument

Let f(t, o) = organizationIdForMetaRead(t, o). f answers either o or undefined, so f(t, f(t, o)) === f(t, o) for every t and o — a second application over the same type is an algebraic no-op. The load-bearing half is therefore "the same type", and it holds for both sites that reach the method:

  • Site 2 gates on canonicalMetaUrlType(req.params.type) and then passes type: req.params.type, the raw segment. The first statement of getMetaItems folds that segment through canonicalizeMetaRequestType, which iscanonicalMetaUrlType — the identical map, so request.type inside is the identical string the door gated on.
  • Site 6 gates on 'page' and passes 'page'.

The proof is executable, not prose

packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts §3 measures both halves over the complete accepted-spelling population — 61 spellings, derived from META_URL_TO_SINGULAR unioned with the registry, so a new type or a changed fold cannot slip past:

  • §3a (61 cases) — the algebra, per spelling.
  • §3b (61 cases) — the behavioural half. For every spelling, a caller that gates first receives partitions equal to what the pre-change implementation read for that same argument (which it spent verbatim, so the expectation is the old behaviour written out).
  • §3c (61 cases) — the complement: an ungated caller is narrowed. Without it the suite would pass on an implementation that changed nothing.

§3b is green on both sides of the ablation, and that is its positive content — a case green with and without the change is exactly the demonstration the tripwire asked for.


What the change DOES move — and the semver derivation

Callers that pass a raw active organization are narrowed for non-overridable types. Enumerated mechanically from call sites rather than from the card's table:

callermulti-type?today
runtime/domains/meta.ts:921 — dispatcher GET /metadata/:typeone per request, any typeraw org ⇒ narrowed
runtime/domains/packages.ts:1160assemblePackageManifest export sweepyes, every plural keyraw org ⇒ narrowed
runtime/domains/packages.ts:603 — ADR-0045 publish visibility flip readapp onlyraw org ⇒ narrowed
getMetaDiagnostics untyped sweepyes, whole registryno org passed by REST today ⇒ correct when one is forwarded
findReferencesToMetayes, per matcher.fromTypeno org passed by REST today ⇒ correct when one is forwarded

Semver: patch — derived from this change's own lineage. A published /meta read door's row set changing is not a new class here; it is the class this predicate was born in, and all three landed instances shipped patch:

commitwhat changedlevel
b6c769019 (#9454 / #9727)the row set every /meta read door returns — org rows addedmetadata-core, metadata-protocol, rest: all patch
26f3588fb (#10340 / #10519)which partition two spellings read — rows movedrest, metadata-core: patch
67ceb9aef (#11553)the same fold-before-scope repair on the dispatcher doorruntime: patch

The first is the commit that introduced organizationIdForMetaRead itself. Adding the org partition to every read door was patch; moving which partition two spellings read was patch; withholding the org partition from types that never had a read channel for it is the same class one verb further in, and takes the same level.

Not minor.scripts/check-changeset-no-major.mjs refuses major outright, so during the launch window a genuinely breaking change ships as minor (pre-1.0, whole-stack lockstep) — #13925 is exactly that: "@objectstack/core": minor, carrying a bolded incompatibility banner and an adr-0087: marker for a narrowed published accept set.

But the implication runs one way only, and the gate's own header is explicit that it does: during the window minor is the union of ordinary new-functionality bumps and banner-marked breaking ones — 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps carrying no banner at all — so the bump level "tells a consumer nothing about whether the release breaks them". The carriers of breaking-ness are the bolded banner in the changeset body and the ADR-0087 disposition: "during the window they are the only signal there is".

⇒ So minor here would not claim an incompatibility — it would claim nothing about compatibility, which is precisely the cost the header names. This change carries neither carrier because it owes neither: nothing is retired, no accept set narrows, and check-adr-0087-registration reads it as non-breaking. The level is patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted.

⚠️ The previous revision of this paragraph asserted that minormeans "breaking" here and that claiming it would signal an incompatibility. That affirmed the consequent — breaking ⇒ minor holds, minor ⇒ breaking does not — and contradicted the very file it cited. Corrected above; the conclusion (patch) is unchanged, only the reason it rests on.

Nothing here is incompatible, and the reason is what the withheld rows are. They are the #6190 phantoms: org-scoped rows of types with no per-org read channel. The platform has refused to mint them since ac244ad09 / 6155c3c24, boot hydration skips them, reportUnhydratableOrgScopedRows audits them, and every REST /meta read door has already withheld them since b6c769019. The only doors still serving them were the dispatcher list (runtime/src/domains/meta.ts:921) and the runtime manifest and publish-flip reads (packages.ts:1160, :603) — so this change aligns those three with the published /meta surface rather than departing from it. A consumer reading those rows was reading through a door inconsistent with /meta, on data the platform had already ruled dead.

⛔ Not "only a refactor of where the predicate lives" either — triage refused that sentence and it is not the reason for the level. The predicate's new position does change which rows three doors serve; that is why this is a behaviour entry rather than an internal note, and the lineage above is why its level is patch.

⚠️Corrected from the first revision of this body, which claimed minor on the strength of a precedent that does not exist: issue 13973 (named without a link so this PR does not pull an unrelated blocked card into its timeline — the withdrawal is checkable as written) is still open and blocked, never landed on main, and its subject is driver date materialisation. It says nothing about read doors at any level. That citation is withdrawn rather than replaced; the three landed commits above are the real lineage. The same revision also mis-stated #13925 as carrying a major bump, which check-changeset-no-major.mjs forbids outright; it shipped minor, as above.


Reverse verification

Mutation: the gate line replaced by const orgId = request.organizationId; — i.e. the pre-change behaviour restored, in packages/metadata-protocol/src/protocol.ts.

Predicted in writing before mutating: 54 red / 140 green, named — §1 phantom case (1), §2 draft case (1), §3c for the 50 spellings folding to a non-overridable type, §4 sweep (1), §5 sources (1).

Observed: exactly 54 failed / 140 passed (194), and the named set matched — §1×1, §2×1, §3×50, §4×1, §5×1, with zero §3b failures.

Discipline on both legs:

  • Mutation confirmed on disk, not by an editor exit code: the gated line went 1 -> 0 and the injected line 0 -> 1, asserted before the run; a miss aborts and voids the reading. ⚠️ To be exact about that second anchor: the injected string carried an // ABLATION: gate removed suffix, and it is that exact string which went 0 -> 1. The bare statement const orgId = request.organizationId; already occurs twice in protocol.ts (getMetaItem at :7348 and getMetaItemLayered at :7760), so counted bare it went 2 -> 3. The suffix is what made the anchor unambiguous; the first revision of this body quoted the count without saying so.
  • Resolution path measured, not assumed. The test imports ./protocol.js — same-package relative, so it reads source. Demonstrated positively: dist/index.js still contained the gate throughout the mutated run (grep count 1), so a dist-mediated test could not have gone red. It did.
  • Restore proved by state, not exit code: git checkout HEAD -- PATH with PATH absolute (naming HEAD so a written index cannot hand the mutation back), then git hash-object compared against the HEAD blob — 13b37b5d468e9bcff6c9edd588a2699bac36b157, matched — plus git diff HEAD empty. An empty hash is read as failure, never as "nothing to compare".
  • trap ... EXIT INT TERM with an absolute repo root resolved via git rev-parse --show-toplevel, so a foreground-cap SIGTERM mid-mutation cannot leave the tree mutated.

Fixture triage

Two existing cases asserted a union the platform must no longer perform. Both were re-spelled, not deleted — the invariant each pins is unchanged and simply has to be measured on a type that has an org partition:

  • packages/metadata-protocol/src/meta-overlay-cache.test.ts §7 "an org-scoped read does not answer from the env-wide entry" — object to view. The key-separation invariant survives; on object it would now be asserting a separation the platform deliberately does not have.
  • packages/objectql/src/protocol-meta.test.ts "getMetaItems unions env-wide and org-specific rows" — app to view. This is the read-side twin of the [#6190] re-spelling three cases up in the same file. Its two getMetaItem (singular) siblings keep app on purpose: that verb is untouched here.

Scanned by the rule's consumption radius rather than by the edited package: every *.test.ts in the tree naming both getMetaItems and organizationId (22 files) was enumerated and the affected packages run.

Verification

origin/main merge base 5258b63f8. Union re-run at 3bbeb09f0 (git rev-parse --short HEAD at the time of the run) — the commit carrying the whole code tree. The commits after it are changeset prose only and touch nothing under packages/**, so the runs below still describe this PR's code exactly.

runresult
@objectstack/metadata-protocol full suite157 files / 2335 passed, 2 skipped
new pin file get-meta-items-org-read-gate.test.ts194 passed
@objectstack/objectql (4 affected files)169 passed
@objectstack/rest (6 affected files)190 passed
@objectstack/runtime (2 affected files)267 passed
@objectstack/metadata-protocol typecheckclean — and it does reach the new test file (it reported TS2345 there before the fix, so this is not a phantom pass)
@objectstack/objectql typecheckclean
pnpm lint (repo-wide eslint . --no-inline-config)clean — run whole, no narrowing claimed

Gate families re-derived on the FINAL file list with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed — the script takes its own change set from the merge base): 38 families, harvested with --commands so neither spelling nor the convention block could be dropped. The claim-time seed named 23; the extra families come from the changeset and from the "adds or edits a test file" convention block.

36 of 38 green. Two remain NOT MEASURED, in the gates' own words — neither is a pass and neither is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved turbo run test log; "running the family locally, record this gate as NOT MEASURED".
  • node scripts/pm/check-half-states.mjs — exit 3: the instrument refuses to run in an agent container.

Two others exited 3 on the first pass and were converted into real readings by building the workspace closure (turbo run build --filter='./packages/*' --filter='./packages/*/*', 71/71) rather than left unmeasured:

  • pnpm check:type-check-debtgreen: 22 ledger entries re-measured in 141.2s, 755 raw tsc errors, none above its recorded number, "surplus: none — every entry sits exactly at its measurement, so any new error is red". This is the ratchet family the new test file moves.
  • pnpm check:dual-build-cjs-loadsgreen: 102 require entry points across 66 packages load, 610 CJS files parse.

One gate went red and was repaired: check-adr-0087-registration read this changeset's prose citation of #13925's marker as this changeset's own breaking declaration (its detector is /\*\*BREAKING/i plus a line-initial BREAKING[ -]CHANGE). The change retires nothing and declares nothing breaking, so the repair is the wording — ⛔ not an adr-0087: disposition marker for a question this change does not raise.

The changeset has since been re-levelled to patch and its semver reasoning corrected twice (the two ⚠️ notes in the semver section above). After each, the three changeset-sensitive families were re-run on the final tree: node scripts/check-empty-changeset.mjsexit 0, node scripts/check-changeset-no-major.mjsexit 0, node scripts/check-adr-0087-registration.mjsexit 0. Nothing else was re-run, and nothing else needed to be: every commit after 3bbeb09f0 touches only .changeset/getmetaitems-org-read-gate.md, and packages/metadata-protocol/src/protocol.ts is still blob 13b37b5d468e9bcff6c9edd588a2699bac36b157 — the same blob the runs and the ablation above measured.

⚠️ Spelling note for anyone re-running these: there is nocheck:changeset-no-major npm script (root package.json has only check:changeset-gate-self-tests, which runs the --self-tests and not the gate itself). pnpm check:changeset-no-major therefore exits 254 as a missing script, which is not a gate reading. The direct node scripts/check-changeset-no-major.mjs spelling — the one dispatch-gates --commands emits — is the gate.

Scope

packages/rest/** is not touched — the fence held, and it turned out to be a proof obligation rather than an edit, exactly as claimed. packages/spec/** and packages/objectql/src/engine.ts are not touched either. The one file outside packages/metadata-protocol is packages/objectql/src/protocol-meta.test.ts, a forced fixture re-spelling.

The half this PR does not do, hence Part of rather than a closing keyword: the two doors named on the card — GET /meta/diagnostics with no ?type=, and GET /meta/:type/:name/references — pass noorganizationId at all on origin/main today, so nothing about their answers changes here. The gate is now in the right place for whenever a caller does forward one; the forwarding itself lives in packages/rest, which this card fences off. #13753 and #14677 are where that half belongs, and neither is addressed here.

Generated by Claude Code

Snapshot before verification. `organizationIdForMetaRead(request.type,
request.organizationId)` is resolved once, after the canonical fold, and
spent by both the active-overlay arm and the previewDrafts arm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Includes the idempotence proof the direction-A ruling was conditional on,
mechanised over the complete accepted-spelling population.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…n overridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…ridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… signal tokens (#14683)
The body cites #13925's major-plus-marker precedent to REFUSE it. Spelled with
the literal markers, check-adr-0087-registration reads the citation as this
changeset's own declaration and demands an ADR-0087 disposition for a change
that retires nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…] for the probe sites (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 9 documentable anchor(s).

31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 9 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 224f8ea4a0776d72de0003a77695bfb98c2206cdpackageMentionDocs.

Which tree this was computed on

This run read content/docs from cb52c281cb890f79d76a18fdc7e227be37f4b66e — the merge of head fc986acdab8dddc2e351409cd0ade81d34d20893 into base 224f8ea4a0776d72de0003a77695bfb98c2206cd, 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 cb52c281cb890f79d76a18fdc7e227be37f4b66e && git checkout cb52c281cb890f79d76a18fdc7e227be37f4b66e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 224f8ea4a0776d72de0003a77695bfb98c2206cd fc986acdab8dddc2e351409cd0ade81d34d20893 && git checkout -B drift-repro 224f8ea4a0776d72de0003a77695bfb98c2206cd && git merge --no-ff fc986acdab8dddc2e351409cd0ade81d34d20893
node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd

⚠️ 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 224f8ea4a0776d72de0003a77695bfb98c2206cd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — FAIL, adopted verbatim. Narrow: semver only, and it is repairable in prose.

Performed by an isolated reviewer at the configured tier in its own worktree cut from 3bbeb09f0, with no access to this seat's reasoning. This seat is off CONTRACT_REVIEW_TIER, so the verdict is adopted whole. The reviewer's own summary: "a patch round that only touches the changeset text and the PR body would pass." Rework is dispatched; ⛔ the PR stays draft.

The blocking finding — minor is over-declared, and both supporting citations are wrong

#13973 is not a landed precedent. This seat flagged it as a doubt in the review brief rather than letting it pass, and the reviewer confirmed it with evidence: state open, labels priority:p2, pm:blocked, domain:engine, body opens Blocked-by: #14078; git log origin/main --grep=13973 returns nothing; and its subject is Date-materialising drivers vs ISO-text drivers — it says nothing about read doors at any level. The changeset itself cites no number at all, only "the minor precedent", which does not exist.

The landed precedents for exactly this class all shipped patch — and they are this change's own lineage:

commitwhat changed on a published read doorlevel
b6c769019 (#9454/#9727)the row set every /meta read door returns (org rows added)metadata-protocol: patch, rest: patch, metadata-core: patch
26f3588fb (#10340/#10519)which partition two spellings read (rows moved)rest: patch, metadata-core: patch
67ceb9aef (#11553)the same, on the dispatcher doorruntime: patch

Against the other rungs: minor ⇔ a new export (87ad30c10, 3c1bbd2a8); minor + **BREAKING** + adr-0087: ⇔ a published type narrowing (d8024f050). This PR is fix(, adds no export, narrows no type. ⇒ patch.

A second factual error, introduced by the wording-repair commit 9f3244830: the changeset now says #13925's shape "would carry a major bump plus an explicit incompatibility marker". #13925 shipped as "@objectstack/core": **minor** with **BREAKING** and an adr-0087: marker, and scripts/check-changeset-no-major.mjs forbids major outright under pre-1.0 semantics. ⚠️ The pre-repair wording at cd9d3ee89 was accurate — the gate-driven repair broke a claim it was not aimed at. Worth recording as a class: a repair that satisfies a gate can damage a neighbouring sentence the gate does not read.

No BREAKING banner is owed, and the reviewer's reasoning is stronger than the changeset's: the rows a raw-org caller loses are the #6190 phantoms — refused at write since ac244ad09/6155c3c24, skipped by boot hydration, audited by reportUnhydratableOrgScopedRows, and already withheld by every REST /meta read door since b6c769019. The only doors still serving them were the dispatcher list and the runtime manifest/flip reads; this PR aligns them with REST. check-adr-0087-registration is green on the final tree — the repair laundered nothing, because nothing is owed.

What the review verified rather than accepted — the parts that PASS

  • ⭐ The idempotence tripwire is genuinely discharged, not circular. The reviewer read the pre-change protocol.ts (5258b63f8: :6811const orgId = (request as any).organizationId, :6873-6874 the two queryByOrg calls) and confirmed §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate, OVERRIDABLE, or the post-fold type. The "same type" half verified too: canonicalizeMetaRequestTypecanonicalMetaTypecanonicalMetaUrlType, the identical map the door gated on.
  • The ablation reproduced independently: 194 total, 54 failed / 140 passed, per section §1:1 §2:1 §3c:50 §4:1 §5:1, and §3b 0 of 61 on both legs — exactly the shape the tripwire asked for. Restore proved by blob hash 13b37b5d… and empty git diff HEAD.
  • ⭐ Both re-spellings are legitimate, and this was measured rather than argued: the reviewer ran the pre-change versions of both test files against the new protocol.ts and got exactly one failure each — the case that was re-spelled — and nothing else. app really did roll back to allowOrgOverride: false in ee58392e1 (ADR-0005:57 白名单表与注册表的分歧不止 flow:page/app/action/permission/tool/skill 六类 ADR 写 ❌ 而 allowOrgOverride:true,另有 dataset/book/position 三类表里没有却默认成了 true #6483/fix(spec): ADR-0005 白名单强制 —— 九类未获批 allowOrgOverride:true 回滚为 false (#6483) #6608). No coverage lost.
  • Call-site enumeration confirmed: five non-test organizationIdForMetaRead sites in rest-server.ts, a sixth at protocol.ts:11117, exactly two reaching getMetaItems; the other seven getMetaItems callers in rest-server.ts pass no organizationId. ⚠️ One caveat stated honestly: the reachability grep was this\.getMetaItems(, so an (this as any).getMetaItems( spelling would have evaded it — none seen, absence not exhaustively proven.
  • Third sweep verified (packages.ts:1160, assemblePackageManifest) and covered by the callee-side gate with no runtime edit. Part of is honest: neither door forwards an organization on origin/main today, so neither door's answer changes with this PR.
  • packages/rest/** and packages/spec/** untouched — the fence held.

Advisory findings


Generated by Claude Code

Contract review returned FAIL on the semver level alone. `minor` was
derived from #13973, which is open, `pm:blocked`, never landed, and is
about date-materialising drivers — it says nothing about read doors.
The real precedents are this predicate's own lineage, and all three
shipped `patch`: b6c7690 (#9454/#9727) added the org partition to every
`/meta` read door; 26f3588 (#10340/#10519) moved which partition two
spellings read; 67ceb9a (#11553) repaired the same on the dispatcher.
Withholding a partition from types that never had a read channel is the
same class.
Also corrects the #13925 claim this file's previous wording-repair commit
introduced: #13925 shipped `"@objectstack/core": minor` with a bolded
banner and an `adr-0087:` marker, not a major bump —
`check-changeset-no-major.mjs` refuses `major` outright, so `minor` in
this repo MEANS breaking, which is why claiming it here would be wrong.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Delta re-review — FAIL again, on one sentence, and it vindicates sending it back

The rework met every mechanical part of the tier's PASS condition — this seat verified that independently before asking (git diff --name-only 3bbeb09f0..000a9861c → one file; -- 'packages/**' → empty; level patch; 13973 × 0). ⛔ It still did not clear the fuse, because a FAIL is not a PASS and this seat is off tier. The rework was sent to the same reviewer as a scoped delta rather than adopted here.

That call is what caught this. The reworked changeset added an argument nobody asked for, and it is false.

The false sentence, and the file that refutes it

.changeset/getmetaitems-org-read-gate.md:59-60:

minor therefore means "breaking" here, and claiming it for this change would signal an incompatibility that does not exist.

scripts/check-changeset-no-major.mjs header lines 44-52 — the source the sentence cites — says the opposite:

Until then it is NOT the carrier, and that is the whole cost of the window: a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them. The mandatory information carriers for breaking-ness in the meantime are the BREAKING banner … and the ADR-0087 migration-ledger disposition …

Affirming the consequent.breaking ⇒ minor is true (header lines 25-27); minor ⇒ breaking is not, and the landed record carries the counter-examples: 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps with zeroBREAKING tokens. So claiming minor would signal nothing about compatibility — exactly the cost the header names — not a false incompatibility.

A false claim about the repo's versioning convention, in a release-notes input, contradicted by its own citation: the same class as the first FAIL. The conclusion (patch) is right; the sentence would teach the next author that every minor is breaking. Round 3 is dispatched to replace lines 59-60 and the PR body's mirror; ⛔ lines 54-58 stay as written.

Everything else in the delta — confirmed by the tier

  • The lineage table is accurate row by row, and the added claim that b6c769019"is the commit that introduced organizationIdForMetaRead itself" was verified: git show b6c769019 -- packages/metadata-core/src/meta-write-org-scope.ts shows +export function organizationIdForMetaRead(, absent from the parent.
  • The [finding] Plugin.type is typed string on a published surface — the eight-value set is enforced by prose only, though packages/core already depends on spec #13925 sentence is accurate and was not blunted by avoiding the literal token. d8024f050 is "@objectstack/core": minor, body line 7 carries the banner, line 35 the adr-0087: marker. Both detector regexes evaluate false on the current body and check-adr-0087-registration is exit 0.
  • The Clause-② restatement matches the tier's own finding — spec-described-semantics limb named, rows/tenancy limb withdrawn with the correct reason.
  • The ablation anchor arithmetic holds, and is subtler than either party first put it. A suffixed injection contains the bare statement as a prefix, so a -cF count on the bare string reads 3 after mutation while the suffixed string reads 1. 2→3 and 0→1 are both correct and consistent, as is 1→0 on the gate line. The dev's precision here was better than the correction it was given.
  • Code byte-identical: protocol.ts's blob at 000a9861c is still 13b37b5d…, the blob the tier ablated.

One advisory

The #13973 withdrawal states accurate facts but is not self-verifiable without naming the card. Suggested: spell it issue 13973 without the #, so a reader can check the withdrawal without this PR auto-linking an unrelated blocked card into its timeline. The dev's call, with a stated reason either way.

⭐ Platform fact, confirmed and now recorded

Root package.json has nocheck:changeset-no-major script — only check:changeset-gate-self-tests (line 120), which runs the three --self-tests and not the gate. So pnpm check:changeset-no-major exiting 254 (ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL … Did you mean "pnpm check:changeset-gate-self-tests"?) is a missing script, not a gate reading. The real gate is the direct node scripts/check-changeset-no-major.mjs, which is also the spelling dispatch-gates --commands emits. The dev reported its own 254 as a spelling error rather than banking it as a verdict — the right reading, and the reason this is now a fact rather than a mystery.


Generated by Claude Code

#14683)
I wrote that `minor` "means breaking" here, and that claiming it would
signal an incompatibility. That affirms the consequent: breaking => minor
holds, minor => breaking does not. check-changeset-no-major.mjs says so in
the header I cited — during the window the bump level "tells a consumer
nothing about whether the release breaks them", and the carriers are the
banner and the ADR-0087 disposition, "the only signal there is".
The landed record carries the other half: 87ad30c ('@objectstack/types':
minor) and 3c1bbd2 ("@objectstack/metadata": minor) are new-export bumps
with zero banner tokens. So `minor` is the union, not a breaking marker.
Corrected to what the gate actually says: the level carries nothing either
way, this change owes neither carrier, and `patch` follows from the lineage
and from no export being added. Lines 54-58 unchanged.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim (round 3)

The isolated reviewer at the configured tier returns PASS on the replacement sentence, judged on that and nothing else. Its words: "The fuse can be cleared in one stroke."

⚠️The fuse is not cleared in this comment. The label writes are read-modify-write with a compare read-back, and the label read path (GraphQL) is rate-limited right now while REST is up. ⛔ Writing a label set without a fresh read is how a concurrent write gets silently stripped, so the clearing waits for the read channel — and nothing is gated on it, because CI on fc986acda is still running (Lint & Repo Gates in progress at the time of writing).

What the tier verified on the final head

Re-derived at fc986acda in a throwaway worktree, not accepted from the seat or the dev:

git diff --name-only 000a9861c..fc986acda → .changeset/getmetaitems-org-read-gate.md
git diff --name-only … -- 'packages/**' → (empty)
protocol.ts blob → 13b37b5d… (still the blob it ablated)
level → "@objectstack/metadata-protocol": patch
'therefore *means* "breaking"' → 0 · 'ONE WAY ONLY' → 1 · '13973' → 0
both ADR-0087 detector regexes → false
check-empty-changeset · check-changeset-no-major · check-adr-0087-registration → exit 0

check-adr-0087-registration --base 5258b63f8 reports "1 non-breaking changeset(s) seen".

⭐ The replacement was checked line by line against its own source

This is the part worth recording, because the two earlier FAILs were both citations that contradicted the file they cited:

the new text sayscheck-changeset-no-major.mjs says
"the implication runs ONE WAY ONLY, and the gate's own header is explicit that it does"lines 46-47: "a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them"
minor is the union of new-functionality and banner-marked breaking bumpsverified last round: 87ad30c10, 3c1bbd2a8 are minor with zeroBREAKING tokens
"tells a consumer nothing about whether the release breaks them" / "during the window they are the only signal there is"verbatim at header lines 46-47 and 52
"which is precisely the cost the header names"line 45: "that is the whole cost of the window"
"patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted"the tier's own stated PASS condition — and it no longer affirms the consequent

One nit, explicitly not a fourth round

"check-adr-0087-registration reads it as non-breaking" describes what the gate detects — the absence of a declared breaking marker — which is slightly weaker than "non-breaking" simpliciter. Paired with the preceding "nothing is retired, no accept set narrows" (both true; the request accept set is unchanged), the sentence is not misleading. ⛔ Recorded, not actioned.

On the PR body

The tier judged it as found and confirms every statement in it matches something verified across the three rounds. ⚠️ It explicitly makes no claim about how the body came to be updated, and neither does this seat: the dev reported its publish blocked, the body nonetheless carries all three staged edits, and the working hypothesis — that the write landed while the tool's response path failed — is unproven. Recorded as an open platform question rather than a fact, because the consequence if true is real: "retry on error" could double-apply a mutation that already succeeded.


⭐ Three rounds, three FAILs, and the code was correct from the first oneprotocol.ts's blob never changed after 3bbeb09f0. Every finding landed in the prose explaining why the code is correct, and prose is what compiles into release notes. That is the contract review doing exactly the job it exists for.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 3, 2026 01:30
@os-musk
os-musk enabled auto-merge September 3, 2026 01:30
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at fc986acda.

1. Contract review PASS on round 3, adopted verbatim from an isolated reviewer at the required tier (5518644676). ⭐ Three rounds, three verdicts, and the code was correct from the first oneprotocol.ts is still blob 13b37b5d…, the blob the reviewer ablated at 3bbeb09f0. Every finding landed in the prose explaining why the code is right, and prose is what compiles into release notes.

2. The fuse cleared from BOTH carriers in one stroke, read-modify-write with a compare read-back: card #14683 and this PR. ⛔ It stayed on through two FAILs — a FAIL does not clear a fuse, only a PASS does.

3. The tripwire the ruling made this conditional on is discharged, and the tier confirmed it is not circular. It read the pre-change protocol.ts (5258b63f8:6811, :6873-6874) and verified §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate or OVERRIDABLE. It reproduced the ablation independently: 54 failed / 140 passed, §3b 0 of 61 on both legs, §3c 50 of 61, restore blob-hash matched.

4. Governed-surface predicate, re-run on the FINAL 5-file list — ⛔ not recalled:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/getmetaitems-org-read-gate.md \
packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts \
packages/metadata-protocol/src/meta-overlay-cache.test.ts \
packages/metadata-protocol/src/protocol.ts \
packages/objectql/src/protocol-meta.test.ts
→ 0 of 5 path(s) hit the register — NOT governed; ordinary queue landing applies.

5. Every check run green or skipped — 36 runs read at perPage: 50, each by its own completed + conclusion, ⛔ never by an aggregate. Test Core (1/6) took 23 min and passed; Lint & Repo Gates 16 min.

6. Flipped draft: false, read back, then armed SQUASH.⚠️ The echo reads method: MERGE regardless — known artifact; the merge-queue build ref is the reading.

What this PR does and does not close

Part of #14683, not Fixes — and the tier confirmed the split is honest. On origin/main today neither door the card names (GET /meta/diagnostics untyped, GET /meta/:type/:name/references) forwards an organization at all, so neither door's answer changes here. The gate is now in the right place for when a caller does forward one; the forwarding half lives in packages/rest and belongs to #13753 / #14677.

Follow-ups already filed, so nothing is lost

⭐ One platform reading this landing produced

issue_read get_labels refuses a PR number, which has made PR label reads expensive all shift. search_pull_requests with fields: ["number","labels","draft"] returns both compactly for every open PR by an author in one call — it served as the fresh read and the compare read-back here, at a fraction of a full pull_request_read get.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33706455410 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 40 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type - #14767

Merged
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate
Sep 3, 2026
Merged

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type#14767
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate

Conversation

@os-musk

@os-muskos-musk commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Part of #14683

getMetaItems now applies the allowOrgOverride read gate itself, so a metadata sweep that reads more than one type per request is scoped per type instead of per request.

Clause-②: yes. No new exported symbol and no new key on a published payload, so the mechanical floor is not hit. The limb that holds is that this change alters the runtime meaning of a spec-described request member without touching the spec: GetMetaItemsRequestSchema.organizationId is described as "Selects the org partition in the ADR-0005 overlay read order…", and after this PR the org partition is not consulted at all for a registry-non-overridable type. The described contract and the served behaviour diverge, with the spec unchanged.

⚠️ Corrected from the first revision of this body, which named the limb as "which rows a caller gets back, with a tenancy dimension". That limb does not hold: a tenancy read-scope behaviour change sits on the human floor rather than the contract-review one, and the accept/refuse criterion is not met either — the same requests are accepted and the same refusals raised. needs:contract-review requested either way.


The change

One line of behaviour, in packages/metadata-protocol/src/protocol.ts:

request=canonicalizeMetaRequestType(request);const{ packageId }=request;constorgId=organizationIdForMetaRead(request.type,request.organizationId);

The two per-arm re-reads of request.organizationId (the active-overlay read and the previewDrafts read) are deleted and both now spend that one resolution — a gate threaded into only one arm would leave the draft preview resurrecting exactly what the active list had stopped serving.

Three placement facts, each load-bearing:

  • After the canonical fold, never before.declaresOrgOverride tolerates the MANIFEST plurals and not the URL-only ones (translations, email_templates have no manifest key). Handed a raw URL segment it answers env-wide for two genuinely org-overridable types — one item in two partitions, addressed by spelling.
  • One resolution, both arms.
  • No new dependency edge.organizationIdForMetaRead was already imported into protocol.ts (line 57) from @objectstack/metadata-core, already a workspace:* dependency of packages/metadata-protocol. Nothing moved between packages, no cycle. (Measured, not assumed — this was a declared stop condition.)

The harm class is RESURRECTION, not concealment

Carried forward from triage deliberately, because a fix written against the opposite premise would aim at the wrong failure.

SysMetadataRepository.history() filters organization_id by strict equality, so naming the tenant therehides an allowOrgOverride: false type's rows. On this path the two queryByOrg reads are UNIONed, so naming it can only add — and what it adds are the pre-#6190 phantoms: org-scoped rows of types with no per-org read channel, which loadMetaFromDb walks past and reportUnhydratableOrgScopedRows exists to warn about. Read back, they surface inside a clearance rendered before a destructive action, where a resurrected row is worse than an omission because it reads as evidence.


⭐ The idempotence proof (the tripwire)

Direction A was ruled conditional on showing that moving the predicate inside does not change the scope any already-gating call site receives. Verdict: it does not. No gating call site's scope moves.

The measured call-site population

Counted on this branch's own tree, not taken on faith:

#sitereaches getMetaItems?effect of this change
1rest-server.ts:3172/layersno — getMetaItemLayereduntouched
2rest-server.ts:4823GET /meta/:type listyes (:4839)unchanged — proof below
3rest-server.ts:5661 by-name readno — getMetaItem / getMetaItemCacheduntouched
4rest-server.ts:6517/historyno — historyMetaItemuntouched
5rest-server.ts:7085/diffno — diffMetaItemuntouched
6protocol.ts:11091 search sweep's page readyes (:11118)unchanged — page is non-overridable, both readings are undefined

organizationIdForMetaRead has FIVE call sites in rest-server.ts on origin/main, not six. The sixth is the page read inside packages/metadata-protocol itself. Both readings of "six" are now reconciled: five in that file, six in the tree. (PR #14677 would make it six in the file; it has not landed.)

The argument

Let f(t, o) = organizationIdForMetaRead(t, o). f answers either o or undefined, so f(t, f(t, o)) === f(t, o) for every t and o — a second application over the same type is an algebraic no-op. The load-bearing half is therefore "the same type", and it holds for both sites that reach the method:

  • Site 2 gates on canonicalMetaUrlType(req.params.type) and then passes type: req.params.type, the raw segment. The first statement of getMetaItems folds that segment through canonicalizeMetaRequestType, which iscanonicalMetaUrlType — the identical map, so request.type inside is the identical string the door gated on.
  • Site 6 gates on 'page' and passes 'page'.

The proof is executable, not prose

packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts §3 measures both halves over the complete accepted-spelling population — 61 spellings, derived from META_URL_TO_SINGULAR unioned with the registry, so a new type or a changed fold cannot slip past:

  • §3a (61 cases) — the algebra, per spelling.
  • §3b (61 cases) — the behavioural half. For every spelling, a caller that gates first receives partitions equal to what the pre-change implementation read for that same argument (which it spent verbatim, so the expectation is the old behaviour written out).
  • §3c (61 cases) — the complement: an ungated caller is narrowed. Without it the suite would pass on an implementation that changed nothing.

§3b is green on both sides of the ablation, and that is its positive content — a case green with and without the change is exactly the demonstration the tripwire asked for.


What the change DOES move — and the semver derivation

Callers that pass a raw active organization are narrowed for non-overridable types. Enumerated mechanically from call sites rather than from the card's table:

callermulti-type?today
runtime/domains/meta.ts:921 — dispatcher GET /metadata/:typeone per request, any typeraw org ⇒ narrowed
runtime/domains/packages.ts:1160assemblePackageManifest export sweepyes, every plural keyraw org ⇒ narrowed
runtime/domains/packages.ts:603 — ADR-0045 publish visibility flip readapp onlyraw org ⇒ narrowed
getMetaDiagnostics untyped sweepyes, whole registryno org passed by REST today ⇒ correct when one is forwarded
findReferencesToMetayes, per matcher.fromTypeno org passed by REST today ⇒ correct when one is forwarded

Semver: patch — derived from this change's own lineage. A published /meta read door's row set changing is not a new class here; it is the class this predicate was born in, and all three landed instances shipped patch:

commitwhat changedlevel
b6c769019 (#9454 / #9727)the row set every /meta read door returns — org rows addedmetadata-core, metadata-protocol, rest: all patch
26f3588fb (#10340 / #10519)which partition two spellings read — rows movedrest, metadata-core: patch
67ceb9aef (#11553)the same fold-before-scope repair on the dispatcher doorruntime: patch

The first is the commit that introduced organizationIdForMetaRead itself. Adding the org partition to every read door was patch; moving which partition two spellings read was patch; withholding the org partition from types that never had a read channel for it is the same class one verb further in, and takes the same level.

Not minor.scripts/check-changeset-no-major.mjs refuses major outright, so during the launch window a genuinely breaking change ships as minor (pre-1.0, whole-stack lockstep) — #13925 is exactly that: "@objectstack/core": minor, carrying a bolded incompatibility banner and an adr-0087: marker for a narrowed published accept set.

But the implication runs one way only, and the gate's own header is explicit that it does: during the window minor is the union of ordinary new-functionality bumps and banner-marked breaking ones — 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps carrying no banner at all — so the bump level "tells a consumer nothing about whether the release breaks them". The carriers of breaking-ness are the bolded banner in the changeset body and the ADR-0087 disposition: "during the window they are the only signal there is".

⇒ So minor here would not claim an incompatibility — it would claim nothing about compatibility, which is precisely the cost the header names. This change carries neither carrier because it owes neither: nothing is retired, no accept set narrows, and check-adr-0087-registration reads it as non-breaking. The level is patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted.

⚠️ The previous revision of this paragraph asserted that minormeans "breaking" here and that claiming it would signal an incompatibility. That affirmed the consequent — breaking ⇒ minor holds, minor ⇒ breaking does not — and contradicted the very file it cited. Corrected above; the conclusion (patch) is unchanged, only the reason it rests on.

Nothing here is incompatible, and the reason is what the withheld rows are. They are the #6190 phantoms: org-scoped rows of types with no per-org read channel. The platform has refused to mint them since ac244ad09 / 6155c3c24, boot hydration skips them, reportUnhydratableOrgScopedRows audits them, and every REST /meta read door has already withheld them since b6c769019. The only doors still serving them were the dispatcher list (runtime/src/domains/meta.ts:921) and the runtime manifest and publish-flip reads (packages.ts:1160, :603) — so this change aligns those three with the published /meta surface rather than departing from it. A consumer reading those rows was reading through a door inconsistent with /meta, on data the platform had already ruled dead.

⛔ Not "only a refactor of where the predicate lives" either — triage refused that sentence and it is not the reason for the level. The predicate's new position does change which rows three doors serve; that is why this is a behaviour entry rather than an internal note, and the lineage above is why its level is patch.

⚠️Corrected from the first revision of this body, which claimed minor on the strength of a precedent that does not exist: issue 13973 (named without a link so this PR does not pull an unrelated blocked card into its timeline — the withdrawal is checkable as written) is still open and blocked, never landed on main, and its subject is driver date materialisation. It says nothing about read doors at any level. That citation is withdrawn rather than replaced; the three landed commits above are the real lineage. The same revision also mis-stated #13925 as carrying a major bump, which check-changeset-no-major.mjs forbids outright; it shipped minor, as above.


Reverse verification

Mutation: the gate line replaced by const orgId = request.organizationId; — i.e. the pre-change behaviour restored, in packages/metadata-protocol/src/protocol.ts.

Predicted in writing before mutating: 54 red / 140 green, named — §1 phantom case (1), §2 draft case (1), §3c for the 50 spellings folding to a non-overridable type, §4 sweep (1), §5 sources (1).

Observed: exactly 54 failed / 140 passed (194), and the named set matched — §1×1, §2×1, §3×50, §4×1, §5×1, with zero §3b failures.

Discipline on both legs:

  • Mutation confirmed on disk, not by an editor exit code: the gated line went 1 -> 0 and the injected line 0 -> 1, asserted before the run; a miss aborts and voids the reading. ⚠️ To be exact about that second anchor: the injected string carried an // ABLATION: gate removed suffix, and it is that exact string which went 0 -> 1. The bare statement const orgId = request.organizationId; already occurs twice in protocol.ts (getMetaItem at :7348 and getMetaItemLayered at :7760), so counted bare it went 2 -> 3. The suffix is what made the anchor unambiguous; the first revision of this body quoted the count without saying so.
  • Resolution path measured, not assumed. The test imports ./protocol.js — same-package relative, so it reads source. Demonstrated positively: dist/index.js still contained the gate throughout the mutated run (grep count 1), so a dist-mediated test could not have gone red. It did.
  • Restore proved by state, not exit code: git checkout HEAD -- PATH with PATH absolute (naming HEAD so a written index cannot hand the mutation back), then git hash-object compared against the HEAD blob — 13b37b5d468e9bcff6c9edd588a2699bac36b157, matched — plus git diff HEAD empty. An empty hash is read as failure, never as "nothing to compare".
  • trap ... EXIT INT TERM with an absolute repo root resolved via git rev-parse --show-toplevel, so a foreground-cap SIGTERM mid-mutation cannot leave the tree mutated.

Fixture triage

Two existing cases asserted a union the platform must no longer perform. Both were re-spelled, not deleted — the invariant each pins is unchanged and simply has to be measured on a type that has an org partition:

  • packages/metadata-protocol/src/meta-overlay-cache.test.ts §7 "an org-scoped read does not answer from the env-wide entry" — object to view. The key-separation invariant survives; on object it would now be asserting a separation the platform deliberately does not have.
  • packages/objectql/src/protocol-meta.test.ts "getMetaItems unions env-wide and org-specific rows" — app to view. This is the read-side twin of the [#6190] re-spelling three cases up in the same file. Its two getMetaItem (singular) siblings keep app on purpose: that verb is untouched here.

Scanned by the rule's consumption radius rather than by the edited package: every *.test.ts in the tree naming both getMetaItems and organizationId (22 files) was enumerated and the affected packages run.

Verification

origin/main merge base 5258b63f8. Union re-run at 3bbeb09f0 (git rev-parse --short HEAD at the time of the run) — the commit carrying the whole code tree. The commits after it are changeset prose only and touch nothing under packages/**, so the runs below still describe this PR's code exactly.

runresult
@objectstack/metadata-protocol full suite157 files / 2335 passed, 2 skipped
new pin file get-meta-items-org-read-gate.test.ts194 passed
@objectstack/objectql (4 affected files)169 passed
@objectstack/rest (6 affected files)190 passed
@objectstack/runtime (2 affected files)267 passed
@objectstack/metadata-protocol typecheckclean — and it does reach the new test file (it reported TS2345 there before the fix, so this is not a phantom pass)
@objectstack/objectql typecheckclean
pnpm lint (repo-wide eslint . --no-inline-config)clean — run whole, no narrowing claimed

Gate families re-derived on the FINAL file list with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed — the script takes its own change set from the merge base): 38 families, harvested with --commands so neither spelling nor the convention block could be dropped. The claim-time seed named 23; the extra families come from the changeset and from the "adds or edits a test file" convention block.

36 of 38 green. Two remain NOT MEASURED, in the gates' own words — neither is a pass and neither is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved turbo run test log; "running the family locally, record this gate as NOT MEASURED".
  • node scripts/pm/check-half-states.mjs — exit 3: the instrument refuses to run in an agent container.

Two others exited 3 on the first pass and were converted into real readings by building the workspace closure (turbo run build --filter='./packages/*' --filter='./packages/*/*', 71/71) rather than left unmeasured:

  • pnpm check:type-check-debtgreen: 22 ledger entries re-measured in 141.2s, 755 raw tsc errors, none above its recorded number, "surplus: none — every entry sits exactly at its measurement, so any new error is red". This is the ratchet family the new test file moves.
  • pnpm check:dual-build-cjs-loadsgreen: 102 require entry points across 66 packages load, 610 CJS files parse.

One gate went red and was repaired: check-adr-0087-registration read this changeset's prose citation of #13925's marker as this changeset's own breaking declaration (its detector is /\*\*BREAKING/i plus a line-initial BREAKING[ -]CHANGE). The change retires nothing and declares nothing breaking, so the repair is the wording — ⛔ not an adr-0087: disposition marker for a question this change does not raise.

The changeset has since been re-levelled to patch and its semver reasoning corrected twice (the two ⚠️ notes in the semver section above). After each, the three changeset-sensitive families were re-run on the final tree: node scripts/check-empty-changeset.mjsexit 0, node scripts/check-changeset-no-major.mjsexit 0, node scripts/check-adr-0087-registration.mjsexit 0. Nothing else was re-run, and nothing else needed to be: every commit after 3bbeb09f0 touches only .changeset/getmetaitems-org-read-gate.md, and packages/metadata-protocol/src/protocol.ts is still blob 13b37b5d468e9bcff6c9edd588a2699bac36b157 — the same blob the runs and the ablation above measured.

⚠️ Spelling note for anyone re-running these: there is nocheck:changeset-no-major npm script (root package.json has only check:changeset-gate-self-tests, which runs the --self-tests and not the gate itself). pnpm check:changeset-no-major therefore exits 254 as a missing script, which is not a gate reading. The direct node scripts/check-changeset-no-major.mjs spelling — the one dispatch-gates --commands emits — is the gate.

Scope

packages/rest/** is not touched — the fence held, and it turned out to be a proof obligation rather than an edit, exactly as claimed. packages/spec/** and packages/objectql/src/engine.ts are not touched either. The one file outside packages/metadata-protocol is packages/objectql/src/protocol-meta.test.ts, a forced fixture re-spelling.

The half this PR does not do, hence Part of rather than a closing keyword: the two doors named on the card — GET /meta/diagnostics with no ?type=, and GET /meta/:type/:name/references — pass noorganizationId at all on origin/main today, so nothing about their answers changes here. The gate is now in the right place for whenever a caller does forward one; the forwarding itself lives in packages/rest, which this card fences off. #13753 and #14677 are where that half belongs, and neither is addressed here.

Generated by Claude Code

Snapshot before verification. `organizationIdForMetaRead(request.type,
request.organizationId)` is resolved once, after the canonical fold, and
spent by both the active-overlay arm and the previewDrafts arm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Includes the idempotence proof the direction-A ruling was conditional on,
mechanised over the complete accepted-spelling population.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…n overridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…ridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… signal tokens (#14683)
The body cites #13925's major-plus-marker precedent to REFUSE it. Spelled with
the literal markers, check-adr-0087-registration reads the citation as this
changeset's own declaration and demands an ADR-0087 disposition for a change
that retires nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…] for the probe sites (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 9 documentable anchor(s).

31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 9 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 224f8ea4a0776d72de0003a77695bfb98c2206cdpackageMentionDocs.

Which tree this was computed on

This run read content/docs from cb52c281cb890f79d76a18fdc7e227be37f4b66e — the merge of head fc986acdab8dddc2e351409cd0ade81d34d20893 into base 224f8ea4a0776d72de0003a77695bfb98c2206cd, 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 cb52c281cb890f79d76a18fdc7e227be37f4b66e && git checkout cb52c281cb890f79d76a18fdc7e227be37f4b66e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 224f8ea4a0776d72de0003a77695bfb98c2206cd fc986acdab8dddc2e351409cd0ade81d34d20893 && git checkout -B drift-repro 224f8ea4a0776d72de0003a77695bfb98c2206cd && git merge --no-ff fc986acdab8dddc2e351409cd0ade81d34d20893
node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd

⚠️ 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 224f8ea4a0776d72de0003a77695bfb98c2206cd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — FAIL, adopted verbatim. Narrow: semver only, and it is repairable in prose.

Performed by an isolated reviewer at the configured tier in its own worktree cut from 3bbeb09f0, with no access to this seat's reasoning. This seat is off CONTRACT_REVIEW_TIER, so the verdict is adopted whole. The reviewer's own summary: "a patch round that only touches the changeset text and the PR body would pass." Rework is dispatched; ⛔ the PR stays draft.

The blocking finding — minor is over-declared, and both supporting citations are wrong

#13973 is not a landed precedent. This seat flagged it as a doubt in the review brief rather than letting it pass, and the reviewer confirmed it with evidence: state open, labels priority:p2, pm:blocked, domain:engine, body opens Blocked-by: #14078; git log origin/main --grep=13973 returns nothing; and its subject is Date-materialising drivers vs ISO-text drivers — it says nothing about read doors at any level. The changeset itself cites no number at all, only "the minor precedent", which does not exist.

The landed precedents for exactly this class all shipped patch — and they are this change's own lineage:

commitwhat changed on a published read doorlevel
b6c769019 (#9454/#9727)the row set every /meta read door returns (org rows added)metadata-protocol: patch, rest: patch, metadata-core: patch
26f3588fb (#10340/#10519)which partition two spellings read (rows moved)rest: patch, metadata-core: patch
67ceb9aef (#11553)the same, on the dispatcher doorruntime: patch

Against the other rungs: minor ⇔ a new export (87ad30c10, 3c1bbd2a8); minor + **BREAKING** + adr-0087: ⇔ a published type narrowing (d8024f050). This PR is fix(, adds no export, narrows no type. ⇒ patch.

A second factual error, introduced by the wording-repair commit 9f3244830: the changeset now says #13925's shape "would carry a major bump plus an explicit incompatibility marker". #13925 shipped as "@objectstack/core": **minor** with **BREAKING** and an adr-0087: marker, and scripts/check-changeset-no-major.mjs forbids major outright under pre-1.0 semantics. ⚠️ The pre-repair wording at cd9d3ee89 was accurate — the gate-driven repair broke a claim it was not aimed at. Worth recording as a class: a repair that satisfies a gate can damage a neighbouring sentence the gate does not read.

No BREAKING banner is owed, and the reviewer's reasoning is stronger than the changeset's: the rows a raw-org caller loses are the #6190 phantoms — refused at write since ac244ad09/6155c3c24, skipped by boot hydration, audited by reportUnhydratableOrgScopedRows, and already withheld by every REST /meta read door since b6c769019. The only doors still serving them were the dispatcher list and the runtime manifest/flip reads; this PR aligns them with REST. check-adr-0087-registration is green on the final tree — the repair laundered nothing, because nothing is owed.

What the review verified rather than accepted — the parts that PASS

  • ⭐ The idempotence tripwire is genuinely discharged, not circular. The reviewer read the pre-change protocol.ts (5258b63f8: :6811const orgId = (request as any).organizationId, :6873-6874 the two queryByOrg calls) and confirmed §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate, OVERRIDABLE, or the post-fold type. The "same type" half verified too: canonicalizeMetaRequestTypecanonicalMetaTypecanonicalMetaUrlType, the identical map the door gated on.
  • The ablation reproduced independently: 194 total, 54 failed / 140 passed, per section §1:1 §2:1 §3c:50 §4:1 §5:1, and §3b 0 of 61 on both legs — exactly the shape the tripwire asked for. Restore proved by blob hash 13b37b5d… and empty git diff HEAD.
  • ⭐ Both re-spellings are legitimate, and this was measured rather than argued: the reviewer ran the pre-change versions of both test files against the new protocol.ts and got exactly one failure each — the case that was re-spelled — and nothing else. app really did roll back to allowOrgOverride: false in ee58392e1 (ADR-0005:57 白名单表与注册表的分歧不止 flow:page/app/action/permission/tool/skill 六类 ADR 写 ❌ 而 allowOrgOverride:true,另有 dataset/book/position 三类表里没有却默认成了 true #6483/fix(spec): ADR-0005 白名单强制 —— 九类未获批 allowOrgOverride:true 回滚为 false (#6483) #6608). No coverage lost.
  • Call-site enumeration confirmed: five non-test organizationIdForMetaRead sites in rest-server.ts, a sixth at protocol.ts:11117, exactly two reaching getMetaItems; the other seven getMetaItems callers in rest-server.ts pass no organizationId. ⚠️ One caveat stated honestly: the reachability grep was this\.getMetaItems(, so an (this as any).getMetaItems( spelling would have evaded it — none seen, absence not exhaustively proven.
  • Third sweep verified (packages.ts:1160, assemblePackageManifest) and covered by the callee-side gate with no runtime edit. Part of is honest: neither door forwards an organization on origin/main today, so neither door's answer changes with this PR.
  • packages/rest/** and packages/spec/** untouched — the fence held.

Advisory findings


Generated by Claude Code

Contract review returned FAIL on the semver level alone. `minor` was
derived from #13973, which is open, `pm:blocked`, never landed, and is
about date-materialising drivers — it says nothing about read doors.
The real precedents are this predicate's own lineage, and all three
shipped `patch`: b6c7690 (#9454/#9727) added the org partition to every
`/meta` read door; 26f3588 (#10340/#10519) moved which partition two
spellings read; 67ceb9a (#11553) repaired the same on the dispatcher.
Withholding a partition from types that never had a read channel is the
same class.
Also corrects the #13925 claim this file's previous wording-repair commit
introduced: #13925 shipped `"@objectstack/core": minor` with a bolded
banner and an `adr-0087:` marker, not a major bump —
`check-changeset-no-major.mjs` refuses `major` outright, so `minor` in
this repo MEANS breaking, which is why claiming it here would be wrong.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Delta re-review — FAIL again, on one sentence, and it vindicates sending it back

The rework met every mechanical part of the tier's PASS condition — this seat verified that independently before asking (git diff --name-only 3bbeb09f0..000a9861c → one file; -- 'packages/**' → empty; level patch; 13973 × 0). ⛔ It still did not clear the fuse, because a FAIL is not a PASS and this seat is off tier. The rework was sent to the same reviewer as a scoped delta rather than adopted here.

That call is what caught this. The reworked changeset added an argument nobody asked for, and it is false.

The false sentence, and the file that refutes it

.changeset/getmetaitems-org-read-gate.md:59-60:

minor therefore means "breaking" here, and claiming it for this change would signal an incompatibility that does not exist.

scripts/check-changeset-no-major.mjs header lines 44-52 — the source the sentence cites — says the opposite:

Until then it is NOT the carrier, and that is the whole cost of the window: a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them. The mandatory information carriers for breaking-ness in the meantime are the BREAKING banner … and the ADR-0087 migration-ledger disposition …

Affirming the consequent.breaking ⇒ minor is true (header lines 25-27); minor ⇒ breaking is not, and the landed record carries the counter-examples: 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps with zeroBREAKING tokens. So claiming minor would signal nothing about compatibility — exactly the cost the header names — not a false incompatibility.

A false claim about the repo's versioning convention, in a release-notes input, contradicted by its own citation: the same class as the first FAIL. The conclusion (patch) is right; the sentence would teach the next author that every minor is breaking. Round 3 is dispatched to replace lines 59-60 and the PR body's mirror; ⛔ lines 54-58 stay as written.

Everything else in the delta — confirmed by the tier

  • The lineage table is accurate row by row, and the added claim that b6c769019"is the commit that introduced organizationIdForMetaRead itself" was verified: git show b6c769019 -- packages/metadata-core/src/meta-write-org-scope.ts shows +export function organizationIdForMetaRead(, absent from the parent.
  • The [finding] Plugin.type is typed string on a published surface — the eight-value set is enforced by prose only, though packages/core already depends on spec #13925 sentence is accurate and was not blunted by avoiding the literal token. d8024f050 is "@objectstack/core": minor, body line 7 carries the banner, line 35 the adr-0087: marker. Both detector regexes evaluate false on the current body and check-adr-0087-registration is exit 0.
  • The Clause-② restatement matches the tier's own finding — spec-described-semantics limb named, rows/tenancy limb withdrawn with the correct reason.
  • The ablation anchor arithmetic holds, and is subtler than either party first put it. A suffixed injection contains the bare statement as a prefix, so a -cF count on the bare string reads 3 after mutation while the suffixed string reads 1. 2→3 and 0→1 are both correct and consistent, as is 1→0 on the gate line. The dev's precision here was better than the correction it was given.
  • Code byte-identical: protocol.ts's blob at 000a9861c is still 13b37b5d…, the blob the tier ablated.

One advisory

The #13973 withdrawal states accurate facts but is not self-verifiable without naming the card. Suggested: spell it issue 13973 without the #, so a reader can check the withdrawal without this PR auto-linking an unrelated blocked card into its timeline. The dev's call, with a stated reason either way.

⭐ Platform fact, confirmed and now recorded

Root package.json has nocheck:changeset-no-major script — only check:changeset-gate-self-tests (line 120), which runs the three --self-tests and not the gate. So pnpm check:changeset-no-major exiting 254 (ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL … Did you mean "pnpm check:changeset-gate-self-tests"?) is a missing script, not a gate reading. The real gate is the direct node scripts/check-changeset-no-major.mjs, which is also the spelling dispatch-gates --commands emits. The dev reported its own 254 as a spelling error rather than banking it as a verdict — the right reading, and the reason this is now a fact rather than a mystery.


Generated by Claude Code

#14683)
I wrote that `minor` "means breaking" here, and that claiming it would
signal an incompatibility. That affirms the consequent: breaking => minor
holds, minor => breaking does not. check-changeset-no-major.mjs says so in
the header I cited — during the window the bump level "tells a consumer
nothing about whether the release breaks them", and the carriers are the
banner and the ADR-0087 disposition, "the only signal there is".
The landed record carries the other half: 87ad30c ('@objectstack/types':
minor) and 3c1bbd2 ("@objectstack/metadata": minor) are new-export bumps
with zero banner tokens. So `minor` is the union, not a breaking marker.
Corrected to what the gate actually says: the level carries nothing either
way, this change owes neither carrier, and `patch` follows from the lineage
and from no export being added. Lines 54-58 unchanged.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim (round 3)

The isolated reviewer at the configured tier returns PASS on the replacement sentence, judged on that and nothing else. Its words: "The fuse can be cleared in one stroke."

⚠️The fuse is not cleared in this comment. The label writes are read-modify-write with a compare read-back, and the label read path (GraphQL) is rate-limited right now while REST is up. ⛔ Writing a label set without a fresh read is how a concurrent write gets silently stripped, so the clearing waits for the read channel — and nothing is gated on it, because CI on fc986acda is still running (Lint & Repo Gates in progress at the time of writing).

What the tier verified on the final head

Re-derived at fc986acda in a throwaway worktree, not accepted from the seat or the dev:

git diff --name-only 000a9861c..fc986acda → .changeset/getmetaitems-org-read-gate.md
git diff --name-only … -- 'packages/**' → (empty)
protocol.ts blob → 13b37b5d… (still the blob it ablated)
level → "@objectstack/metadata-protocol": patch
'therefore *means* "breaking"' → 0 · 'ONE WAY ONLY' → 1 · '13973' → 0
both ADR-0087 detector regexes → false
check-empty-changeset · check-changeset-no-major · check-adr-0087-registration → exit 0

check-adr-0087-registration --base 5258b63f8 reports "1 non-breaking changeset(s) seen".

⭐ The replacement was checked line by line against its own source

This is the part worth recording, because the two earlier FAILs were both citations that contradicted the file they cited:

the new text sayscheck-changeset-no-major.mjs says
"the implication runs ONE WAY ONLY, and the gate's own header is explicit that it does"lines 46-47: "a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them"
minor is the union of new-functionality and banner-marked breaking bumpsverified last round: 87ad30c10, 3c1bbd2a8 are minor with zeroBREAKING tokens
"tells a consumer nothing about whether the release breaks them" / "during the window they are the only signal there is"verbatim at header lines 46-47 and 52
"which is precisely the cost the header names"line 45: "that is the whole cost of the window"
"patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted"the tier's own stated PASS condition — and it no longer affirms the consequent

One nit, explicitly not a fourth round

"check-adr-0087-registration reads it as non-breaking" describes what the gate detects — the absence of a declared breaking marker — which is slightly weaker than "non-breaking" simpliciter. Paired with the preceding "nothing is retired, no accept set narrows" (both true; the request accept set is unchanged), the sentence is not misleading. ⛔ Recorded, not actioned.

On the PR body

The tier judged it as found and confirms every statement in it matches something verified across the three rounds. ⚠️ It explicitly makes no claim about how the body came to be updated, and neither does this seat: the dev reported its publish blocked, the body nonetheless carries all three staged edits, and the working hypothesis — that the write landed while the tool's response path failed — is unproven. Recorded as an open platform question rather than a fact, because the consequence if true is real: "retry on error" could double-apply a mutation that already succeeded.


⭐ Three rounds, three FAILs, and the code was correct from the first oneprotocol.ts's blob never changed after 3bbeb09f0. Every finding landed in the prose explaining why the code is correct, and prose is what compiles into release notes. That is the contract review doing exactly the job it exists for.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 3, 2026 01:30
@os-musk
os-musk enabled auto-merge September 3, 2026 01:30
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at fc986acda.

1. Contract review PASS on round 3, adopted verbatim from an isolated reviewer at the required tier (5518644676). ⭐ Three rounds, three verdicts, and the code was correct from the first oneprotocol.ts is still blob 13b37b5d…, the blob the reviewer ablated at 3bbeb09f0. Every finding landed in the prose explaining why the code is right, and prose is what compiles into release notes.

2. The fuse cleared from BOTH carriers in one stroke, read-modify-write with a compare read-back: card #14683 and this PR. ⛔ It stayed on through two FAILs — a FAIL does not clear a fuse, only a PASS does.

3. The tripwire the ruling made this conditional on is discharged, and the tier confirmed it is not circular. It read the pre-change protocol.ts (5258b63f8:6811, :6873-6874) and verified §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate or OVERRIDABLE. It reproduced the ablation independently: 54 failed / 140 passed, §3b 0 of 61 on both legs, §3c 50 of 61, restore blob-hash matched.

4. Governed-surface predicate, re-run on the FINAL 5-file list — ⛔ not recalled:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/getmetaitems-org-read-gate.md \
packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts \
packages/metadata-protocol/src/meta-overlay-cache.test.ts \
packages/metadata-protocol/src/protocol.ts \
packages/objectql/src/protocol-meta.test.ts
→ 0 of 5 path(s) hit the register — NOT governed; ordinary queue landing applies.

5. Every check run green or skipped — 36 runs read at perPage: 50, each by its own completed + conclusion, ⛔ never by an aggregate. Test Core (1/6) took 23 min and passed; Lint & Repo Gates 16 min.

6. Flipped draft: false, read back, then armed SQUASH.⚠️ The echo reads method: MERGE regardless — known artifact; the merge-queue build ref is the reading.

What this PR does and does not close

Part of #14683, not Fixes — and the tier confirmed the split is honest. On origin/main today neither door the card names (GET /meta/diagnostics untyped, GET /meta/:type/:name/references) forwards an organization at all, so neither door's answer changes here. The gate is now in the right place for when a caller does forward one; the forwarding half lives in packages/rest and belongs to #13753 / #14677.

Follow-ups already filed, so nothing is lost

⭐ One platform reading this landing produced

issue_read get_labels refuses a PR number, which has made PR label reads expensive all shift. search_pull_requests with fields: ["number","labels","draft"] returns both compactly for every open PR by an author in one call — it served as the fresh read and the compare read-back here, at a fraction of a full pull_request_read get.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33706455410 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 40 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type - #14767

Merged
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate
Sep 3, 2026
Merged

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type#14767
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate

Conversation

@os-musk

@os-muskos-musk commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Part of #14683

getMetaItems now applies the allowOrgOverride read gate itself, so a metadata sweep that reads more than one type per request is scoped per type instead of per request.

Clause-②: yes. No new exported symbol and no new key on a published payload, so the mechanical floor is not hit. The limb that holds is that this change alters the runtime meaning of a spec-described request member without touching the spec: GetMetaItemsRequestSchema.organizationId is described as "Selects the org partition in the ADR-0005 overlay read order…", and after this PR the org partition is not consulted at all for a registry-non-overridable type. The described contract and the served behaviour diverge, with the spec unchanged.

⚠️ Corrected from the first revision of this body, which named the limb as "which rows a caller gets back, with a tenancy dimension". That limb does not hold: a tenancy read-scope behaviour change sits on the human floor rather than the contract-review one, and the accept/refuse criterion is not met either — the same requests are accepted and the same refusals raised. needs:contract-review requested either way.


The change

One line of behaviour, in packages/metadata-protocol/src/protocol.ts:

request=canonicalizeMetaRequestType(request);const{ packageId }=request;constorgId=organizationIdForMetaRead(request.type,request.organizationId);

The two per-arm re-reads of request.organizationId (the active-overlay read and the previewDrafts read) are deleted and both now spend that one resolution — a gate threaded into only one arm would leave the draft preview resurrecting exactly what the active list had stopped serving.

Three placement facts, each load-bearing:

  • After the canonical fold, never before.declaresOrgOverride tolerates the MANIFEST plurals and not the URL-only ones (translations, email_templates have no manifest key). Handed a raw URL segment it answers env-wide for two genuinely org-overridable types — one item in two partitions, addressed by spelling.
  • One resolution, both arms.
  • No new dependency edge.organizationIdForMetaRead was already imported into protocol.ts (line 57) from @objectstack/metadata-core, already a workspace:* dependency of packages/metadata-protocol. Nothing moved between packages, no cycle. (Measured, not assumed — this was a declared stop condition.)

The harm class is RESURRECTION, not concealment

Carried forward from triage deliberately, because a fix written against the opposite premise would aim at the wrong failure.

SysMetadataRepository.history() filters organization_id by strict equality, so naming the tenant therehides an allowOrgOverride: false type's rows. On this path the two queryByOrg reads are UNIONed, so naming it can only add — and what it adds are the pre-#6190 phantoms: org-scoped rows of types with no per-org read channel, which loadMetaFromDb walks past and reportUnhydratableOrgScopedRows exists to warn about. Read back, they surface inside a clearance rendered before a destructive action, where a resurrected row is worse than an omission because it reads as evidence.


⭐ The idempotence proof (the tripwire)

Direction A was ruled conditional on showing that moving the predicate inside does not change the scope any already-gating call site receives. Verdict: it does not. No gating call site's scope moves.

The measured call-site population

Counted on this branch's own tree, not taken on faith:

#sitereaches getMetaItems?effect of this change
1rest-server.ts:3172/layersno — getMetaItemLayereduntouched
2rest-server.ts:4823GET /meta/:type listyes (:4839)unchanged — proof below
3rest-server.ts:5661 by-name readno — getMetaItem / getMetaItemCacheduntouched
4rest-server.ts:6517/historyno — historyMetaItemuntouched
5rest-server.ts:7085/diffno — diffMetaItemuntouched
6protocol.ts:11091 search sweep's page readyes (:11118)unchanged — page is non-overridable, both readings are undefined

organizationIdForMetaRead has FIVE call sites in rest-server.ts on origin/main, not six. The sixth is the page read inside packages/metadata-protocol itself. Both readings of "six" are now reconciled: five in that file, six in the tree. (PR #14677 would make it six in the file; it has not landed.)

The argument

Let f(t, o) = organizationIdForMetaRead(t, o). f answers either o or undefined, so f(t, f(t, o)) === f(t, o) for every t and o — a second application over the same type is an algebraic no-op. The load-bearing half is therefore "the same type", and it holds for both sites that reach the method:

  • Site 2 gates on canonicalMetaUrlType(req.params.type) and then passes type: req.params.type, the raw segment. The first statement of getMetaItems folds that segment through canonicalizeMetaRequestType, which iscanonicalMetaUrlType — the identical map, so request.type inside is the identical string the door gated on.
  • Site 6 gates on 'page' and passes 'page'.

The proof is executable, not prose

packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts §3 measures both halves over the complete accepted-spelling population — 61 spellings, derived from META_URL_TO_SINGULAR unioned with the registry, so a new type or a changed fold cannot slip past:

  • §3a (61 cases) — the algebra, per spelling.
  • §3b (61 cases) — the behavioural half. For every spelling, a caller that gates first receives partitions equal to what the pre-change implementation read for that same argument (which it spent verbatim, so the expectation is the old behaviour written out).
  • §3c (61 cases) — the complement: an ungated caller is narrowed. Without it the suite would pass on an implementation that changed nothing.

§3b is green on both sides of the ablation, and that is its positive content — a case green with and without the change is exactly the demonstration the tripwire asked for.


What the change DOES move — and the semver derivation

Callers that pass a raw active organization are narrowed for non-overridable types. Enumerated mechanically from call sites rather than from the card's table:

callermulti-type?today
runtime/domains/meta.ts:921 — dispatcher GET /metadata/:typeone per request, any typeraw org ⇒ narrowed
runtime/domains/packages.ts:1160assemblePackageManifest export sweepyes, every plural keyraw org ⇒ narrowed
runtime/domains/packages.ts:603 — ADR-0045 publish visibility flip readapp onlyraw org ⇒ narrowed
getMetaDiagnostics untyped sweepyes, whole registryno org passed by REST today ⇒ correct when one is forwarded
findReferencesToMetayes, per matcher.fromTypeno org passed by REST today ⇒ correct when one is forwarded

Semver: patch — derived from this change's own lineage. A published /meta read door's row set changing is not a new class here; it is the class this predicate was born in, and all three landed instances shipped patch:

commitwhat changedlevel
b6c769019 (#9454 / #9727)the row set every /meta read door returns — org rows addedmetadata-core, metadata-protocol, rest: all patch
26f3588fb (#10340 / #10519)which partition two spellings read — rows movedrest, metadata-core: patch
67ceb9aef (#11553)the same fold-before-scope repair on the dispatcher doorruntime: patch

The first is the commit that introduced organizationIdForMetaRead itself. Adding the org partition to every read door was patch; moving which partition two spellings read was patch; withholding the org partition from types that never had a read channel for it is the same class one verb further in, and takes the same level.

Not minor.scripts/check-changeset-no-major.mjs refuses major outright, so during the launch window a genuinely breaking change ships as minor (pre-1.0, whole-stack lockstep) — #13925 is exactly that: "@objectstack/core": minor, carrying a bolded incompatibility banner and an adr-0087: marker for a narrowed published accept set.

But the implication runs one way only, and the gate's own header is explicit that it does: during the window minor is the union of ordinary new-functionality bumps and banner-marked breaking ones — 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps carrying no banner at all — so the bump level "tells a consumer nothing about whether the release breaks them". The carriers of breaking-ness are the bolded banner in the changeset body and the ADR-0087 disposition: "during the window they are the only signal there is".

⇒ So minor here would not claim an incompatibility — it would claim nothing about compatibility, which is precisely the cost the header names. This change carries neither carrier because it owes neither: nothing is retired, no accept set narrows, and check-adr-0087-registration reads it as non-breaking. The level is patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted.

⚠️ The previous revision of this paragraph asserted that minormeans "breaking" here and that claiming it would signal an incompatibility. That affirmed the consequent — breaking ⇒ minor holds, minor ⇒ breaking does not — and contradicted the very file it cited. Corrected above; the conclusion (patch) is unchanged, only the reason it rests on.

Nothing here is incompatible, and the reason is what the withheld rows are. They are the #6190 phantoms: org-scoped rows of types with no per-org read channel. The platform has refused to mint them since ac244ad09 / 6155c3c24, boot hydration skips them, reportUnhydratableOrgScopedRows audits them, and every REST /meta read door has already withheld them since b6c769019. The only doors still serving them were the dispatcher list (runtime/src/domains/meta.ts:921) and the runtime manifest and publish-flip reads (packages.ts:1160, :603) — so this change aligns those three with the published /meta surface rather than departing from it. A consumer reading those rows was reading through a door inconsistent with /meta, on data the platform had already ruled dead.

⛔ Not "only a refactor of where the predicate lives" either — triage refused that sentence and it is not the reason for the level. The predicate's new position does change which rows three doors serve; that is why this is a behaviour entry rather than an internal note, and the lineage above is why its level is patch.

⚠️Corrected from the first revision of this body, which claimed minor on the strength of a precedent that does not exist: issue 13973 (named without a link so this PR does not pull an unrelated blocked card into its timeline — the withdrawal is checkable as written) is still open and blocked, never landed on main, and its subject is driver date materialisation. It says nothing about read doors at any level. That citation is withdrawn rather than replaced; the three landed commits above are the real lineage. The same revision also mis-stated #13925 as carrying a major bump, which check-changeset-no-major.mjs forbids outright; it shipped minor, as above.


Reverse verification

Mutation: the gate line replaced by const orgId = request.organizationId; — i.e. the pre-change behaviour restored, in packages/metadata-protocol/src/protocol.ts.

Predicted in writing before mutating: 54 red / 140 green, named — §1 phantom case (1), §2 draft case (1), §3c for the 50 spellings folding to a non-overridable type, §4 sweep (1), §5 sources (1).

Observed: exactly 54 failed / 140 passed (194), and the named set matched — §1×1, §2×1, §3×50, §4×1, §5×1, with zero §3b failures.

Discipline on both legs:

  • Mutation confirmed on disk, not by an editor exit code: the gated line went 1 -> 0 and the injected line 0 -> 1, asserted before the run; a miss aborts and voids the reading. ⚠️ To be exact about that second anchor: the injected string carried an // ABLATION: gate removed suffix, and it is that exact string which went 0 -> 1. The bare statement const orgId = request.organizationId; already occurs twice in protocol.ts (getMetaItem at :7348 and getMetaItemLayered at :7760), so counted bare it went 2 -> 3. The suffix is what made the anchor unambiguous; the first revision of this body quoted the count without saying so.
  • Resolution path measured, not assumed. The test imports ./protocol.js — same-package relative, so it reads source. Demonstrated positively: dist/index.js still contained the gate throughout the mutated run (grep count 1), so a dist-mediated test could not have gone red. It did.
  • Restore proved by state, not exit code: git checkout HEAD -- PATH with PATH absolute (naming HEAD so a written index cannot hand the mutation back), then git hash-object compared against the HEAD blob — 13b37b5d468e9bcff6c9edd588a2699bac36b157, matched — plus git diff HEAD empty. An empty hash is read as failure, never as "nothing to compare".
  • trap ... EXIT INT TERM with an absolute repo root resolved via git rev-parse --show-toplevel, so a foreground-cap SIGTERM mid-mutation cannot leave the tree mutated.

Fixture triage

Two existing cases asserted a union the platform must no longer perform. Both were re-spelled, not deleted — the invariant each pins is unchanged and simply has to be measured on a type that has an org partition:

  • packages/metadata-protocol/src/meta-overlay-cache.test.ts §7 "an org-scoped read does not answer from the env-wide entry" — object to view. The key-separation invariant survives; on object it would now be asserting a separation the platform deliberately does not have.
  • packages/objectql/src/protocol-meta.test.ts "getMetaItems unions env-wide and org-specific rows" — app to view. This is the read-side twin of the [#6190] re-spelling three cases up in the same file. Its two getMetaItem (singular) siblings keep app on purpose: that verb is untouched here.

Scanned by the rule's consumption radius rather than by the edited package: every *.test.ts in the tree naming both getMetaItems and organizationId (22 files) was enumerated and the affected packages run.

Verification

origin/main merge base 5258b63f8. Union re-run at 3bbeb09f0 (git rev-parse --short HEAD at the time of the run) — the commit carrying the whole code tree. The commits after it are changeset prose only and touch nothing under packages/**, so the runs below still describe this PR's code exactly.

runresult
@objectstack/metadata-protocol full suite157 files / 2335 passed, 2 skipped
new pin file get-meta-items-org-read-gate.test.ts194 passed
@objectstack/objectql (4 affected files)169 passed
@objectstack/rest (6 affected files)190 passed
@objectstack/runtime (2 affected files)267 passed
@objectstack/metadata-protocol typecheckclean — and it does reach the new test file (it reported TS2345 there before the fix, so this is not a phantom pass)
@objectstack/objectql typecheckclean
pnpm lint (repo-wide eslint . --no-inline-config)clean — run whole, no narrowing claimed

Gate families re-derived on the FINAL file list with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed — the script takes its own change set from the merge base): 38 families, harvested with --commands so neither spelling nor the convention block could be dropped. The claim-time seed named 23; the extra families come from the changeset and from the "adds or edits a test file" convention block.

36 of 38 green. Two remain NOT MEASURED, in the gates' own words — neither is a pass and neither is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved turbo run test log; "running the family locally, record this gate as NOT MEASURED".
  • node scripts/pm/check-half-states.mjs — exit 3: the instrument refuses to run in an agent container.

Two others exited 3 on the first pass and were converted into real readings by building the workspace closure (turbo run build --filter='./packages/*' --filter='./packages/*/*', 71/71) rather than left unmeasured:

  • pnpm check:type-check-debtgreen: 22 ledger entries re-measured in 141.2s, 755 raw tsc errors, none above its recorded number, "surplus: none — every entry sits exactly at its measurement, so any new error is red". This is the ratchet family the new test file moves.
  • pnpm check:dual-build-cjs-loadsgreen: 102 require entry points across 66 packages load, 610 CJS files parse.

One gate went red and was repaired: check-adr-0087-registration read this changeset's prose citation of #13925's marker as this changeset's own breaking declaration (its detector is /\*\*BREAKING/i plus a line-initial BREAKING[ -]CHANGE). The change retires nothing and declares nothing breaking, so the repair is the wording — ⛔ not an adr-0087: disposition marker for a question this change does not raise.

The changeset has since been re-levelled to patch and its semver reasoning corrected twice (the two ⚠️ notes in the semver section above). After each, the three changeset-sensitive families were re-run on the final tree: node scripts/check-empty-changeset.mjsexit 0, node scripts/check-changeset-no-major.mjsexit 0, node scripts/check-adr-0087-registration.mjsexit 0. Nothing else was re-run, and nothing else needed to be: every commit after 3bbeb09f0 touches only .changeset/getmetaitems-org-read-gate.md, and packages/metadata-protocol/src/protocol.ts is still blob 13b37b5d468e9bcff6c9edd588a2699bac36b157 — the same blob the runs and the ablation above measured.

⚠️ Spelling note for anyone re-running these: there is nocheck:changeset-no-major npm script (root package.json has only check:changeset-gate-self-tests, which runs the --self-tests and not the gate itself). pnpm check:changeset-no-major therefore exits 254 as a missing script, which is not a gate reading. The direct node scripts/check-changeset-no-major.mjs spelling — the one dispatch-gates --commands emits — is the gate.

Scope

packages/rest/** is not touched — the fence held, and it turned out to be a proof obligation rather than an edit, exactly as claimed. packages/spec/** and packages/objectql/src/engine.ts are not touched either. The one file outside packages/metadata-protocol is packages/objectql/src/protocol-meta.test.ts, a forced fixture re-spelling.

The half this PR does not do, hence Part of rather than a closing keyword: the two doors named on the card — GET /meta/diagnostics with no ?type=, and GET /meta/:type/:name/references — pass noorganizationId at all on origin/main today, so nothing about their answers changes here. The gate is now in the right place for whenever a caller does forward one; the forwarding itself lives in packages/rest, which this card fences off. #13753 and #14677 are where that half belongs, and neither is addressed here.

Generated by Claude Code

Snapshot before verification. `organizationIdForMetaRead(request.type,
request.organizationId)` is resolved once, after the canonical fold, and
spent by both the active-overlay arm and the previewDrafts arm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Includes the idempotence proof the direction-A ruling was conditional on,
mechanised over the complete accepted-spelling population.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…n overridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…ridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… signal tokens (#14683)
The body cites #13925's major-plus-marker precedent to REFUSE it. Spelled with
the literal markers, check-adr-0087-registration reads the citation as this
changeset's own declaration and demands an ADR-0087 disposition for a change
that retires nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…] for the probe sites (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 9 documentable anchor(s).

31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 9 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 224f8ea4a0776d72de0003a77695bfb98c2206cdpackageMentionDocs.

Which tree this was computed on

This run read content/docs from cb52c281cb890f79d76a18fdc7e227be37f4b66e — the merge of head fc986acdab8dddc2e351409cd0ade81d34d20893 into base 224f8ea4a0776d72de0003a77695bfb98c2206cd, 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 cb52c281cb890f79d76a18fdc7e227be37f4b66e && git checkout cb52c281cb890f79d76a18fdc7e227be37f4b66e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 224f8ea4a0776d72de0003a77695bfb98c2206cd fc986acdab8dddc2e351409cd0ade81d34d20893 && git checkout -B drift-repro 224f8ea4a0776d72de0003a77695bfb98c2206cd && git merge --no-ff fc986acdab8dddc2e351409cd0ade81d34d20893
node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd

⚠️ 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 224f8ea4a0776d72de0003a77695bfb98c2206cd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — FAIL, adopted verbatim. Narrow: semver only, and it is repairable in prose.

Performed by an isolated reviewer at the configured tier in its own worktree cut from 3bbeb09f0, with no access to this seat's reasoning. This seat is off CONTRACT_REVIEW_TIER, so the verdict is adopted whole. The reviewer's own summary: "a patch round that only touches the changeset text and the PR body would pass." Rework is dispatched; ⛔ the PR stays draft.

The blocking finding — minor is over-declared, and both supporting citations are wrong

#13973 is not a landed precedent. This seat flagged it as a doubt in the review brief rather than letting it pass, and the reviewer confirmed it with evidence: state open, labels priority:p2, pm:blocked, domain:engine, body opens Blocked-by: #14078; git log origin/main --grep=13973 returns nothing; and its subject is Date-materialising drivers vs ISO-text drivers — it says nothing about read doors at any level. The changeset itself cites no number at all, only "the minor precedent", which does not exist.

The landed precedents for exactly this class all shipped patch — and they are this change's own lineage:

commitwhat changed on a published read doorlevel
b6c769019 (#9454/#9727)the row set every /meta read door returns (org rows added)metadata-protocol: patch, rest: patch, metadata-core: patch
26f3588fb (#10340/#10519)which partition two spellings read (rows moved)rest: patch, metadata-core: patch
67ceb9aef (#11553)the same, on the dispatcher doorruntime: patch

Against the other rungs: minor ⇔ a new export (87ad30c10, 3c1bbd2a8); minor + **BREAKING** + adr-0087: ⇔ a published type narrowing (d8024f050). This PR is fix(, adds no export, narrows no type. ⇒ patch.

A second factual error, introduced by the wording-repair commit 9f3244830: the changeset now says #13925's shape "would carry a major bump plus an explicit incompatibility marker". #13925 shipped as "@objectstack/core": **minor** with **BREAKING** and an adr-0087: marker, and scripts/check-changeset-no-major.mjs forbids major outright under pre-1.0 semantics. ⚠️ The pre-repair wording at cd9d3ee89 was accurate — the gate-driven repair broke a claim it was not aimed at. Worth recording as a class: a repair that satisfies a gate can damage a neighbouring sentence the gate does not read.

No BREAKING banner is owed, and the reviewer's reasoning is stronger than the changeset's: the rows a raw-org caller loses are the #6190 phantoms — refused at write since ac244ad09/6155c3c24, skipped by boot hydration, audited by reportUnhydratableOrgScopedRows, and already withheld by every REST /meta read door since b6c769019. The only doors still serving them were the dispatcher list and the runtime manifest/flip reads; this PR aligns them with REST. check-adr-0087-registration is green on the final tree — the repair laundered nothing, because nothing is owed.

What the review verified rather than accepted — the parts that PASS

  • ⭐ The idempotence tripwire is genuinely discharged, not circular. The reviewer read the pre-change protocol.ts (5258b63f8: :6811const orgId = (request as any).organizationId, :6873-6874 the two queryByOrg calls) and confirmed §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate, OVERRIDABLE, or the post-fold type. The "same type" half verified too: canonicalizeMetaRequestTypecanonicalMetaTypecanonicalMetaUrlType, the identical map the door gated on.
  • The ablation reproduced independently: 194 total, 54 failed / 140 passed, per section §1:1 §2:1 §3c:50 §4:1 §5:1, and §3b 0 of 61 on both legs — exactly the shape the tripwire asked for. Restore proved by blob hash 13b37b5d… and empty git diff HEAD.
  • ⭐ Both re-spellings are legitimate, and this was measured rather than argued: the reviewer ran the pre-change versions of both test files against the new protocol.ts and got exactly one failure each — the case that was re-spelled — and nothing else. app really did roll back to allowOrgOverride: false in ee58392e1 (ADR-0005:57 白名单表与注册表的分歧不止 flow:page/app/action/permission/tool/skill 六类 ADR 写 ❌ 而 allowOrgOverride:true,另有 dataset/book/position 三类表里没有却默认成了 true #6483/fix(spec): ADR-0005 白名单强制 —— 九类未获批 allowOrgOverride:true 回滚为 false (#6483) #6608). No coverage lost.
  • Call-site enumeration confirmed: five non-test organizationIdForMetaRead sites in rest-server.ts, a sixth at protocol.ts:11117, exactly two reaching getMetaItems; the other seven getMetaItems callers in rest-server.ts pass no organizationId. ⚠️ One caveat stated honestly: the reachability grep was this\.getMetaItems(, so an (this as any).getMetaItems( spelling would have evaded it — none seen, absence not exhaustively proven.
  • Third sweep verified (packages.ts:1160, assemblePackageManifest) and covered by the callee-side gate with no runtime edit. Part of is honest: neither door forwards an organization on origin/main today, so neither door's answer changes with this PR.
  • packages/rest/** and packages/spec/** untouched — the fence held.

Advisory findings


Generated by Claude Code

Contract review returned FAIL on the semver level alone. `minor` was
derived from #13973, which is open, `pm:blocked`, never landed, and is
about date-materialising drivers — it says nothing about read doors.
The real precedents are this predicate's own lineage, and all three
shipped `patch`: b6c7690 (#9454/#9727) added the org partition to every
`/meta` read door; 26f3588 (#10340/#10519) moved which partition two
spellings read; 67ceb9a (#11553) repaired the same on the dispatcher.
Withholding a partition from types that never had a read channel is the
same class.
Also corrects the #13925 claim this file's previous wording-repair commit
introduced: #13925 shipped `"@objectstack/core": minor` with a bolded
banner and an `adr-0087:` marker, not a major bump —
`check-changeset-no-major.mjs` refuses `major` outright, so `minor` in
this repo MEANS breaking, which is why claiming it here would be wrong.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Delta re-review — FAIL again, on one sentence, and it vindicates sending it back

The rework met every mechanical part of the tier's PASS condition — this seat verified that independently before asking (git diff --name-only 3bbeb09f0..000a9861c → one file; -- 'packages/**' → empty; level patch; 13973 × 0). ⛔ It still did not clear the fuse, because a FAIL is not a PASS and this seat is off tier. The rework was sent to the same reviewer as a scoped delta rather than adopted here.

That call is what caught this. The reworked changeset added an argument nobody asked for, and it is false.

The false sentence, and the file that refutes it

.changeset/getmetaitems-org-read-gate.md:59-60:

minor therefore means "breaking" here, and claiming it for this change would signal an incompatibility that does not exist.

scripts/check-changeset-no-major.mjs header lines 44-52 — the source the sentence cites — says the opposite:

Until then it is NOT the carrier, and that is the whole cost of the window: a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them. The mandatory information carriers for breaking-ness in the meantime are the BREAKING banner … and the ADR-0087 migration-ledger disposition …

Affirming the consequent.breaking ⇒ minor is true (header lines 25-27); minor ⇒ breaking is not, and the landed record carries the counter-examples: 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps with zeroBREAKING tokens. So claiming minor would signal nothing about compatibility — exactly the cost the header names — not a false incompatibility.

A false claim about the repo's versioning convention, in a release-notes input, contradicted by its own citation: the same class as the first FAIL. The conclusion (patch) is right; the sentence would teach the next author that every minor is breaking. Round 3 is dispatched to replace lines 59-60 and the PR body's mirror; ⛔ lines 54-58 stay as written.

Everything else in the delta — confirmed by the tier

  • The lineage table is accurate row by row, and the added claim that b6c769019"is the commit that introduced organizationIdForMetaRead itself" was verified: git show b6c769019 -- packages/metadata-core/src/meta-write-org-scope.ts shows +export function organizationIdForMetaRead(, absent from the parent.
  • The [finding] Plugin.type is typed string on a published surface — the eight-value set is enforced by prose only, though packages/core already depends on spec #13925 sentence is accurate and was not blunted by avoiding the literal token. d8024f050 is "@objectstack/core": minor, body line 7 carries the banner, line 35 the adr-0087: marker. Both detector regexes evaluate false on the current body and check-adr-0087-registration is exit 0.
  • The Clause-② restatement matches the tier's own finding — spec-described-semantics limb named, rows/tenancy limb withdrawn with the correct reason.
  • The ablation anchor arithmetic holds, and is subtler than either party first put it. A suffixed injection contains the bare statement as a prefix, so a -cF count on the bare string reads 3 after mutation while the suffixed string reads 1. 2→3 and 0→1 are both correct and consistent, as is 1→0 on the gate line. The dev's precision here was better than the correction it was given.
  • Code byte-identical: protocol.ts's blob at 000a9861c is still 13b37b5d…, the blob the tier ablated.

One advisory

The #13973 withdrawal states accurate facts but is not self-verifiable without naming the card. Suggested: spell it issue 13973 without the #, so a reader can check the withdrawal without this PR auto-linking an unrelated blocked card into its timeline. The dev's call, with a stated reason either way.

⭐ Platform fact, confirmed and now recorded

Root package.json has nocheck:changeset-no-major script — only check:changeset-gate-self-tests (line 120), which runs the three --self-tests and not the gate. So pnpm check:changeset-no-major exiting 254 (ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL … Did you mean "pnpm check:changeset-gate-self-tests"?) is a missing script, not a gate reading. The real gate is the direct node scripts/check-changeset-no-major.mjs, which is also the spelling dispatch-gates --commands emits. The dev reported its own 254 as a spelling error rather than banking it as a verdict — the right reading, and the reason this is now a fact rather than a mystery.


Generated by Claude Code

#14683)
I wrote that `minor` "means breaking" here, and that claiming it would
signal an incompatibility. That affirms the consequent: breaking => minor
holds, minor => breaking does not. check-changeset-no-major.mjs says so in
the header I cited — during the window the bump level "tells a consumer
nothing about whether the release breaks them", and the carriers are the
banner and the ADR-0087 disposition, "the only signal there is".
The landed record carries the other half: 87ad30c ('@objectstack/types':
minor) and 3c1bbd2 ("@objectstack/metadata": minor) are new-export bumps
with zero banner tokens. So `minor` is the union, not a breaking marker.
Corrected to what the gate actually says: the level carries nothing either
way, this change owes neither carrier, and `patch` follows from the lineage
and from no export being added. Lines 54-58 unchanged.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim (round 3)

The isolated reviewer at the configured tier returns PASS on the replacement sentence, judged on that and nothing else. Its words: "The fuse can be cleared in one stroke."

⚠️The fuse is not cleared in this comment. The label writes are read-modify-write with a compare read-back, and the label read path (GraphQL) is rate-limited right now while REST is up. ⛔ Writing a label set without a fresh read is how a concurrent write gets silently stripped, so the clearing waits for the read channel — and nothing is gated on it, because CI on fc986acda is still running (Lint & Repo Gates in progress at the time of writing).

What the tier verified on the final head

Re-derived at fc986acda in a throwaway worktree, not accepted from the seat or the dev:

git diff --name-only 000a9861c..fc986acda → .changeset/getmetaitems-org-read-gate.md
git diff --name-only … -- 'packages/**' → (empty)
protocol.ts blob → 13b37b5d… (still the blob it ablated)
level → "@objectstack/metadata-protocol": patch
'therefore *means* "breaking"' → 0 · 'ONE WAY ONLY' → 1 · '13973' → 0
both ADR-0087 detector regexes → false
check-empty-changeset · check-changeset-no-major · check-adr-0087-registration → exit 0

check-adr-0087-registration --base 5258b63f8 reports "1 non-breaking changeset(s) seen".

⭐ The replacement was checked line by line against its own source

This is the part worth recording, because the two earlier FAILs were both citations that contradicted the file they cited:

the new text sayscheck-changeset-no-major.mjs says
"the implication runs ONE WAY ONLY, and the gate's own header is explicit that it does"lines 46-47: "a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them"
minor is the union of new-functionality and banner-marked breaking bumpsverified last round: 87ad30c10, 3c1bbd2a8 are minor with zeroBREAKING tokens
"tells a consumer nothing about whether the release breaks them" / "during the window they are the only signal there is"verbatim at header lines 46-47 and 52
"which is precisely the cost the header names"line 45: "that is the whole cost of the window"
"patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted"the tier's own stated PASS condition — and it no longer affirms the consequent

One nit, explicitly not a fourth round

"check-adr-0087-registration reads it as non-breaking" describes what the gate detects — the absence of a declared breaking marker — which is slightly weaker than "non-breaking" simpliciter. Paired with the preceding "nothing is retired, no accept set narrows" (both true; the request accept set is unchanged), the sentence is not misleading. ⛔ Recorded, not actioned.

On the PR body

The tier judged it as found and confirms every statement in it matches something verified across the three rounds. ⚠️ It explicitly makes no claim about how the body came to be updated, and neither does this seat: the dev reported its publish blocked, the body nonetheless carries all three staged edits, and the working hypothesis — that the write landed while the tool's response path failed — is unproven. Recorded as an open platform question rather than a fact, because the consequence if true is real: "retry on error" could double-apply a mutation that already succeeded.


⭐ Three rounds, three FAILs, and the code was correct from the first oneprotocol.ts's blob never changed after 3bbeb09f0. Every finding landed in the prose explaining why the code is correct, and prose is what compiles into release notes. That is the contract review doing exactly the job it exists for.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 3, 2026 01:30
@os-musk
os-musk enabled auto-merge September 3, 2026 01:30
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at fc986acda.

1. Contract review PASS on round 3, adopted verbatim from an isolated reviewer at the required tier (5518644676). ⭐ Three rounds, three verdicts, and the code was correct from the first oneprotocol.ts is still blob 13b37b5d…, the blob the reviewer ablated at 3bbeb09f0. Every finding landed in the prose explaining why the code is right, and prose is what compiles into release notes.

2. The fuse cleared from BOTH carriers in one stroke, read-modify-write with a compare read-back: card #14683 and this PR. ⛔ It stayed on through two FAILs — a FAIL does not clear a fuse, only a PASS does.

3. The tripwire the ruling made this conditional on is discharged, and the tier confirmed it is not circular. It read the pre-change protocol.ts (5258b63f8:6811, :6873-6874) and verified §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate or OVERRIDABLE. It reproduced the ablation independently: 54 failed / 140 passed, §3b 0 of 61 on both legs, §3c 50 of 61, restore blob-hash matched.

4. Governed-surface predicate, re-run on the FINAL 5-file list — ⛔ not recalled:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/getmetaitems-org-read-gate.md \
packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts \
packages/metadata-protocol/src/meta-overlay-cache.test.ts \
packages/metadata-protocol/src/protocol.ts \
packages/objectql/src/protocol-meta.test.ts
→ 0 of 5 path(s) hit the register — NOT governed; ordinary queue landing applies.

5. Every check run green or skipped — 36 runs read at perPage: 50, each by its own completed + conclusion, ⛔ never by an aggregate. Test Core (1/6) took 23 min and passed; Lint & Repo Gates 16 min.

6. Flipped draft: false, read back, then armed SQUASH.⚠️ The echo reads method: MERGE regardless — known artifact; the merge-queue build ref is the reading.

What this PR does and does not close

Part of #14683, not Fixes — and the tier confirmed the split is honest. On origin/main today neither door the card names (GET /meta/diagnostics untyped, GET /meta/:type/:name/references) forwards an organization at all, so neither door's answer changes here. The gate is now in the right place for when a caller does forward one; the forwarding half lives in packages/rest and belongs to #13753 / #14677.

Follow-ups already filed, so nothing is lost

⭐ One platform reading this landing produced

issue_read get_labels refuses a PR number, which has made PR label reads expensive all shift. search_pull_requests with fields: ["number","labels","draft"] returns both compactly for every open PR by an author in one call — it served as the fresh read and the compare read-back here, at a fraction of a full pull_request_read get.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33706455410 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 40 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type - #14767

Merged
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate
Sep 3, 2026
Merged

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type#14767
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate

Conversation

@os-musk

@os-muskos-musk commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Part of #14683

getMetaItems now applies the allowOrgOverride read gate itself, so a metadata sweep that reads more than one type per request is scoped per type instead of per request.

Clause-②: yes. No new exported symbol and no new key on a published payload, so the mechanical floor is not hit. The limb that holds is that this change alters the runtime meaning of a spec-described request member without touching the spec: GetMetaItemsRequestSchema.organizationId is described as "Selects the org partition in the ADR-0005 overlay read order…", and after this PR the org partition is not consulted at all for a registry-non-overridable type. The described contract and the served behaviour diverge, with the spec unchanged.

⚠️ Corrected from the first revision of this body, which named the limb as "which rows a caller gets back, with a tenancy dimension". That limb does not hold: a tenancy read-scope behaviour change sits on the human floor rather than the contract-review one, and the accept/refuse criterion is not met either — the same requests are accepted and the same refusals raised. needs:contract-review requested either way.


The change

One line of behaviour, in packages/metadata-protocol/src/protocol.ts:

request=canonicalizeMetaRequestType(request);const{ packageId }=request;constorgId=organizationIdForMetaRead(request.type,request.organizationId);

The two per-arm re-reads of request.organizationId (the active-overlay read and the previewDrafts read) are deleted and both now spend that one resolution — a gate threaded into only one arm would leave the draft preview resurrecting exactly what the active list had stopped serving.

Three placement facts, each load-bearing:

  • After the canonical fold, never before.declaresOrgOverride tolerates the MANIFEST plurals and not the URL-only ones (translations, email_templates have no manifest key). Handed a raw URL segment it answers env-wide for two genuinely org-overridable types — one item in two partitions, addressed by spelling.
  • One resolution, both arms.
  • No new dependency edge.organizationIdForMetaRead was already imported into protocol.ts (line 57) from @objectstack/metadata-core, already a workspace:* dependency of packages/metadata-protocol. Nothing moved between packages, no cycle. (Measured, not assumed — this was a declared stop condition.)

The harm class is RESURRECTION, not concealment

Carried forward from triage deliberately, because a fix written against the opposite premise would aim at the wrong failure.

SysMetadataRepository.history() filters organization_id by strict equality, so naming the tenant therehides an allowOrgOverride: false type's rows. On this path the two queryByOrg reads are UNIONed, so naming it can only add — and what it adds are the pre-#6190 phantoms: org-scoped rows of types with no per-org read channel, which loadMetaFromDb walks past and reportUnhydratableOrgScopedRows exists to warn about. Read back, they surface inside a clearance rendered before a destructive action, where a resurrected row is worse than an omission because it reads as evidence.


⭐ The idempotence proof (the tripwire)

Direction A was ruled conditional on showing that moving the predicate inside does not change the scope any already-gating call site receives. Verdict: it does not. No gating call site's scope moves.

The measured call-site population

Counted on this branch's own tree, not taken on faith:

#sitereaches getMetaItems?effect of this change
1rest-server.ts:3172/layersno — getMetaItemLayereduntouched
2rest-server.ts:4823GET /meta/:type listyes (:4839)unchanged — proof below
3rest-server.ts:5661 by-name readno — getMetaItem / getMetaItemCacheduntouched
4rest-server.ts:6517/historyno — historyMetaItemuntouched
5rest-server.ts:7085/diffno — diffMetaItemuntouched
6protocol.ts:11091 search sweep's page readyes (:11118)unchanged — page is non-overridable, both readings are undefined

organizationIdForMetaRead has FIVE call sites in rest-server.ts on origin/main, not six. The sixth is the page read inside packages/metadata-protocol itself. Both readings of "six" are now reconciled: five in that file, six in the tree. (PR #14677 would make it six in the file; it has not landed.)

The argument

Let f(t, o) = organizationIdForMetaRead(t, o). f answers either o or undefined, so f(t, f(t, o)) === f(t, o) for every t and o — a second application over the same type is an algebraic no-op. The load-bearing half is therefore "the same type", and it holds for both sites that reach the method:

  • Site 2 gates on canonicalMetaUrlType(req.params.type) and then passes type: req.params.type, the raw segment. The first statement of getMetaItems folds that segment through canonicalizeMetaRequestType, which iscanonicalMetaUrlType — the identical map, so request.type inside is the identical string the door gated on.
  • Site 6 gates on 'page' and passes 'page'.

The proof is executable, not prose

packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts §3 measures both halves over the complete accepted-spelling population — 61 spellings, derived from META_URL_TO_SINGULAR unioned with the registry, so a new type or a changed fold cannot slip past:

  • §3a (61 cases) — the algebra, per spelling.
  • §3b (61 cases) — the behavioural half. For every spelling, a caller that gates first receives partitions equal to what the pre-change implementation read for that same argument (which it spent verbatim, so the expectation is the old behaviour written out).
  • §3c (61 cases) — the complement: an ungated caller is narrowed. Without it the suite would pass on an implementation that changed nothing.

§3b is green on both sides of the ablation, and that is its positive content — a case green with and without the change is exactly the demonstration the tripwire asked for.


What the change DOES move — and the semver derivation

Callers that pass a raw active organization are narrowed for non-overridable types. Enumerated mechanically from call sites rather than from the card's table:

callermulti-type?today
runtime/domains/meta.ts:921 — dispatcher GET /metadata/:typeone per request, any typeraw org ⇒ narrowed
runtime/domains/packages.ts:1160assemblePackageManifest export sweepyes, every plural keyraw org ⇒ narrowed
runtime/domains/packages.ts:603 — ADR-0045 publish visibility flip readapp onlyraw org ⇒ narrowed
getMetaDiagnostics untyped sweepyes, whole registryno org passed by REST today ⇒ correct when one is forwarded
findReferencesToMetayes, per matcher.fromTypeno org passed by REST today ⇒ correct when one is forwarded

Semver: patch — derived from this change's own lineage. A published /meta read door's row set changing is not a new class here; it is the class this predicate was born in, and all three landed instances shipped patch:

commitwhat changedlevel
b6c769019 (#9454 / #9727)the row set every /meta read door returns — org rows addedmetadata-core, metadata-protocol, rest: all patch
26f3588fb (#10340 / #10519)which partition two spellings read — rows movedrest, metadata-core: patch
67ceb9aef (#11553)the same fold-before-scope repair on the dispatcher doorruntime: patch

The first is the commit that introduced organizationIdForMetaRead itself. Adding the org partition to every read door was patch; moving which partition two spellings read was patch; withholding the org partition from types that never had a read channel for it is the same class one verb further in, and takes the same level.

Not minor.scripts/check-changeset-no-major.mjs refuses major outright, so during the launch window a genuinely breaking change ships as minor (pre-1.0, whole-stack lockstep) — #13925 is exactly that: "@objectstack/core": minor, carrying a bolded incompatibility banner and an adr-0087: marker for a narrowed published accept set.

But the implication runs one way only, and the gate's own header is explicit that it does: during the window minor is the union of ordinary new-functionality bumps and banner-marked breaking ones — 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps carrying no banner at all — so the bump level "tells a consumer nothing about whether the release breaks them". The carriers of breaking-ness are the bolded banner in the changeset body and the ADR-0087 disposition: "during the window they are the only signal there is".

⇒ So minor here would not claim an incompatibility — it would claim nothing about compatibility, which is precisely the cost the header names. This change carries neither carrier because it owes neither: nothing is retired, no accept set narrows, and check-adr-0087-registration reads it as non-breaking. The level is patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted.

⚠️ The previous revision of this paragraph asserted that minormeans "breaking" here and that claiming it would signal an incompatibility. That affirmed the consequent — breaking ⇒ minor holds, minor ⇒ breaking does not — and contradicted the very file it cited. Corrected above; the conclusion (patch) is unchanged, only the reason it rests on.

Nothing here is incompatible, and the reason is what the withheld rows are. They are the #6190 phantoms: org-scoped rows of types with no per-org read channel. The platform has refused to mint them since ac244ad09 / 6155c3c24, boot hydration skips them, reportUnhydratableOrgScopedRows audits them, and every REST /meta read door has already withheld them since b6c769019. The only doors still serving them were the dispatcher list (runtime/src/domains/meta.ts:921) and the runtime manifest and publish-flip reads (packages.ts:1160, :603) — so this change aligns those three with the published /meta surface rather than departing from it. A consumer reading those rows was reading through a door inconsistent with /meta, on data the platform had already ruled dead.

⛔ Not "only a refactor of where the predicate lives" either — triage refused that sentence and it is not the reason for the level. The predicate's new position does change which rows three doors serve; that is why this is a behaviour entry rather than an internal note, and the lineage above is why its level is patch.

⚠️Corrected from the first revision of this body, which claimed minor on the strength of a precedent that does not exist: issue 13973 (named without a link so this PR does not pull an unrelated blocked card into its timeline — the withdrawal is checkable as written) is still open and blocked, never landed on main, and its subject is driver date materialisation. It says nothing about read doors at any level. That citation is withdrawn rather than replaced; the three landed commits above are the real lineage. The same revision also mis-stated #13925 as carrying a major bump, which check-changeset-no-major.mjs forbids outright; it shipped minor, as above.


Reverse verification

Mutation: the gate line replaced by const orgId = request.organizationId; — i.e. the pre-change behaviour restored, in packages/metadata-protocol/src/protocol.ts.

Predicted in writing before mutating: 54 red / 140 green, named — §1 phantom case (1), §2 draft case (1), §3c for the 50 spellings folding to a non-overridable type, §4 sweep (1), §5 sources (1).

Observed: exactly 54 failed / 140 passed (194), and the named set matched — §1×1, §2×1, §3×50, §4×1, §5×1, with zero §3b failures.

Discipline on both legs:

  • Mutation confirmed on disk, not by an editor exit code: the gated line went 1 -> 0 and the injected line 0 -> 1, asserted before the run; a miss aborts and voids the reading. ⚠️ To be exact about that second anchor: the injected string carried an // ABLATION: gate removed suffix, and it is that exact string which went 0 -> 1. The bare statement const orgId = request.organizationId; already occurs twice in protocol.ts (getMetaItem at :7348 and getMetaItemLayered at :7760), so counted bare it went 2 -> 3. The suffix is what made the anchor unambiguous; the first revision of this body quoted the count without saying so.
  • Resolution path measured, not assumed. The test imports ./protocol.js — same-package relative, so it reads source. Demonstrated positively: dist/index.js still contained the gate throughout the mutated run (grep count 1), so a dist-mediated test could not have gone red. It did.
  • Restore proved by state, not exit code: git checkout HEAD -- PATH with PATH absolute (naming HEAD so a written index cannot hand the mutation back), then git hash-object compared against the HEAD blob — 13b37b5d468e9bcff6c9edd588a2699bac36b157, matched — plus git diff HEAD empty. An empty hash is read as failure, never as "nothing to compare".
  • trap ... EXIT INT TERM with an absolute repo root resolved via git rev-parse --show-toplevel, so a foreground-cap SIGTERM mid-mutation cannot leave the tree mutated.

Fixture triage

Two existing cases asserted a union the platform must no longer perform. Both were re-spelled, not deleted — the invariant each pins is unchanged and simply has to be measured on a type that has an org partition:

  • packages/metadata-protocol/src/meta-overlay-cache.test.ts §7 "an org-scoped read does not answer from the env-wide entry" — object to view. The key-separation invariant survives; on object it would now be asserting a separation the platform deliberately does not have.
  • packages/objectql/src/protocol-meta.test.ts "getMetaItems unions env-wide and org-specific rows" — app to view. This is the read-side twin of the [#6190] re-spelling three cases up in the same file. Its two getMetaItem (singular) siblings keep app on purpose: that verb is untouched here.

Scanned by the rule's consumption radius rather than by the edited package: every *.test.ts in the tree naming both getMetaItems and organizationId (22 files) was enumerated and the affected packages run.

Verification

origin/main merge base 5258b63f8. Union re-run at 3bbeb09f0 (git rev-parse --short HEAD at the time of the run) — the commit carrying the whole code tree. The commits after it are changeset prose only and touch nothing under packages/**, so the runs below still describe this PR's code exactly.

runresult
@objectstack/metadata-protocol full suite157 files / 2335 passed, 2 skipped
new pin file get-meta-items-org-read-gate.test.ts194 passed
@objectstack/objectql (4 affected files)169 passed
@objectstack/rest (6 affected files)190 passed
@objectstack/runtime (2 affected files)267 passed
@objectstack/metadata-protocol typecheckclean — and it does reach the new test file (it reported TS2345 there before the fix, so this is not a phantom pass)
@objectstack/objectql typecheckclean
pnpm lint (repo-wide eslint . --no-inline-config)clean — run whole, no narrowing claimed

Gate families re-derived on the FINAL file list with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed — the script takes its own change set from the merge base): 38 families, harvested with --commands so neither spelling nor the convention block could be dropped. The claim-time seed named 23; the extra families come from the changeset and from the "adds or edits a test file" convention block.

36 of 38 green. Two remain NOT MEASURED, in the gates' own words — neither is a pass and neither is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved turbo run test log; "running the family locally, record this gate as NOT MEASURED".
  • node scripts/pm/check-half-states.mjs — exit 3: the instrument refuses to run in an agent container.

Two others exited 3 on the first pass and were converted into real readings by building the workspace closure (turbo run build --filter='./packages/*' --filter='./packages/*/*', 71/71) rather than left unmeasured:

  • pnpm check:type-check-debtgreen: 22 ledger entries re-measured in 141.2s, 755 raw tsc errors, none above its recorded number, "surplus: none — every entry sits exactly at its measurement, so any new error is red". This is the ratchet family the new test file moves.
  • pnpm check:dual-build-cjs-loadsgreen: 102 require entry points across 66 packages load, 610 CJS files parse.

One gate went red and was repaired: check-adr-0087-registration read this changeset's prose citation of #13925's marker as this changeset's own breaking declaration (its detector is /\*\*BREAKING/i plus a line-initial BREAKING[ -]CHANGE). The change retires nothing and declares nothing breaking, so the repair is the wording — ⛔ not an adr-0087: disposition marker for a question this change does not raise.

The changeset has since been re-levelled to patch and its semver reasoning corrected twice (the two ⚠️ notes in the semver section above). After each, the three changeset-sensitive families were re-run on the final tree: node scripts/check-empty-changeset.mjsexit 0, node scripts/check-changeset-no-major.mjsexit 0, node scripts/check-adr-0087-registration.mjsexit 0. Nothing else was re-run, and nothing else needed to be: every commit after 3bbeb09f0 touches only .changeset/getmetaitems-org-read-gate.md, and packages/metadata-protocol/src/protocol.ts is still blob 13b37b5d468e9bcff6c9edd588a2699bac36b157 — the same blob the runs and the ablation above measured.

⚠️ Spelling note for anyone re-running these: there is nocheck:changeset-no-major npm script (root package.json has only check:changeset-gate-self-tests, which runs the --self-tests and not the gate itself). pnpm check:changeset-no-major therefore exits 254 as a missing script, which is not a gate reading. The direct node scripts/check-changeset-no-major.mjs spelling — the one dispatch-gates --commands emits — is the gate.

Scope

packages/rest/** is not touched — the fence held, and it turned out to be a proof obligation rather than an edit, exactly as claimed. packages/spec/** and packages/objectql/src/engine.ts are not touched either. The one file outside packages/metadata-protocol is packages/objectql/src/protocol-meta.test.ts, a forced fixture re-spelling.

The half this PR does not do, hence Part of rather than a closing keyword: the two doors named on the card — GET /meta/diagnostics with no ?type=, and GET /meta/:type/:name/references — pass noorganizationId at all on origin/main today, so nothing about their answers changes here. The gate is now in the right place for whenever a caller does forward one; the forwarding itself lives in packages/rest, which this card fences off. #13753 and #14677 are where that half belongs, and neither is addressed here.

Generated by Claude Code

Snapshot before verification. `organizationIdForMetaRead(request.type,
request.organizationId)` is resolved once, after the canonical fold, and
spent by both the active-overlay arm and the previewDrafts arm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Includes the idempotence proof the direction-A ruling was conditional on,
mechanised over the complete accepted-spelling population.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…n overridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…ridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… signal tokens (#14683)
The body cites #13925's major-plus-marker precedent to REFUSE it. Spelled with
the literal markers, check-adr-0087-registration reads the citation as this
changeset's own declaration and demands an ADR-0087 disposition for a change
that retires nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…] for the probe sites (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 9 documentable anchor(s).

31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 9 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 224f8ea4a0776d72de0003a77695bfb98c2206cdpackageMentionDocs.

Which tree this was computed on

This run read content/docs from cb52c281cb890f79d76a18fdc7e227be37f4b66e — the merge of head fc986acdab8dddc2e351409cd0ade81d34d20893 into base 224f8ea4a0776d72de0003a77695bfb98c2206cd, 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 cb52c281cb890f79d76a18fdc7e227be37f4b66e && git checkout cb52c281cb890f79d76a18fdc7e227be37f4b66e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 224f8ea4a0776d72de0003a77695bfb98c2206cd fc986acdab8dddc2e351409cd0ade81d34d20893 && git checkout -B drift-repro 224f8ea4a0776d72de0003a77695bfb98c2206cd && git merge --no-ff fc986acdab8dddc2e351409cd0ade81d34d20893
node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd

⚠️ 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 224f8ea4a0776d72de0003a77695bfb98c2206cd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — FAIL, adopted verbatim. Narrow: semver only, and it is repairable in prose.

Performed by an isolated reviewer at the configured tier in its own worktree cut from 3bbeb09f0, with no access to this seat's reasoning. This seat is off CONTRACT_REVIEW_TIER, so the verdict is adopted whole. The reviewer's own summary: "a patch round that only touches the changeset text and the PR body would pass." Rework is dispatched; ⛔ the PR stays draft.

The blocking finding — minor is over-declared, and both supporting citations are wrong

#13973 is not a landed precedent. This seat flagged it as a doubt in the review brief rather than letting it pass, and the reviewer confirmed it with evidence: state open, labels priority:p2, pm:blocked, domain:engine, body opens Blocked-by: #14078; git log origin/main --grep=13973 returns nothing; and its subject is Date-materialising drivers vs ISO-text drivers — it says nothing about read doors at any level. The changeset itself cites no number at all, only "the minor precedent", which does not exist.

The landed precedents for exactly this class all shipped patch — and they are this change's own lineage:

commitwhat changed on a published read doorlevel
b6c769019 (#9454/#9727)the row set every /meta read door returns (org rows added)metadata-protocol: patch, rest: patch, metadata-core: patch
26f3588fb (#10340/#10519)which partition two spellings read (rows moved)rest: patch, metadata-core: patch
67ceb9aef (#11553)the same, on the dispatcher doorruntime: patch

Against the other rungs: minor ⇔ a new export (87ad30c10, 3c1bbd2a8); minor + **BREAKING** + adr-0087: ⇔ a published type narrowing (d8024f050). This PR is fix(, adds no export, narrows no type. ⇒ patch.

A second factual error, introduced by the wording-repair commit 9f3244830: the changeset now says #13925's shape "would carry a major bump plus an explicit incompatibility marker". #13925 shipped as "@objectstack/core": **minor** with **BREAKING** and an adr-0087: marker, and scripts/check-changeset-no-major.mjs forbids major outright under pre-1.0 semantics. ⚠️ The pre-repair wording at cd9d3ee89 was accurate — the gate-driven repair broke a claim it was not aimed at. Worth recording as a class: a repair that satisfies a gate can damage a neighbouring sentence the gate does not read.

No BREAKING banner is owed, and the reviewer's reasoning is stronger than the changeset's: the rows a raw-org caller loses are the #6190 phantoms — refused at write since ac244ad09/6155c3c24, skipped by boot hydration, audited by reportUnhydratableOrgScopedRows, and already withheld by every REST /meta read door since b6c769019. The only doors still serving them were the dispatcher list and the runtime manifest/flip reads; this PR aligns them with REST. check-adr-0087-registration is green on the final tree — the repair laundered nothing, because nothing is owed.

What the review verified rather than accepted — the parts that PASS

  • ⭐ The idempotence tripwire is genuinely discharged, not circular. The reviewer read the pre-change protocol.ts (5258b63f8: :6811const orgId = (request as any).organizationId, :6873-6874 the two queryByOrg calls) and confirmed §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate, OVERRIDABLE, or the post-fold type. The "same type" half verified too: canonicalizeMetaRequestTypecanonicalMetaTypecanonicalMetaUrlType, the identical map the door gated on.
  • The ablation reproduced independently: 194 total, 54 failed / 140 passed, per section §1:1 §2:1 §3c:50 §4:1 §5:1, and §3b 0 of 61 on both legs — exactly the shape the tripwire asked for. Restore proved by blob hash 13b37b5d… and empty git diff HEAD.
  • ⭐ Both re-spellings are legitimate, and this was measured rather than argued: the reviewer ran the pre-change versions of both test files against the new protocol.ts and got exactly one failure each — the case that was re-spelled — and nothing else. app really did roll back to allowOrgOverride: false in ee58392e1 (ADR-0005:57 白名单表与注册表的分歧不止 flow:page/app/action/permission/tool/skill 六类 ADR 写 ❌ 而 allowOrgOverride:true,另有 dataset/book/position 三类表里没有却默认成了 true #6483/fix(spec): ADR-0005 白名单强制 —— 九类未获批 allowOrgOverride:true 回滚为 false (#6483) #6608). No coverage lost.
  • Call-site enumeration confirmed: five non-test organizationIdForMetaRead sites in rest-server.ts, a sixth at protocol.ts:11117, exactly two reaching getMetaItems; the other seven getMetaItems callers in rest-server.ts pass no organizationId. ⚠️ One caveat stated honestly: the reachability grep was this\.getMetaItems(, so an (this as any).getMetaItems( spelling would have evaded it — none seen, absence not exhaustively proven.
  • Third sweep verified (packages.ts:1160, assemblePackageManifest) and covered by the callee-side gate with no runtime edit. Part of is honest: neither door forwards an organization on origin/main today, so neither door's answer changes with this PR.
  • packages/rest/** and packages/spec/** untouched — the fence held.

Advisory findings


Generated by Claude Code

Contract review returned FAIL on the semver level alone. `minor` was
derived from #13973, which is open, `pm:blocked`, never landed, and is
about date-materialising drivers — it says nothing about read doors.
The real precedents are this predicate's own lineage, and all three
shipped `patch`: b6c7690 (#9454/#9727) added the org partition to every
`/meta` read door; 26f3588 (#10340/#10519) moved which partition two
spellings read; 67ceb9a (#11553) repaired the same on the dispatcher.
Withholding a partition from types that never had a read channel is the
same class.
Also corrects the #13925 claim this file's previous wording-repair commit
introduced: #13925 shipped `"@objectstack/core": minor` with a bolded
banner and an `adr-0087:` marker, not a major bump —
`check-changeset-no-major.mjs` refuses `major` outright, so `minor` in
this repo MEANS breaking, which is why claiming it here would be wrong.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Delta re-review — FAIL again, on one sentence, and it vindicates sending it back

The rework met every mechanical part of the tier's PASS condition — this seat verified that independently before asking (git diff --name-only 3bbeb09f0..000a9861c → one file; -- 'packages/**' → empty; level patch; 13973 × 0). ⛔ It still did not clear the fuse, because a FAIL is not a PASS and this seat is off tier. The rework was sent to the same reviewer as a scoped delta rather than adopted here.

That call is what caught this. The reworked changeset added an argument nobody asked for, and it is false.

The false sentence, and the file that refutes it

.changeset/getmetaitems-org-read-gate.md:59-60:

minor therefore means "breaking" here, and claiming it for this change would signal an incompatibility that does not exist.

scripts/check-changeset-no-major.mjs header lines 44-52 — the source the sentence cites — says the opposite:

Until then it is NOT the carrier, and that is the whole cost of the window: a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them. The mandatory information carriers for breaking-ness in the meantime are the BREAKING banner … and the ADR-0087 migration-ledger disposition …

Affirming the consequent.breaking ⇒ minor is true (header lines 25-27); minor ⇒ breaking is not, and the landed record carries the counter-examples: 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps with zeroBREAKING tokens. So claiming minor would signal nothing about compatibility — exactly the cost the header names — not a false incompatibility.

A false claim about the repo's versioning convention, in a release-notes input, contradicted by its own citation: the same class as the first FAIL. The conclusion (patch) is right; the sentence would teach the next author that every minor is breaking. Round 3 is dispatched to replace lines 59-60 and the PR body's mirror; ⛔ lines 54-58 stay as written.

Everything else in the delta — confirmed by the tier

  • The lineage table is accurate row by row, and the added claim that b6c769019"is the commit that introduced organizationIdForMetaRead itself" was verified: git show b6c769019 -- packages/metadata-core/src/meta-write-org-scope.ts shows +export function organizationIdForMetaRead(, absent from the parent.
  • The [finding] Plugin.type is typed string on a published surface — the eight-value set is enforced by prose only, though packages/core already depends on spec #13925 sentence is accurate and was not blunted by avoiding the literal token. d8024f050 is "@objectstack/core": minor, body line 7 carries the banner, line 35 the adr-0087: marker. Both detector regexes evaluate false on the current body and check-adr-0087-registration is exit 0.
  • The Clause-② restatement matches the tier's own finding — spec-described-semantics limb named, rows/tenancy limb withdrawn with the correct reason.
  • The ablation anchor arithmetic holds, and is subtler than either party first put it. A suffixed injection contains the bare statement as a prefix, so a -cF count on the bare string reads 3 after mutation while the suffixed string reads 1. 2→3 and 0→1 are both correct and consistent, as is 1→0 on the gate line. The dev's precision here was better than the correction it was given.
  • Code byte-identical: protocol.ts's blob at 000a9861c is still 13b37b5d…, the blob the tier ablated.

One advisory

The #13973 withdrawal states accurate facts but is not self-verifiable without naming the card. Suggested: spell it issue 13973 without the #, so a reader can check the withdrawal without this PR auto-linking an unrelated blocked card into its timeline. The dev's call, with a stated reason either way.

⭐ Platform fact, confirmed and now recorded

Root package.json has nocheck:changeset-no-major script — only check:changeset-gate-self-tests (line 120), which runs the three --self-tests and not the gate. So pnpm check:changeset-no-major exiting 254 (ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL … Did you mean "pnpm check:changeset-gate-self-tests"?) is a missing script, not a gate reading. The real gate is the direct node scripts/check-changeset-no-major.mjs, which is also the spelling dispatch-gates --commands emits. The dev reported its own 254 as a spelling error rather than banking it as a verdict — the right reading, and the reason this is now a fact rather than a mystery.


Generated by Claude Code

#14683)
I wrote that `minor` "means breaking" here, and that claiming it would
signal an incompatibility. That affirms the consequent: breaking => minor
holds, minor => breaking does not. check-changeset-no-major.mjs says so in
the header I cited — during the window the bump level "tells a consumer
nothing about whether the release breaks them", and the carriers are the
banner and the ADR-0087 disposition, "the only signal there is".
The landed record carries the other half: 87ad30c ('@objectstack/types':
minor) and 3c1bbd2 ("@objectstack/metadata": minor) are new-export bumps
with zero banner tokens. So `minor` is the union, not a breaking marker.
Corrected to what the gate actually says: the level carries nothing either
way, this change owes neither carrier, and `patch` follows from the lineage
and from no export being added. Lines 54-58 unchanged.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim (round 3)

The isolated reviewer at the configured tier returns PASS on the replacement sentence, judged on that and nothing else. Its words: "The fuse can be cleared in one stroke."

⚠️The fuse is not cleared in this comment. The label writes are read-modify-write with a compare read-back, and the label read path (GraphQL) is rate-limited right now while REST is up. ⛔ Writing a label set without a fresh read is how a concurrent write gets silently stripped, so the clearing waits for the read channel — and nothing is gated on it, because CI on fc986acda is still running (Lint & Repo Gates in progress at the time of writing).

What the tier verified on the final head

Re-derived at fc986acda in a throwaway worktree, not accepted from the seat or the dev:

git diff --name-only 000a9861c..fc986acda → .changeset/getmetaitems-org-read-gate.md
git diff --name-only … -- 'packages/**' → (empty)
protocol.ts blob → 13b37b5d… (still the blob it ablated)
level → "@objectstack/metadata-protocol": patch
'therefore *means* "breaking"' → 0 · 'ONE WAY ONLY' → 1 · '13973' → 0
both ADR-0087 detector regexes → false
check-empty-changeset · check-changeset-no-major · check-adr-0087-registration → exit 0

check-adr-0087-registration --base 5258b63f8 reports "1 non-breaking changeset(s) seen".

⭐ The replacement was checked line by line against its own source

This is the part worth recording, because the two earlier FAILs were both citations that contradicted the file they cited:

the new text sayscheck-changeset-no-major.mjs says
"the implication runs ONE WAY ONLY, and the gate's own header is explicit that it does"lines 46-47: "a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them"
minor is the union of new-functionality and banner-marked breaking bumpsverified last round: 87ad30c10, 3c1bbd2a8 are minor with zeroBREAKING tokens
"tells a consumer nothing about whether the release breaks them" / "during the window they are the only signal there is"verbatim at header lines 46-47 and 52
"which is precisely the cost the header names"line 45: "that is the whole cost of the window"
"patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted"the tier's own stated PASS condition — and it no longer affirms the consequent

One nit, explicitly not a fourth round

"check-adr-0087-registration reads it as non-breaking" describes what the gate detects — the absence of a declared breaking marker — which is slightly weaker than "non-breaking" simpliciter. Paired with the preceding "nothing is retired, no accept set narrows" (both true; the request accept set is unchanged), the sentence is not misleading. ⛔ Recorded, not actioned.

On the PR body

The tier judged it as found and confirms every statement in it matches something verified across the three rounds. ⚠️ It explicitly makes no claim about how the body came to be updated, and neither does this seat: the dev reported its publish blocked, the body nonetheless carries all three staged edits, and the working hypothesis — that the write landed while the tool's response path failed — is unproven. Recorded as an open platform question rather than a fact, because the consequence if true is real: "retry on error" could double-apply a mutation that already succeeded.


⭐ Three rounds, three FAILs, and the code was correct from the first oneprotocol.ts's blob never changed after 3bbeb09f0. Every finding landed in the prose explaining why the code is correct, and prose is what compiles into release notes. That is the contract review doing exactly the job it exists for.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 3, 2026 01:30
@os-musk
os-musk enabled auto-merge September 3, 2026 01:30
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at fc986acda.

1. Contract review PASS on round 3, adopted verbatim from an isolated reviewer at the required tier (5518644676). ⭐ Three rounds, three verdicts, and the code was correct from the first oneprotocol.ts is still blob 13b37b5d…, the blob the reviewer ablated at 3bbeb09f0. Every finding landed in the prose explaining why the code is right, and prose is what compiles into release notes.

2. The fuse cleared from BOTH carriers in one stroke, read-modify-write with a compare read-back: card #14683 and this PR. ⛔ It stayed on through two FAILs — a FAIL does not clear a fuse, only a PASS does.

3. The tripwire the ruling made this conditional on is discharged, and the tier confirmed it is not circular. It read the pre-change protocol.ts (5258b63f8:6811, :6873-6874) and verified §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate or OVERRIDABLE. It reproduced the ablation independently: 54 failed / 140 passed, §3b 0 of 61 on both legs, §3c 50 of 61, restore blob-hash matched.

4. Governed-surface predicate, re-run on the FINAL 5-file list — ⛔ not recalled:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/getmetaitems-org-read-gate.md \
packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts \
packages/metadata-protocol/src/meta-overlay-cache.test.ts \
packages/metadata-protocol/src/protocol.ts \
packages/objectql/src/protocol-meta.test.ts
→ 0 of 5 path(s) hit the register — NOT governed; ordinary queue landing applies.

5. Every check run green or skipped — 36 runs read at perPage: 50, each by its own completed + conclusion, ⛔ never by an aggregate. Test Core (1/6) took 23 min and passed; Lint & Repo Gates 16 min.

6. Flipped draft: false, read back, then armed SQUASH.⚠️ The echo reads method: MERGE regardless — known artifact; the merge-queue build ref is the reading.

What this PR does and does not close

Part of #14683, not Fixes — and the tier confirmed the split is honest. On origin/main today neither door the card names (GET /meta/diagnostics untyped, GET /meta/:type/:name/references) forwards an organization at all, so neither door's answer changes here. The gate is now in the right place for when a caller does forward one; the forwarding half lives in packages/rest and belongs to #13753 / #14677.

Follow-ups already filed, so nothing is lost

⭐ One platform reading this landing produced

issue_read get_labels refuses a PR number, which has made PR label reads expensive all shift. search_pull_requests with fields: ["number","labels","draft"] returns both compactly for every open PR by an author in one call — it served as the fresh read and the compare read-back here, at a fraction of a full pull_request_read get.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33706455410 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 40 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type - #14767

Merged
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate
Sep 3, 2026
Merged

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type#14767
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate

Conversation

@os-musk

@os-muskos-musk commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Part of #14683

getMetaItems now applies the allowOrgOverride read gate itself, so a metadata sweep that reads more than one type per request is scoped per type instead of per request.

Clause-②: yes. No new exported symbol and no new key on a published payload, so the mechanical floor is not hit. The limb that holds is that this change alters the runtime meaning of a spec-described request member without touching the spec: GetMetaItemsRequestSchema.organizationId is described as "Selects the org partition in the ADR-0005 overlay read order…", and after this PR the org partition is not consulted at all for a registry-non-overridable type. The described contract and the served behaviour diverge, with the spec unchanged.

⚠️ Corrected from the first revision of this body, which named the limb as "which rows a caller gets back, with a tenancy dimension". That limb does not hold: a tenancy read-scope behaviour change sits on the human floor rather than the contract-review one, and the accept/refuse criterion is not met either — the same requests are accepted and the same refusals raised. needs:contract-review requested either way.


The change

One line of behaviour, in packages/metadata-protocol/src/protocol.ts:

request=canonicalizeMetaRequestType(request);const{ packageId }=request;constorgId=organizationIdForMetaRead(request.type,request.organizationId);

The two per-arm re-reads of request.organizationId (the active-overlay read and the previewDrafts read) are deleted and both now spend that one resolution — a gate threaded into only one arm would leave the draft preview resurrecting exactly what the active list had stopped serving.

Three placement facts, each load-bearing:

  • After the canonical fold, never before.declaresOrgOverride tolerates the MANIFEST plurals and not the URL-only ones (translations, email_templates have no manifest key). Handed a raw URL segment it answers env-wide for two genuinely org-overridable types — one item in two partitions, addressed by spelling.
  • One resolution, both arms.
  • No new dependency edge.organizationIdForMetaRead was already imported into protocol.ts (line 57) from @objectstack/metadata-core, already a workspace:* dependency of packages/metadata-protocol. Nothing moved between packages, no cycle. (Measured, not assumed — this was a declared stop condition.)

The harm class is RESURRECTION, not concealment

Carried forward from triage deliberately, because a fix written against the opposite premise would aim at the wrong failure.

SysMetadataRepository.history() filters organization_id by strict equality, so naming the tenant therehides an allowOrgOverride: false type's rows. On this path the two queryByOrg reads are UNIONed, so naming it can only add — and what it adds are the pre-#6190 phantoms: org-scoped rows of types with no per-org read channel, which loadMetaFromDb walks past and reportUnhydratableOrgScopedRows exists to warn about. Read back, they surface inside a clearance rendered before a destructive action, where a resurrected row is worse than an omission because it reads as evidence.


⭐ The idempotence proof (the tripwire)

Direction A was ruled conditional on showing that moving the predicate inside does not change the scope any already-gating call site receives. Verdict: it does not. No gating call site's scope moves.

The measured call-site population

Counted on this branch's own tree, not taken on faith:

#sitereaches getMetaItems?effect of this change
1rest-server.ts:3172/layersno — getMetaItemLayereduntouched
2rest-server.ts:4823GET /meta/:type listyes (:4839)unchanged — proof below
3rest-server.ts:5661 by-name readno — getMetaItem / getMetaItemCacheduntouched
4rest-server.ts:6517/historyno — historyMetaItemuntouched
5rest-server.ts:7085/diffno — diffMetaItemuntouched
6protocol.ts:11091 search sweep's page readyes (:11118)unchanged — page is non-overridable, both readings are undefined

organizationIdForMetaRead has FIVE call sites in rest-server.ts on origin/main, not six. The sixth is the page read inside packages/metadata-protocol itself. Both readings of "six" are now reconciled: five in that file, six in the tree. (PR #14677 would make it six in the file; it has not landed.)

The argument

Let f(t, o) = organizationIdForMetaRead(t, o). f answers either o or undefined, so f(t, f(t, o)) === f(t, o) for every t and o — a second application over the same type is an algebraic no-op. The load-bearing half is therefore "the same type", and it holds for both sites that reach the method:

  • Site 2 gates on canonicalMetaUrlType(req.params.type) and then passes type: req.params.type, the raw segment. The first statement of getMetaItems folds that segment through canonicalizeMetaRequestType, which iscanonicalMetaUrlType — the identical map, so request.type inside is the identical string the door gated on.
  • Site 6 gates on 'page' and passes 'page'.

The proof is executable, not prose

packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts §3 measures both halves over the complete accepted-spelling population — 61 spellings, derived from META_URL_TO_SINGULAR unioned with the registry, so a new type or a changed fold cannot slip past:

  • §3a (61 cases) — the algebra, per spelling.
  • §3b (61 cases) — the behavioural half. For every spelling, a caller that gates first receives partitions equal to what the pre-change implementation read for that same argument (which it spent verbatim, so the expectation is the old behaviour written out).
  • §3c (61 cases) — the complement: an ungated caller is narrowed. Without it the suite would pass on an implementation that changed nothing.

§3b is green on both sides of the ablation, and that is its positive content — a case green with and without the change is exactly the demonstration the tripwire asked for.


What the change DOES move — and the semver derivation

Callers that pass a raw active organization are narrowed for non-overridable types. Enumerated mechanically from call sites rather than from the card's table:

callermulti-type?today
runtime/domains/meta.ts:921 — dispatcher GET /metadata/:typeone per request, any typeraw org ⇒ narrowed
runtime/domains/packages.ts:1160assemblePackageManifest export sweepyes, every plural keyraw org ⇒ narrowed
runtime/domains/packages.ts:603 — ADR-0045 publish visibility flip readapp onlyraw org ⇒ narrowed
getMetaDiagnostics untyped sweepyes, whole registryno org passed by REST today ⇒ correct when one is forwarded
findReferencesToMetayes, per matcher.fromTypeno org passed by REST today ⇒ correct when one is forwarded

Semver: patch — derived from this change's own lineage. A published /meta read door's row set changing is not a new class here; it is the class this predicate was born in, and all three landed instances shipped patch:

commitwhat changedlevel
b6c769019 (#9454 / #9727)the row set every /meta read door returns — org rows addedmetadata-core, metadata-protocol, rest: all patch
26f3588fb (#10340 / #10519)which partition two spellings read — rows movedrest, metadata-core: patch
67ceb9aef (#11553)the same fold-before-scope repair on the dispatcher doorruntime: patch

The first is the commit that introduced organizationIdForMetaRead itself. Adding the org partition to every read door was patch; moving which partition two spellings read was patch; withholding the org partition from types that never had a read channel for it is the same class one verb further in, and takes the same level.

Not minor.scripts/check-changeset-no-major.mjs refuses major outright, so during the launch window a genuinely breaking change ships as minor (pre-1.0, whole-stack lockstep) — #13925 is exactly that: "@objectstack/core": minor, carrying a bolded incompatibility banner and an adr-0087: marker for a narrowed published accept set.

But the implication runs one way only, and the gate's own header is explicit that it does: during the window minor is the union of ordinary new-functionality bumps and banner-marked breaking ones — 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps carrying no banner at all — so the bump level "tells a consumer nothing about whether the release breaks them". The carriers of breaking-ness are the bolded banner in the changeset body and the ADR-0087 disposition: "during the window they are the only signal there is".

⇒ So minor here would not claim an incompatibility — it would claim nothing about compatibility, which is precisely the cost the header names. This change carries neither carrier because it owes neither: nothing is retired, no accept set narrows, and check-adr-0087-registration reads it as non-breaking. The level is patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted.

⚠️ The previous revision of this paragraph asserted that minormeans "breaking" here and that claiming it would signal an incompatibility. That affirmed the consequent — breaking ⇒ minor holds, minor ⇒ breaking does not — and contradicted the very file it cited. Corrected above; the conclusion (patch) is unchanged, only the reason it rests on.

Nothing here is incompatible, and the reason is what the withheld rows are. They are the #6190 phantoms: org-scoped rows of types with no per-org read channel. The platform has refused to mint them since ac244ad09 / 6155c3c24, boot hydration skips them, reportUnhydratableOrgScopedRows audits them, and every REST /meta read door has already withheld them since b6c769019. The only doors still serving them were the dispatcher list (runtime/src/domains/meta.ts:921) and the runtime manifest and publish-flip reads (packages.ts:1160, :603) — so this change aligns those three with the published /meta surface rather than departing from it. A consumer reading those rows was reading through a door inconsistent with /meta, on data the platform had already ruled dead.

⛔ Not "only a refactor of where the predicate lives" either — triage refused that sentence and it is not the reason for the level. The predicate's new position does change which rows three doors serve; that is why this is a behaviour entry rather than an internal note, and the lineage above is why its level is patch.

⚠️Corrected from the first revision of this body, which claimed minor on the strength of a precedent that does not exist: issue 13973 (named without a link so this PR does not pull an unrelated blocked card into its timeline — the withdrawal is checkable as written) is still open and blocked, never landed on main, and its subject is driver date materialisation. It says nothing about read doors at any level. That citation is withdrawn rather than replaced; the three landed commits above are the real lineage. The same revision also mis-stated #13925 as carrying a major bump, which check-changeset-no-major.mjs forbids outright; it shipped minor, as above.


Reverse verification

Mutation: the gate line replaced by const orgId = request.organizationId; — i.e. the pre-change behaviour restored, in packages/metadata-protocol/src/protocol.ts.

Predicted in writing before mutating: 54 red / 140 green, named — §1 phantom case (1), §2 draft case (1), §3c for the 50 spellings folding to a non-overridable type, §4 sweep (1), §5 sources (1).

Observed: exactly 54 failed / 140 passed (194), and the named set matched — §1×1, §2×1, §3×50, §4×1, §5×1, with zero §3b failures.

Discipline on both legs:

  • Mutation confirmed on disk, not by an editor exit code: the gated line went 1 -> 0 and the injected line 0 -> 1, asserted before the run; a miss aborts and voids the reading. ⚠️ To be exact about that second anchor: the injected string carried an // ABLATION: gate removed suffix, and it is that exact string which went 0 -> 1. The bare statement const orgId = request.organizationId; already occurs twice in protocol.ts (getMetaItem at :7348 and getMetaItemLayered at :7760), so counted bare it went 2 -> 3. The suffix is what made the anchor unambiguous; the first revision of this body quoted the count without saying so.
  • Resolution path measured, not assumed. The test imports ./protocol.js — same-package relative, so it reads source. Demonstrated positively: dist/index.js still contained the gate throughout the mutated run (grep count 1), so a dist-mediated test could not have gone red. It did.
  • Restore proved by state, not exit code: git checkout HEAD -- PATH with PATH absolute (naming HEAD so a written index cannot hand the mutation back), then git hash-object compared against the HEAD blob — 13b37b5d468e9bcff6c9edd588a2699bac36b157, matched — plus git diff HEAD empty. An empty hash is read as failure, never as "nothing to compare".
  • trap ... EXIT INT TERM with an absolute repo root resolved via git rev-parse --show-toplevel, so a foreground-cap SIGTERM mid-mutation cannot leave the tree mutated.

Fixture triage

Two existing cases asserted a union the platform must no longer perform. Both were re-spelled, not deleted — the invariant each pins is unchanged and simply has to be measured on a type that has an org partition:

  • packages/metadata-protocol/src/meta-overlay-cache.test.ts §7 "an org-scoped read does not answer from the env-wide entry" — object to view. The key-separation invariant survives; on object it would now be asserting a separation the platform deliberately does not have.
  • packages/objectql/src/protocol-meta.test.ts "getMetaItems unions env-wide and org-specific rows" — app to view. This is the read-side twin of the [#6190] re-spelling three cases up in the same file. Its two getMetaItem (singular) siblings keep app on purpose: that verb is untouched here.

Scanned by the rule's consumption radius rather than by the edited package: every *.test.ts in the tree naming both getMetaItems and organizationId (22 files) was enumerated and the affected packages run.

Verification

origin/main merge base 5258b63f8. Union re-run at 3bbeb09f0 (git rev-parse --short HEAD at the time of the run) — the commit carrying the whole code tree. The commits after it are changeset prose only and touch nothing under packages/**, so the runs below still describe this PR's code exactly.

runresult
@objectstack/metadata-protocol full suite157 files / 2335 passed, 2 skipped
new pin file get-meta-items-org-read-gate.test.ts194 passed
@objectstack/objectql (4 affected files)169 passed
@objectstack/rest (6 affected files)190 passed
@objectstack/runtime (2 affected files)267 passed
@objectstack/metadata-protocol typecheckclean — and it does reach the new test file (it reported TS2345 there before the fix, so this is not a phantom pass)
@objectstack/objectql typecheckclean
pnpm lint (repo-wide eslint . --no-inline-config)clean — run whole, no narrowing claimed

Gate families re-derived on the FINAL file list with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed — the script takes its own change set from the merge base): 38 families, harvested with --commands so neither spelling nor the convention block could be dropped. The claim-time seed named 23; the extra families come from the changeset and from the "adds or edits a test file" convention block.

36 of 38 green. Two remain NOT MEASURED, in the gates' own words — neither is a pass and neither is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved turbo run test log; "running the family locally, record this gate as NOT MEASURED".
  • node scripts/pm/check-half-states.mjs — exit 3: the instrument refuses to run in an agent container.

Two others exited 3 on the first pass and were converted into real readings by building the workspace closure (turbo run build --filter='./packages/*' --filter='./packages/*/*', 71/71) rather than left unmeasured:

  • pnpm check:type-check-debtgreen: 22 ledger entries re-measured in 141.2s, 755 raw tsc errors, none above its recorded number, "surplus: none — every entry sits exactly at its measurement, so any new error is red". This is the ratchet family the new test file moves.
  • pnpm check:dual-build-cjs-loadsgreen: 102 require entry points across 66 packages load, 610 CJS files parse.

One gate went red and was repaired: check-adr-0087-registration read this changeset's prose citation of #13925's marker as this changeset's own breaking declaration (its detector is /\*\*BREAKING/i plus a line-initial BREAKING[ -]CHANGE). The change retires nothing and declares nothing breaking, so the repair is the wording — ⛔ not an adr-0087: disposition marker for a question this change does not raise.

The changeset has since been re-levelled to patch and its semver reasoning corrected twice (the two ⚠️ notes in the semver section above). After each, the three changeset-sensitive families were re-run on the final tree: node scripts/check-empty-changeset.mjsexit 0, node scripts/check-changeset-no-major.mjsexit 0, node scripts/check-adr-0087-registration.mjsexit 0. Nothing else was re-run, and nothing else needed to be: every commit after 3bbeb09f0 touches only .changeset/getmetaitems-org-read-gate.md, and packages/metadata-protocol/src/protocol.ts is still blob 13b37b5d468e9bcff6c9edd588a2699bac36b157 — the same blob the runs and the ablation above measured.

⚠️ Spelling note for anyone re-running these: there is nocheck:changeset-no-major npm script (root package.json has only check:changeset-gate-self-tests, which runs the --self-tests and not the gate itself). pnpm check:changeset-no-major therefore exits 254 as a missing script, which is not a gate reading. The direct node scripts/check-changeset-no-major.mjs spelling — the one dispatch-gates --commands emits — is the gate.

Scope

packages/rest/** is not touched — the fence held, and it turned out to be a proof obligation rather than an edit, exactly as claimed. packages/spec/** and packages/objectql/src/engine.ts are not touched either. The one file outside packages/metadata-protocol is packages/objectql/src/protocol-meta.test.ts, a forced fixture re-spelling.

The half this PR does not do, hence Part of rather than a closing keyword: the two doors named on the card — GET /meta/diagnostics with no ?type=, and GET /meta/:type/:name/references — pass noorganizationId at all on origin/main today, so nothing about their answers changes here. The gate is now in the right place for whenever a caller does forward one; the forwarding itself lives in packages/rest, which this card fences off. #13753 and #14677 are where that half belongs, and neither is addressed here.

Generated by Claude Code

Snapshot before verification. `organizationIdForMetaRead(request.type,
request.organizationId)` is resolved once, after the canonical fold, and
spent by both the active-overlay arm and the previewDrafts arm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Includes the idempotence proof the direction-A ruling was conditional on,
mechanised over the complete accepted-spelling population.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…n overridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…ridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… signal tokens (#14683)
The body cites #13925's major-plus-marker precedent to REFUSE it. Spelled with
the literal markers, check-adr-0087-registration reads the citation as this
changeset's own declaration and demands an ADR-0087 disposition for a change
that retires nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…] for the probe sites (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 9 documentable anchor(s).

31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 9 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 224f8ea4a0776d72de0003a77695bfb98c2206cdpackageMentionDocs.

Which tree this was computed on

This run read content/docs from cb52c281cb890f79d76a18fdc7e227be37f4b66e — the merge of head fc986acdab8dddc2e351409cd0ade81d34d20893 into base 224f8ea4a0776d72de0003a77695bfb98c2206cd, 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 cb52c281cb890f79d76a18fdc7e227be37f4b66e && git checkout cb52c281cb890f79d76a18fdc7e227be37f4b66e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 224f8ea4a0776d72de0003a77695bfb98c2206cd fc986acdab8dddc2e351409cd0ade81d34d20893 && git checkout -B drift-repro 224f8ea4a0776d72de0003a77695bfb98c2206cd && git merge --no-ff fc986acdab8dddc2e351409cd0ade81d34d20893
node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd

⚠️ 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 224f8ea4a0776d72de0003a77695bfb98c2206cd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — FAIL, adopted verbatim. Narrow: semver only, and it is repairable in prose.

Performed by an isolated reviewer at the configured tier in its own worktree cut from 3bbeb09f0, with no access to this seat's reasoning. This seat is off CONTRACT_REVIEW_TIER, so the verdict is adopted whole. The reviewer's own summary: "a patch round that only touches the changeset text and the PR body would pass." Rework is dispatched; ⛔ the PR stays draft.

The blocking finding — minor is over-declared, and both supporting citations are wrong

#13973 is not a landed precedent. This seat flagged it as a doubt in the review brief rather than letting it pass, and the reviewer confirmed it with evidence: state open, labels priority:p2, pm:blocked, domain:engine, body opens Blocked-by: #14078; git log origin/main --grep=13973 returns nothing; and its subject is Date-materialising drivers vs ISO-text drivers — it says nothing about read doors at any level. The changeset itself cites no number at all, only "the minor precedent", which does not exist.

The landed precedents for exactly this class all shipped patch — and they are this change's own lineage:

commitwhat changed on a published read doorlevel
b6c769019 (#9454/#9727)the row set every /meta read door returns (org rows added)metadata-protocol: patch, rest: patch, metadata-core: patch
26f3588fb (#10340/#10519)which partition two spellings read (rows moved)rest: patch, metadata-core: patch
67ceb9aef (#11553)the same, on the dispatcher doorruntime: patch

Against the other rungs: minor ⇔ a new export (87ad30c10, 3c1bbd2a8); minor + **BREAKING** + adr-0087: ⇔ a published type narrowing (d8024f050). This PR is fix(, adds no export, narrows no type. ⇒ patch.

A second factual error, introduced by the wording-repair commit 9f3244830: the changeset now says #13925's shape "would carry a major bump plus an explicit incompatibility marker". #13925 shipped as "@objectstack/core": **minor** with **BREAKING** and an adr-0087: marker, and scripts/check-changeset-no-major.mjs forbids major outright under pre-1.0 semantics. ⚠️ The pre-repair wording at cd9d3ee89 was accurate — the gate-driven repair broke a claim it was not aimed at. Worth recording as a class: a repair that satisfies a gate can damage a neighbouring sentence the gate does not read.

No BREAKING banner is owed, and the reviewer's reasoning is stronger than the changeset's: the rows a raw-org caller loses are the #6190 phantoms — refused at write since ac244ad09/6155c3c24, skipped by boot hydration, audited by reportUnhydratableOrgScopedRows, and already withheld by every REST /meta read door since b6c769019. The only doors still serving them were the dispatcher list and the runtime manifest/flip reads; this PR aligns them with REST. check-adr-0087-registration is green on the final tree — the repair laundered nothing, because nothing is owed.

What the review verified rather than accepted — the parts that PASS

  • ⭐ The idempotence tripwire is genuinely discharged, not circular. The reviewer read the pre-change protocol.ts (5258b63f8: :6811const orgId = (request as any).organizationId, :6873-6874 the two queryByOrg calls) and confirmed §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate, OVERRIDABLE, or the post-fold type. The "same type" half verified too: canonicalizeMetaRequestTypecanonicalMetaTypecanonicalMetaUrlType, the identical map the door gated on.
  • The ablation reproduced independently: 194 total, 54 failed / 140 passed, per section §1:1 §2:1 §3c:50 §4:1 §5:1, and §3b 0 of 61 on both legs — exactly the shape the tripwire asked for. Restore proved by blob hash 13b37b5d… and empty git diff HEAD.
  • ⭐ Both re-spellings are legitimate, and this was measured rather than argued: the reviewer ran the pre-change versions of both test files against the new protocol.ts and got exactly one failure each — the case that was re-spelled — and nothing else. app really did roll back to allowOrgOverride: false in ee58392e1 (ADR-0005:57 白名单表与注册表的分歧不止 flow:page/app/action/permission/tool/skill 六类 ADR 写 ❌ 而 allowOrgOverride:true,另有 dataset/book/position 三类表里没有却默认成了 true #6483/fix(spec): ADR-0005 白名单强制 —— 九类未获批 allowOrgOverride:true 回滚为 false (#6483) #6608). No coverage lost.
  • Call-site enumeration confirmed: five non-test organizationIdForMetaRead sites in rest-server.ts, a sixth at protocol.ts:11117, exactly two reaching getMetaItems; the other seven getMetaItems callers in rest-server.ts pass no organizationId. ⚠️ One caveat stated honestly: the reachability grep was this\.getMetaItems(, so an (this as any).getMetaItems( spelling would have evaded it — none seen, absence not exhaustively proven.
  • Third sweep verified (packages.ts:1160, assemblePackageManifest) and covered by the callee-side gate with no runtime edit. Part of is honest: neither door forwards an organization on origin/main today, so neither door's answer changes with this PR.
  • packages/rest/** and packages/spec/** untouched — the fence held.

Advisory findings


Generated by Claude Code

Contract review returned FAIL on the semver level alone. `minor` was
derived from #13973, which is open, `pm:blocked`, never landed, and is
about date-materialising drivers — it says nothing about read doors.
The real precedents are this predicate's own lineage, and all three
shipped `patch`: b6c7690 (#9454/#9727) added the org partition to every
`/meta` read door; 26f3588 (#10340/#10519) moved which partition two
spellings read; 67ceb9a (#11553) repaired the same on the dispatcher.
Withholding a partition from types that never had a read channel is the
same class.
Also corrects the #13925 claim this file's previous wording-repair commit
introduced: #13925 shipped `"@objectstack/core": minor` with a bolded
banner and an `adr-0087:` marker, not a major bump —
`check-changeset-no-major.mjs` refuses `major` outright, so `minor` in
this repo MEANS breaking, which is why claiming it here would be wrong.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Delta re-review — FAIL again, on one sentence, and it vindicates sending it back

The rework met every mechanical part of the tier's PASS condition — this seat verified that independently before asking (git diff --name-only 3bbeb09f0..000a9861c → one file; -- 'packages/**' → empty; level patch; 13973 × 0). ⛔ It still did not clear the fuse, because a FAIL is not a PASS and this seat is off tier. The rework was sent to the same reviewer as a scoped delta rather than adopted here.

That call is what caught this. The reworked changeset added an argument nobody asked for, and it is false.

The false sentence, and the file that refutes it

.changeset/getmetaitems-org-read-gate.md:59-60:

minor therefore means "breaking" here, and claiming it for this change would signal an incompatibility that does not exist.

scripts/check-changeset-no-major.mjs header lines 44-52 — the source the sentence cites — says the opposite:

Until then it is NOT the carrier, and that is the whole cost of the window: a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them. The mandatory information carriers for breaking-ness in the meantime are the BREAKING banner … and the ADR-0087 migration-ledger disposition …

Affirming the consequent.breaking ⇒ minor is true (header lines 25-27); minor ⇒ breaking is not, and the landed record carries the counter-examples: 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps with zeroBREAKING tokens. So claiming minor would signal nothing about compatibility — exactly the cost the header names — not a false incompatibility.

A false claim about the repo's versioning convention, in a release-notes input, contradicted by its own citation: the same class as the first FAIL. The conclusion (patch) is right; the sentence would teach the next author that every minor is breaking. Round 3 is dispatched to replace lines 59-60 and the PR body's mirror; ⛔ lines 54-58 stay as written.

Everything else in the delta — confirmed by the tier

  • The lineage table is accurate row by row, and the added claim that b6c769019"is the commit that introduced organizationIdForMetaRead itself" was verified: git show b6c769019 -- packages/metadata-core/src/meta-write-org-scope.ts shows +export function organizationIdForMetaRead(, absent from the parent.
  • The [finding] Plugin.type is typed string on a published surface — the eight-value set is enforced by prose only, though packages/core already depends on spec #13925 sentence is accurate and was not blunted by avoiding the literal token. d8024f050 is "@objectstack/core": minor, body line 7 carries the banner, line 35 the adr-0087: marker. Both detector regexes evaluate false on the current body and check-adr-0087-registration is exit 0.
  • The Clause-② restatement matches the tier's own finding — spec-described-semantics limb named, rows/tenancy limb withdrawn with the correct reason.
  • The ablation anchor arithmetic holds, and is subtler than either party first put it. A suffixed injection contains the bare statement as a prefix, so a -cF count on the bare string reads 3 after mutation while the suffixed string reads 1. 2→3 and 0→1 are both correct and consistent, as is 1→0 on the gate line. The dev's precision here was better than the correction it was given.
  • Code byte-identical: protocol.ts's blob at 000a9861c is still 13b37b5d…, the blob the tier ablated.

One advisory

The #13973 withdrawal states accurate facts but is not self-verifiable without naming the card. Suggested: spell it issue 13973 without the #, so a reader can check the withdrawal without this PR auto-linking an unrelated blocked card into its timeline. The dev's call, with a stated reason either way.

⭐ Platform fact, confirmed and now recorded

Root package.json has nocheck:changeset-no-major script — only check:changeset-gate-self-tests (line 120), which runs the three --self-tests and not the gate. So pnpm check:changeset-no-major exiting 254 (ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL … Did you mean "pnpm check:changeset-gate-self-tests"?) is a missing script, not a gate reading. The real gate is the direct node scripts/check-changeset-no-major.mjs, which is also the spelling dispatch-gates --commands emits. The dev reported its own 254 as a spelling error rather than banking it as a verdict — the right reading, and the reason this is now a fact rather than a mystery.


Generated by Claude Code

#14683)
I wrote that `minor` "means breaking" here, and that claiming it would
signal an incompatibility. That affirms the consequent: breaking => minor
holds, minor => breaking does not. check-changeset-no-major.mjs says so in
the header I cited — during the window the bump level "tells a consumer
nothing about whether the release breaks them", and the carriers are the
banner and the ADR-0087 disposition, "the only signal there is".
The landed record carries the other half: 87ad30c ('@objectstack/types':
minor) and 3c1bbd2 ("@objectstack/metadata": minor) are new-export bumps
with zero banner tokens. So `minor` is the union, not a breaking marker.
Corrected to what the gate actually says: the level carries nothing either
way, this change owes neither carrier, and `patch` follows from the lineage
and from no export being added. Lines 54-58 unchanged.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim (round 3)

The isolated reviewer at the configured tier returns PASS on the replacement sentence, judged on that and nothing else. Its words: "The fuse can be cleared in one stroke."

⚠️The fuse is not cleared in this comment. The label writes are read-modify-write with a compare read-back, and the label read path (GraphQL) is rate-limited right now while REST is up. ⛔ Writing a label set without a fresh read is how a concurrent write gets silently stripped, so the clearing waits for the read channel — and nothing is gated on it, because CI on fc986acda is still running (Lint & Repo Gates in progress at the time of writing).

What the tier verified on the final head

Re-derived at fc986acda in a throwaway worktree, not accepted from the seat or the dev:

git diff --name-only 000a9861c..fc986acda → .changeset/getmetaitems-org-read-gate.md
git diff --name-only … -- 'packages/**' → (empty)
protocol.ts blob → 13b37b5d… (still the blob it ablated)
level → "@objectstack/metadata-protocol": patch
'therefore *means* "breaking"' → 0 · 'ONE WAY ONLY' → 1 · '13973' → 0
both ADR-0087 detector regexes → false
check-empty-changeset · check-changeset-no-major · check-adr-0087-registration → exit 0

check-adr-0087-registration --base 5258b63f8 reports "1 non-breaking changeset(s) seen".

⭐ The replacement was checked line by line against its own source

This is the part worth recording, because the two earlier FAILs were both citations that contradicted the file they cited:

the new text sayscheck-changeset-no-major.mjs says
"the implication runs ONE WAY ONLY, and the gate's own header is explicit that it does"lines 46-47: "a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them"
minor is the union of new-functionality and banner-marked breaking bumpsverified last round: 87ad30c10, 3c1bbd2a8 are minor with zeroBREAKING tokens
"tells a consumer nothing about whether the release breaks them" / "during the window they are the only signal there is"verbatim at header lines 46-47 and 52
"which is precisely the cost the header names"line 45: "that is the whole cost of the window"
"patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted"the tier's own stated PASS condition — and it no longer affirms the consequent

One nit, explicitly not a fourth round

"check-adr-0087-registration reads it as non-breaking" describes what the gate detects — the absence of a declared breaking marker — which is slightly weaker than "non-breaking" simpliciter. Paired with the preceding "nothing is retired, no accept set narrows" (both true; the request accept set is unchanged), the sentence is not misleading. ⛔ Recorded, not actioned.

On the PR body

The tier judged it as found and confirms every statement in it matches something verified across the three rounds. ⚠️ It explicitly makes no claim about how the body came to be updated, and neither does this seat: the dev reported its publish blocked, the body nonetheless carries all three staged edits, and the working hypothesis — that the write landed while the tool's response path failed — is unproven. Recorded as an open platform question rather than a fact, because the consequence if true is real: "retry on error" could double-apply a mutation that already succeeded.


⭐ Three rounds, three FAILs, and the code was correct from the first oneprotocol.ts's blob never changed after 3bbeb09f0. Every finding landed in the prose explaining why the code is correct, and prose is what compiles into release notes. That is the contract review doing exactly the job it exists for.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 3, 2026 01:30
@os-musk
os-musk enabled auto-merge September 3, 2026 01:30
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at fc986acda.

1. Contract review PASS on round 3, adopted verbatim from an isolated reviewer at the required tier (5518644676). ⭐ Three rounds, three verdicts, and the code was correct from the first oneprotocol.ts is still blob 13b37b5d…, the blob the reviewer ablated at 3bbeb09f0. Every finding landed in the prose explaining why the code is right, and prose is what compiles into release notes.

2. The fuse cleared from BOTH carriers in one stroke, read-modify-write with a compare read-back: card #14683 and this PR. ⛔ It stayed on through two FAILs — a FAIL does not clear a fuse, only a PASS does.

3. The tripwire the ruling made this conditional on is discharged, and the tier confirmed it is not circular. It read the pre-change protocol.ts (5258b63f8:6811, :6873-6874) and verified §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate or OVERRIDABLE. It reproduced the ablation independently: 54 failed / 140 passed, §3b 0 of 61 on both legs, §3c 50 of 61, restore blob-hash matched.

4. Governed-surface predicate, re-run on the FINAL 5-file list — ⛔ not recalled:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/getmetaitems-org-read-gate.md \
packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts \
packages/metadata-protocol/src/meta-overlay-cache.test.ts \
packages/metadata-protocol/src/protocol.ts \
packages/objectql/src/protocol-meta.test.ts
→ 0 of 5 path(s) hit the register — NOT governed; ordinary queue landing applies.

5. Every check run green or skipped — 36 runs read at perPage: 50, each by its own completed + conclusion, ⛔ never by an aggregate. Test Core (1/6) took 23 min and passed; Lint & Repo Gates 16 min.

6. Flipped draft: false, read back, then armed SQUASH.⚠️ The echo reads method: MERGE regardless — known artifact; the merge-queue build ref is the reading.

What this PR does and does not close

Part of #14683, not Fixes — and the tier confirmed the split is honest. On origin/main today neither door the card names (GET /meta/diagnostics untyped, GET /meta/:type/:name/references) forwards an organization at all, so neither door's answer changes here. The gate is now in the right place for when a caller does forward one; the forwarding half lives in packages/rest and belongs to #13753 / #14677.

Follow-ups already filed, so nothing is lost

⭐ One platform reading this landing produced

issue_read get_labels refuses a PR number, which has made PR label reads expensive all shift. search_pull_requests with fields: ["number","labels","draft"] returns both compactly for every open PR by an author in one call — it served as the fresh read and the compare read-back here, at a fraction of a full pull_request_read get.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33706455410 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 40 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type - #14767

Merged
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate
Sep 3, 2026
Merged

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type#14767
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate

Conversation

@os-musk

@os-muskos-musk commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Part of #14683

getMetaItems now applies the allowOrgOverride read gate itself, so a metadata sweep that reads more than one type per request is scoped per type instead of per request.

Clause-②: yes. No new exported symbol and no new key on a published payload, so the mechanical floor is not hit. The limb that holds is that this change alters the runtime meaning of a spec-described request member without touching the spec: GetMetaItemsRequestSchema.organizationId is described as "Selects the org partition in the ADR-0005 overlay read order…", and after this PR the org partition is not consulted at all for a registry-non-overridable type. The described contract and the served behaviour diverge, with the spec unchanged.

⚠️ Corrected from the first revision of this body, which named the limb as "which rows a caller gets back, with a tenancy dimension". That limb does not hold: a tenancy read-scope behaviour change sits on the human floor rather than the contract-review one, and the accept/refuse criterion is not met either — the same requests are accepted and the same refusals raised. needs:contract-review requested either way.


The change

One line of behaviour, in packages/metadata-protocol/src/protocol.ts:

request=canonicalizeMetaRequestType(request);const{ packageId }=request;constorgId=organizationIdForMetaRead(request.type,request.organizationId);

The two per-arm re-reads of request.organizationId (the active-overlay read and the previewDrafts read) are deleted and both now spend that one resolution — a gate threaded into only one arm would leave the draft preview resurrecting exactly what the active list had stopped serving.

Three placement facts, each load-bearing:

  • After the canonical fold, never before.declaresOrgOverride tolerates the MANIFEST plurals and not the URL-only ones (translations, email_templates have no manifest key). Handed a raw URL segment it answers env-wide for two genuinely org-overridable types — one item in two partitions, addressed by spelling.
  • One resolution, both arms.
  • No new dependency edge.organizationIdForMetaRead was already imported into protocol.ts (line 57) from @objectstack/metadata-core, already a workspace:* dependency of packages/metadata-protocol. Nothing moved between packages, no cycle. (Measured, not assumed — this was a declared stop condition.)

The harm class is RESURRECTION, not concealment

Carried forward from triage deliberately, because a fix written against the opposite premise would aim at the wrong failure.

SysMetadataRepository.history() filters organization_id by strict equality, so naming the tenant therehides an allowOrgOverride: false type's rows. On this path the two queryByOrg reads are UNIONed, so naming it can only add — and what it adds are the pre-#6190 phantoms: org-scoped rows of types with no per-org read channel, which loadMetaFromDb walks past and reportUnhydratableOrgScopedRows exists to warn about. Read back, they surface inside a clearance rendered before a destructive action, where a resurrected row is worse than an omission because it reads as evidence.


⭐ The idempotence proof (the tripwire)

Direction A was ruled conditional on showing that moving the predicate inside does not change the scope any already-gating call site receives. Verdict: it does not. No gating call site's scope moves.

The measured call-site population

Counted on this branch's own tree, not taken on faith:

#sitereaches getMetaItems?effect of this change
1rest-server.ts:3172/layersno — getMetaItemLayereduntouched
2rest-server.ts:4823GET /meta/:type listyes (:4839)unchanged — proof below
3rest-server.ts:5661 by-name readno — getMetaItem / getMetaItemCacheduntouched
4rest-server.ts:6517/historyno — historyMetaItemuntouched
5rest-server.ts:7085/diffno — diffMetaItemuntouched
6protocol.ts:11091 search sweep's page readyes (:11118)unchanged — page is non-overridable, both readings are undefined

organizationIdForMetaRead has FIVE call sites in rest-server.ts on origin/main, not six. The sixth is the page read inside packages/metadata-protocol itself. Both readings of "six" are now reconciled: five in that file, six in the tree. (PR #14677 would make it six in the file; it has not landed.)

The argument

Let f(t, o) = organizationIdForMetaRead(t, o). f answers either o or undefined, so f(t, f(t, o)) === f(t, o) for every t and o — a second application over the same type is an algebraic no-op. The load-bearing half is therefore "the same type", and it holds for both sites that reach the method:

  • Site 2 gates on canonicalMetaUrlType(req.params.type) and then passes type: req.params.type, the raw segment. The first statement of getMetaItems folds that segment through canonicalizeMetaRequestType, which iscanonicalMetaUrlType — the identical map, so request.type inside is the identical string the door gated on.
  • Site 6 gates on 'page' and passes 'page'.

The proof is executable, not prose

packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts §3 measures both halves over the complete accepted-spelling population — 61 spellings, derived from META_URL_TO_SINGULAR unioned with the registry, so a new type or a changed fold cannot slip past:

  • §3a (61 cases) — the algebra, per spelling.
  • §3b (61 cases) — the behavioural half. For every spelling, a caller that gates first receives partitions equal to what the pre-change implementation read for that same argument (which it spent verbatim, so the expectation is the old behaviour written out).
  • §3c (61 cases) — the complement: an ungated caller is narrowed. Without it the suite would pass on an implementation that changed nothing.

§3b is green on both sides of the ablation, and that is its positive content — a case green with and without the change is exactly the demonstration the tripwire asked for.


What the change DOES move — and the semver derivation

Callers that pass a raw active organization are narrowed for non-overridable types. Enumerated mechanically from call sites rather than from the card's table:

callermulti-type?today
runtime/domains/meta.ts:921 — dispatcher GET /metadata/:typeone per request, any typeraw org ⇒ narrowed
runtime/domains/packages.ts:1160assemblePackageManifest export sweepyes, every plural keyraw org ⇒ narrowed
runtime/domains/packages.ts:603 — ADR-0045 publish visibility flip readapp onlyraw org ⇒ narrowed
getMetaDiagnostics untyped sweepyes, whole registryno org passed by REST today ⇒ correct when one is forwarded
findReferencesToMetayes, per matcher.fromTypeno org passed by REST today ⇒ correct when one is forwarded

Semver: patch — derived from this change's own lineage. A published /meta read door's row set changing is not a new class here; it is the class this predicate was born in, and all three landed instances shipped patch:

commitwhat changedlevel
b6c769019 (#9454 / #9727)the row set every /meta read door returns — org rows addedmetadata-core, metadata-protocol, rest: all patch
26f3588fb (#10340 / #10519)which partition two spellings read — rows movedrest, metadata-core: patch
67ceb9aef (#11553)the same fold-before-scope repair on the dispatcher doorruntime: patch

The first is the commit that introduced organizationIdForMetaRead itself. Adding the org partition to every read door was patch; moving which partition two spellings read was patch; withholding the org partition from types that never had a read channel for it is the same class one verb further in, and takes the same level.

Not minor.scripts/check-changeset-no-major.mjs refuses major outright, so during the launch window a genuinely breaking change ships as minor (pre-1.0, whole-stack lockstep) — #13925 is exactly that: "@objectstack/core": minor, carrying a bolded incompatibility banner and an adr-0087: marker for a narrowed published accept set.

But the implication runs one way only, and the gate's own header is explicit that it does: during the window minor is the union of ordinary new-functionality bumps and banner-marked breaking ones — 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps carrying no banner at all — so the bump level "tells a consumer nothing about whether the release breaks them". The carriers of breaking-ness are the bolded banner in the changeset body and the ADR-0087 disposition: "during the window they are the only signal there is".

⇒ So minor here would not claim an incompatibility — it would claim nothing about compatibility, which is precisely the cost the header names. This change carries neither carrier because it owes neither: nothing is retired, no accept set narrows, and check-adr-0087-registration reads it as non-breaking. The level is patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted.

⚠️ The previous revision of this paragraph asserted that minormeans "breaking" here and that claiming it would signal an incompatibility. That affirmed the consequent — breaking ⇒ minor holds, minor ⇒ breaking does not — and contradicted the very file it cited. Corrected above; the conclusion (patch) is unchanged, only the reason it rests on.

Nothing here is incompatible, and the reason is what the withheld rows are. They are the #6190 phantoms: org-scoped rows of types with no per-org read channel. The platform has refused to mint them since ac244ad09 / 6155c3c24, boot hydration skips them, reportUnhydratableOrgScopedRows audits them, and every REST /meta read door has already withheld them since b6c769019. The only doors still serving them were the dispatcher list (runtime/src/domains/meta.ts:921) and the runtime manifest and publish-flip reads (packages.ts:1160, :603) — so this change aligns those three with the published /meta surface rather than departing from it. A consumer reading those rows was reading through a door inconsistent with /meta, on data the platform had already ruled dead.

⛔ Not "only a refactor of where the predicate lives" either — triage refused that sentence and it is not the reason for the level. The predicate's new position does change which rows three doors serve; that is why this is a behaviour entry rather than an internal note, and the lineage above is why its level is patch.

⚠️Corrected from the first revision of this body, which claimed minor on the strength of a precedent that does not exist: issue 13973 (named without a link so this PR does not pull an unrelated blocked card into its timeline — the withdrawal is checkable as written) is still open and blocked, never landed on main, and its subject is driver date materialisation. It says nothing about read doors at any level. That citation is withdrawn rather than replaced; the three landed commits above are the real lineage. The same revision also mis-stated #13925 as carrying a major bump, which check-changeset-no-major.mjs forbids outright; it shipped minor, as above.


Reverse verification

Mutation: the gate line replaced by const orgId = request.organizationId; — i.e. the pre-change behaviour restored, in packages/metadata-protocol/src/protocol.ts.

Predicted in writing before mutating: 54 red / 140 green, named — §1 phantom case (1), §2 draft case (1), §3c for the 50 spellings folding to a non-overridable type, §4 sweep (1), §5 sources (1).

Observed: exactly 54 failed / 140 passed (194), and the named set matched — §1×1, §2×1, §3×50, §4×1, §5×1, with zero §3b failures.

Discipline on both legs:

  • Mutation confirmed on disk, not by an editor exit code: the gated line went 1 -> 0 and the injected line 0 -> 1, asserted before the run; a miss aborts and voids the reading. ⚠️ To be exact about that second anchor: the injected string carried an // ABLATION: gate removed suffix, and it is that exact string which went 0 -> 1. The bare statement const orgId = request.organizationId; already occurs twice in protocol.ts (getMetaItem at :7348 and getMetaItemLayered at :7760), so counted bare it went 2 -> 3. The suffix is what made the anchor unambiguous; the first revision of this body quoted the count without saying so.
  • Resolution path measured, not assumed. The test imports ./protocol.js — same-package relative, so it reads source. Demonstrated positively: dist/index.js still contained the gate throughout the mutated run (grep count 1), so a dist-mediated test could not have gone red. It did.
  • Restore proved by state, not exit code: git checkout HEAD -- PATH with PATH absolute (naming HEAD so a written index cannot hand the mutation back), then git hash-object compared against the HEAD blob — 13b37b5d468e9bcff6c9edd588a2699bac36b157, matched — plus git diff HEAD empty. An empty hash is read as failure, never as "nothing to compare".
  • trap ... EXIT INT TERM with an absolute repo root resolved via git rev-parse --show-toplevel, so a foreground-cap SIGTERM mid-mutation cannot leave the tree mutated.

Fixture triage

Two existing cases asserted a union the platform must no longer perform. Both were re-spelled, not deleted — the invariant each pins is unchanged and simply has to be measured on a type that has an org partition:

  • packages/metadata-protocol/src/meta-overlay-cache.test.ts §7 "an org-scoped read does not answer from the env-wide entry" — object to view. The key-separation invariant survives; on object it would now be asserting a separation the platform deliberately does not have.
  • packages/objectql/src/protocol-meta.test.ts "getMetaItems unions env-wide and org-specific rows" — app to view. This is the read-side twin of the [#6190] re-spelling three cases up in the same file. Its two getMetaItem (singular) siblings keep app on purpose: that verb is untouched here.

Scanned by the rule's consumption radius rather than by the edited package: every *.test.ts in the tree naming both getMetaItems and organizationId (22 files) was enumerated and the affected packages run.

Verification

origin/main merge base 5258b63f8. Union re-run at 3bbeb09f0 (git rev-parse --short HEAD at the time of the run) — the commit carrying the whole code tree. The commits after it are changeset prose only and touch nothing under packages/**, so the runs below still describe this PR's code exactly.

runresult
@objectstack/metadata-protocol full suite157 files / 2335 passed, 2 skipped
new pin file get-meta-items-org-read-gate.test.ts194 passed
@objectstack/objectql (4 affected files)169 passed
@objectstack/rest (6 affected files)190 passed
@objectstack/runtime (2 affected files)267 passed
@objectstack/metadata-protocol typecheckclean — and it does reach the new test file (it reported TS2345 there before the fix, so this is not a phantom pass)
@objectstack/objectql typecheckclean
pnpm lint (repo-wide eslint . --no-inline-config)clean — run whole, no narrowing claimed

Gate families re-derived on the FINAL file list with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed — the script takes its own change set from the merge base): 38 families, harvested with --commands so neither spelling nor the convention block could be dropped. The claim-time seed named 23; the extra families come from the changeset and from the "adds or edits a test file" convention block.

36 of 38 green. Two remain NOT MEASURED, in the gates' own words — neither is a pass and neither is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved turbo run test log; "running the family locally, record this gate as NOT MEASURED".
  • node scripts/pm/check-half-states.mjs — exit 3: the instrument refuses to run in an agent container.

Two others exited 3 on the first pass and were converted into real readings by building the workspace closure (turbo run build --filter='./packages/*' --filter='./packages/*/*', 71/71) rather than left unmeasured:

  • pnpm check:type-check-debtgreen: 22 ledger entries re-measured in 141.2s, 755 raw tsc errors, none above its recorded number, "surplus: none — every entry sits exactly at its measurement, so any new error is red". This is the ratchet family the new test file moves.
  • pnpm check:dual-build-cjs-loadsgreen: 102 require entry points across 66 packages load, 610 CJS files parse.

One gate went red and was repaired: check-adr-0087-registration read this changeset's prose citation of #13925's marker as this changeset's own breaking declaration (its detector is /\*\*BREAKING/i plus a line-initial BREAKING[ -]CHANGE). The change retires nothing and declares nothing breaking, so the repair is the wording — ⛔ not an adr-0087: disposition marker for a question this change does not raise.

The changeset has since been re-levelled to patch and its semver reasoning corrected twice (the two ⚠️ notes in the semver section above). After each, the three changeset-sensitive families were re-run on the final tree: node scripts/check-empty-changeset.mjsexit 0, node scripts/check-changeset-no-major.mjsexit 0, node scripts/check-adr-0087-registration.mjsexit 0. Nothing else was re-run, and nothing else needed to be: every commit after 3bbeb09f0 touches only .changeset/getmetaitems-org-read-gate.md, and packages/metadata-protocol/src/protocol.ts is still blob 13b37b5d468e9bcff6c9edd588a2699bac36b157 — the same blob the runs and the ablation above measured.

⚠️ Spelling note for anyone re-running these: there is nocheck:changeset-no-major npm script (root package.json has only check:changeset-gate-self-tests, which runs the --self-tests and not the gate itself). pnpm check:changeset-no-major therefore exits 254 as a missing script, which is not a gate reading. The direct node scripts/check-changeset-no-major.mjs spelling — the one dispatch-gates --commands emits — is the gate.

Scope

packages/rest/** is not touched — the fence held, and it turned out to be a proof obligation rather than an edit, exactly as claimed. packages/spec/** and packages/objectql/src/engine.ts are not touched either. The one file outside packages/metadata-protocol is packages/objectql/src/protocol-meta.test.ts, a forced fixture re-spelling.

The half this PR does not do, hence Part of rather than a closing keyword: the two doors named on the card — GET /meta/diagnostics with no ?type=, and GET /meta/:type/:name/references — pass noorganizationId at all on origin/main today, so nothing about their answers changes here. The gate is now in the right place for whenever a caller does forward one; the forwarding itself lives in packages/rest, which this card fences off. #13753 and #14677 are where that half belongs, and neither is addressed here.

Generated by Claude Code

Snapshot before verification. `organizationIdForMetaRead(request.type,
request.organizationId)` is resolved once, after the canonical fold, and
spent by both the active-overlay arm and the previewDrafts arm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Includes the idempotence proof the direction-A ruling was conditional on,
mechanised over the complete accepted-spelling population.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…n overridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…ridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… signal tokens (#14683)
The body cites #13925's major-plus-marker precedent to REFUSE it. Spelled with
the literal markers, check-adr-0087-registration reads the citation as this
changeset's own declaration and demands an ADR-0087 disposition for a change
that retires nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…] for the probe sites (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 9 documentable anchor(s).

31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 9 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 224f8ea4a0776d72de0003a77695bfb98c2206cdpackageMentionDocs.

Which tree this was computed on

This run read content/docs from cb52c281cb890f79d76a18fdc7e227be37f4b66e — the merge of head fc986acdab8dddc2e351409cd0ade81d34d20893 into base 224f8ea4a0776d72de0003a77695bfb98c2206cd, 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 cb52c281cb890f79d76a18fdc7e227be37f4b66e && git checkout cb52c281cb890f79d76a18fdc7e227be37f4b66e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 224f8ea4a0776d72de0003a77695bfb98c2206cd fc986acdab8dddc2e351409cd0ade81d34d20893 && git checkout -B drift-repro 224f8ea4a0776d72de0003a77695bfb98c2206cd && git merge --no-ff fc986acdab8dddc2e351409cd0ade81d34d20893
node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd

⚠️ 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 224f8ea4a0776d72de0003a77695bfb98c2206cd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — FAIL, adopted verbatim. Narrow: semver only, and it is repairable in prose.

Performed by an isolated reviewer at the configured tier in its own worktree cut from 3bbeb09f0, with no access to this seat's reasoning. This seat is off CONTRACT_REVIEW_TIER, so the verdict is adopted whole. The reviewer's own summary: "a patch round that only touches the changeset text and the PR body would pass." Rework is dispatched; ⛔ the PR stays draft.

The blocking finding — minor is over-declared, and both supporting citations are wrong

#13973 is not a landed precedent. This seat flagged it as a doubt in the review brief rather than letting it pass, and the reviewer confirmed it with evidence: state open, labels priority:p2, pm:blocked, domain:engine, body opens Blocked-by: #14078; git log origin/main --grep=13973 returns nothing; and its subject is Date-materialising drivers vs ISO-text drivers — it says nothing about read doors at any level. The changeset itself cites no number at all, only "the minor precedent", which does not exist.

The landed precedents for exactly this class all shipped patch — and they are this change's own lineage:

commitwhat changed on a published read doorlevel
b6c769019 (#9454/#9727)the row set every /meta read door returns (org rows added)metadata-protocol: patch, rest: patch, metadata-core: patch
26f3588fb (#10340/#10519)which partition two spellings read (rows moved)rest: patch, metadata-core: patch
67ceb9aef (#11553)the same, on the dispatcher doorruntime: patch

Against the other rungs: minor ⇔ a new export (87ad30c10, 3c1bbd2a8); minor + **BREAKING** + adr-0087: ⇔ a published type narrowing (d8024f050). This PR is fix(, adds no export, narrows no type. ⇒ patch.

A second factual error, introduced by the wording-repair commit 9f3244830: the changeset now says #13925's shape "would carry a major bump plus an explicit incompatibility marker". #13925 shipped as "@objectstack/core": **minor** with **BREAKING** and an adr-0087: marker, and scripts/check-changeset-no-major.mjs forbids major outright under pre-1.0 semantics. ⚠️ The pre-repair wording at cd9d3ee89 was accurate — the gate-driven repair broke a claim it was not aimed at. Worth recording as a class: a repair that satisfies a gate can damage a neighbouring sentence the gate does not read.

No BREAKING banner is owed, and the reviewer's reasoning is stronger than the changeset's: the rows a raw-org caller loses are the #6190 phantoms — refused at write since ac244ad09/6155c3c24, skipped by boot hydration, audited by reportUnhydratableOrgScopedRows, and already withheld by every REST /meta read door since b6c769019. The only doors still serving them were the dispatcher list and the runtime manifest/flip reads; this PR aligns them with REST. check-adr-0087-registration is green on the final tree — the repair laundered nothing, because nothing is owed.

What the review verified rather than accepted — the parts that PASS

  • ⭐ The idempotence tripwire is genuinely discharged, not circular. The reviewer read the pre-change protocol.ts (5258b63f8: :6811const orgId = (request as any).organizationId, :6873-6874 the two queryByOrg calls) and confirmed §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate, OVERRIDABLE, or the post-fold type. The "same type" half verified too: canonicalizeMetaRequestTypecanonicalMetaTypecanonicalMetaUrlType, the identical map the door gated on.
  • The ablation reproduced independently: 194 total, 54 failed / 140 passed, per section §1:1 §2:1 §3c:50 §4:1 §5:1, and §3b 0 of 61 on both legs — exactly the shape the tripwire asked for. Restore proved by blob hash 13b37b5d… and empty git diff HEAD.
  • ⭐ Both re-spellings are legitimate, and this was measured rather than argued: the reviewer ran the pre-change versions of both test files against the new protocol.ts and got exactly one failure each — the case that was re-spelled — and nothing else. app really did roll back to allowOrgOverride: false in ee58392e1 (ADR-0005:57 白名单表与注册表的分歧不止 flow:page/app/action/permission/tool/skill 六类 ADR 写 ❌ 而 allowOrgOverride:true,另有 dataset/book/position 三类表里没有却默认成了 true #6483/fix(spec): ADR-0005 白名单强制 —— 九类未获批 allowOrgOverride:true 回滚为 false (#6483) #6608). No coverage lost.
  • Call-site enumeration confirmed: five non-test organizationIdForMetaRead sites in rest-server.ts, a sixth at protocol.ts:11117, exactly two reaching getMetaItems; the other seven getMetaItems callers in rest-server.ts pass no organizationId. ⚠️ One caveat stated honestly: the reachability grep was this\.getMetaItems(, so an (this as any).getMetaItems( spelling would have evaded it — none seen, absence not exhaustively proven.
  • Third sweep verified (packages.ts:1160, assemblePackageManifest) and covered by the callee-side gate with no runtime edit. Part of is honest: neither door forwards an organization on origin/main today, so neither door's answer changes with this PR.
  • packages/rest/** and packages/spec/** untouched — the fence held.

Advisory findings


Generated by Claude Code

Contract review returned FAIL on the semver level alone. `minor` was
derived from #13973, which is open, `pm:blocked`, never landed, and is
about date-materialising drivers — it says nothing about read doors.
The real precedents are this predicate's own lineage, and all three
shipped `patch`: b6c7690 (#9454/#9727) added the org partition to every
`/meta` read door; 26f3588 (#10340/#10519) moved which partition two
spellings read; 67ceb9a (#11553) repaired the same on the dispatcher.
Withholding a partition from types that never had a read channel is the
same class.
Also corrects the #13925 claim this file's previous wording-repair commit
introduced: #13925 shipped `"@objectstack/core": minor` with a bolded
banner and an `adr-0087:` marker, not a major bump —
`check-changeset-no-major.mjs` refuses `major` outright, so `minor` in
this repo MEANS breaking, which is why claiming it here would be wrong.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Delta re-review — FAIL again, on one sentence, and it vindicates sending it back

The rework met every mechanical part of the tier's PASS condition — this seat verified that independently before asking (git diff --name-only 3bbeb09f0..000a9861c → one file; -- 'packages/**' → empty; level patch; 13973 × 0). ⛔ It still did not clear the fuse, because a FAIL is not a PASS and this seat is off tier. The rework was sent to the same reviewer as a scoped delta rather than adopted here.

That call is what caught this. The reworked changeset added an argument nobody asked for, and it is false.

The false sentence, and the file that refutes it

.changeset/getmetaitems-org-read-gate.md:59-60:

minor therefore means "breaking" here, and claiming it for this change would signal an incompatibility that does not exist.

scripts/check-changeset-no-major.mjs header lines 44-52 — the source the sentence cites — says the opposite:

Until then it is NOT the carrier, and that is the whole cost of the window: a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them. The mandatory information carriers for breaking-ness in the meantime are the BREAKING banner … and the ADR-0087 migration-ledger disposition …

Affirming the consequent.breaking ⇒ minor is true (header lines 25-27); minor ⇒ breaking is not, and the landed record carries the counter-examples: 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps with zeroBREAKING tokens. So claiming minor would signal nothing about compatibility — exactly the cost the header names — not a false incompatibility.

A false claim about the repo's versioning convention, in a release-notes input, contradicted by its own citation: the same class as the first FAIL. The conclusion (patch) is right; the sentence would teach the next author that every minor is breaking. Round 3 is dispatched to replace lines 59-60 and the PR body's mirror; ⛔ lines 54-58 stay as written.

Everything else in the delta — confirmed by the tier

  • The lineage table is accurate row by row, and the added claim that b6c769019"is the commit that introduced organizationIdForMetaRead itself" was verified: git show b6c769019 -- packages/metadata-core/src/meta-write-org-scope.ts shows +export function organizationIdForMetaRead(, absent from the parent.
  • The [finding] Plugin.type is typed string on a published surface — the eight-value set is enforced by prose only, though packages/core already depends on spec #13925 sentence is accurate and was not blunted by avoiding the literal token. d8024f050 is "@objectstack/core": minor, body line 7 carries the banner, line 35 the adr-0087: marker. Both detector regexes evaluate false on the current body and check-adr-0087-registration is exit 0.
  • The Clause-② restatement matches the tier's own finding — spec-described-semantics limb named, rows/tenancy limb withdrawn with the correct reason.
  • The ablation anchor arithmetic holds, and is subtler than either party first put it. A suffixed injection contains the bare statement as a prefix, so a -cF count on the bare string reads 3 after mutation while the suffixed string reads 1. 2→3 and 0→1 are both correct and consistent, as is 1→0 on the gate line. The dev's precision here was better than the correction it was given.
  • Code byte-identical: protocol.ts's blob at 000a9861c is still 13b37b5d…, the blob the tier ablated.

One advisory

The #13973 withdrawal states accurate facts but is not self-verifiable without naming the card. Suggested: spell it issue 13973 without the #, so a reader can check the withdrawal without this PR auto-linking an unrelated blocked card into its timeline. The dev's call, with a stated reason either way.

⭐ Platform fact, confirmed and now recorded

Root package.json has nocheck:changeset-no-major script — only check:changeset-gate-self-tests (line 120), which runs the three --self-tests and not the gate. So pnpm check:changeset-no-major exiting 254 (ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL … Did you mean "pnpm check:changeset-gate-self-tests"?) is a missing script, not a gate reading. The real gate is the direct node scripts/check-changeset-no-major.mjs, which is also the spelling dispatch-gates --commands emits. The dev reported its own 254 as a spelling error rather than banking it as a verdict — the right reading, and the reason this is now a fact rather than a mystery.


Generated by Claude Code

#14683)
I wrote that `minor` "means breaking" here, and that claiming it would
signal an incompatibility. That affirms the consequent: breaking => minor
holds, minor => breaking does not. check-changeset-no-major.mjs says so in
the header I cited — during the window the bump level "tells a consumer
nothing about whether the release breaks them", and the carriers are the
banner and the ADR-0087 disposition, "the only signal there is".
The landed record carries the other half: 87ad30c ('@objectstack/types':
minor) and 3c1bbd2 ("@objectstack/metadata": minor) are new-export bumps
with zero banner tokens. So `minor` is the union, not a breaking marker.
Corrected to what the gate actually says: the level carries nothing either
way, this change owes neither carrier, and `patch` follows from the lineage
and from no export being added. Lines 54-58 unchanged.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim (round 3)

The isolated reviewer at the configured tier returns PASS on the replacement sentence, judged on that and nothing else. Its words: "The fuse can be cleared in one stroke."

⚠️The fuse is not cleared in this comment. The label writes are read-modify-write with a compare read-back, and the label read path (GraphQL) is rate-limited right now while REST is up. ⛔ Writing a label set without a fresh read is how a concurrent write gets silently stripped, so the clearing waits for the read channel — and nothing is gated on it, because CI on fc986acda is still running (Lint & Repo Gates in progress at the time of writing).

What the tier verified on the final head

Re-derived at fc986acda in a throwaway worktree, not accepted from the seat or the dev:

git diff --name-only 000a9861c..fc986acda → .changeset/getmetaitems-org-read-gate.md
git diff --name-only … -- 'packages/**' → (empty)
protocol.ts blob → 13b37b5d… (still the blob it ablated)
level → "@objectstack/metadata-protocol": patch
'therefore *means* "breaking"' → 0 · 'ONE WAY ONLY' → 1 · '13973' → 0
both ADR-0087 detector regexes → false
check-empty-changeset · check-changeset-no-major · check-adr-0087-registration → exit 0

check-adr-0087-registration --base 5258b63f8 reports "1 non-breaking changeset(s) seen".

⭐ The replacement was checked line by line against its own source

This is the part worth recording, because the two earlier FAILs were both citations that contradicted the file they cited:

the new text sayscheck-changeset-no-major.mjs says
"the implication runs ONE WAY ONLY, and the gate's own header is explicit that it does"lines 46-47: "a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them"
minor is the union of new-functionality and banner-marked breaking bumpsverified last round: 87ad30c10, 3c1bbd2a8 are minor with zeroBREAKING tokens
"tells a consumer nothing about whether the release breaks them" / "during the window they are the only signal there is"verbatim at header lines 46-47 and 52
"which is precisely the cost the header names"line 45: "that is the whole cost of the window"
"patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted"the tier's own stated PASS condition — and it no longer affirms the consequent

One nit, explicitly not a fourth round

"check-adr-0087-registration reads it as non-breaking" describes what the gate detects — the absence of a declared breaking marker — which is slightly weaker than "non-breaking" simpliciter. Paired with the preceding "nothing is retired, no accept set narrows" (both true; the request accept set is unchanged), the sentence is not misleading. ⛔ Recorded, not actioned.

On the PR body

The tier judged it as found and confirms every statement in it matches something verified across the three rounds. ⚠️ It explicitly makes no claim about how the body came to be updated, and neither does this seat: the dev reported its publish blocked, the body nonetheless carries all three staged edits, and the working hypothesis — that the write landed while the tool's response path failed — is unproven. Recorded as an open platform question rather than a fact, because the consequence if true is real: "retry on error" could double-apply a mutation that already succeeded.


⭐ Three rounds, three FAILs, and the code was correct from the first oneprotocol.ts's blob never changed after 3bbeb09f0. Every finding landed in the prose explaining why the code is correct, and prose is what compiles into release notes. That is the contract review doing exactly the job it exists for.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 3, 2026 01:30
@os-musk
os-musk enabled auto-merge September 3, 2026 01:30
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at fc986acda.

1. Contract review PASS on round 3, adopted verbatim from an isolated reviewer at the required tier (5518644676). ⭐ Three rounds, three verdicts, and the code was correct from the first oneprotocol.ts is still blob 13b37b5d…, the blob the reviewer ablated at 3bbeb09f0. Every finding landed in the prose explaining why the code is right, and prose is what compiles into release notes.

2. The fuse cleared from BOTH carriers in one stroke, read-modify-write with a compare read-back: card #14683 and this PR. ⛔ It stayed on through two FAILs — a FAIL does not clear a fuse, only a PASS does.

3. The tripwire the ruling made this conditional on is discharged, and the tier confirmed it is not circular. It read the pre-change protocol.ts (5258b63f8:6811, :6873-6874) and verified §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate or OVERRIDABLE. It reproduced the ablation independently: 54 failed / 140 passed, §3b 0 of 61 on both legs, §3c 50 of 61, restore blob-hash matched.

4. Governed-surface predicate, re-run on the FINAL 5-file list — ⛔ not recalled:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/getmetaitems-org-read-gate.md \
packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts \
packages/metadata-protocol/src/meta-overlay-cache.test.ts \
packages/metadata-protocol/src/protocol.ts \
packages/objectql/src/protocol-meta.test.ts
→ 0 of 5 path(s) hit the register — NOT governed; ordinary queue landing applies.

5. Every check run green or skipped — 36 runs read at perPage: 50, each by its own completed + conclusion, ⛔ never by an aggregate. Test Core (1/6) took 23 min and passed; Lint & Repo Gates 16 min.

6. Flipped draft: false, read back, then armed SQUASH.⚠️ The echo reads method: MERGE regardless — known artifact; the merge-queue build ref is the reading.

What this PR does and does not close

Part of #14683, not Fixes — and the tier confirmed the split is honest. On origin/main today neither door the card names (GET /meta/diagnostics untyped, GET /meta/:type/:name/references) forwards an organization at all, so neither door's answer changes here. The gate is now in the right place for when a caller does forward one; the forwarding half lives in packages/rest and belongs to #13753 / #14677.

Follow-ups already filed, so nothing is lost

⭐ One platform reading this landing produced

issue_read get_labels refuses a PR number, which has made PR label reads expensive all shift. search_pull_requests with fields: ["number","labels","draft"] returns both compactly for every open PR by an author in one call — it served as the fresh read and the compare read-back here, at a fraction of a full pull_request_read get.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33706455410 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 40 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type - #14767

Merged
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate
Sep 3, 2026
Merged

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type#14767
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate

Conversation

@os-musk

@os-muskos-musk commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Part of #14683

getMetaItems now applies the allowOrgOverride read gate itself, so a metadata sweep that reads more than one type per request is scoped per type instead of per request.

Clause-②: yes. No new exported symbol and no new key on a published payload, so the mechanical floor is not hit. The limb that holds is that this change alters the runtime meaning of a spec-described request member without touching the spec: GetMetaItemsRequestSchema.organizationId is described as "Selects the org partition in the ADR-0005 overlay read order…", and after this PR the org partition is not consulted at all for a registry-non-overridable type. The described contract and the served behaviour diverge, with the spec unchanged.

⚠️ Corrected from the first revision of this body, which named the limb as "which rows a caller gets back, with a tenancy dimension". That limb does not hold: a tenancy read-scope behaviour change sits on the human floor rather than the contract-review one, and the accept/refuse criterion is not met either — the same requests are accepted and the same refusals raised. needs:contract-review requested either way.


The change

One line of behaviour, in packages/metadata-protocol/src/protocol.ts:

request=canonicalizeMetaRequestType(request);const{ packageId }=request;constorgId=organizationIdForMetaRead(request.type,request.organizationId);

The two per-arm re-reads of request.organizationId (the active-overlay read and the previewDrafts read) are deleted and both now spend that one resolution — a gate threaded into only one arm would leave the draft preview resurrecting exactly what the active list had stopped serving.

Three placement facts, each load-bearing:

  • After the canonical fold, never before.declaresOrgOverride tolerates the MANIFEST plurals and not the URL-only ones (translations, email_templates have no manifest key). Handed a raw URL segment it answers env-wide for two genuinely org-overridable types — one item in two partitions, addressed by spelling.
  • One resolution, both arms.
  • No new dependency edge.organizationIdForMetaRead was already imported into protocol.ts (line 57) from @objectstack/metadata-core, already a workspace:* dependency of packages/metadata-protocol. Nothing moved between packages, no cycle. (Measured, not assumed — this was a declared stop condition.)

The harm class is RESURRECTION, not concealment

Carried forward from triage deliberately, because a fix written against the opposite premise would aim at the wrong failure.

SysMetadataRepository.history() filters organization_id by strict equality, so naming the tenant therehides an allowOrgOverride: false type's rows. On this path the two queryByOrg reads are UNIONed, so naming it can only add — and what it adds are the pre-#6190 phantoms: org-scoped rows of types with no per-org read channel, which loadMetaFromDb walks past and reportUnhydratableOrgScopedRows exists to warn about. Read back, they surface inside a clearance rendered before a destructive action, where a resurrected row is worse than an omission because it reads as evidence.


⭐ The idempotence proof (the tripwire)

Direction A was ruled conditional on showing that moving the predicate inside does not change the scope any already-gating call site receives. Verdict: it does not. No gating call site's scope moves.

The measured call-site population

Counted on this branch's own tree, not taken on faith:

#sitereaches getMetaItems?effect of this change
1rest-server.ts:3172/layersno — getMetaItemLayereduntouched
2rest-server.ts:4823GET /meta/:type listyes (:4839)unchanged — proof below
3rest-server.ts:5661 by-name readno — getMetaItem / getMetaItemCacheduntouched
4rest-server.ts:6517/historyno — historyMetaItemuntouched
5rest-server.ts:7085/diffno — diffMetaItemuntouched
6protocol.ts:11091 search sweep's page readyes (:11118)unchanged — page is non-overridable, both readings are undefined

organizationIdForMetaRead has FIVE call sites in rest-server.ts on origin/main, not six. The sixth is the page read inside packages/metadata-protocol itself. Both readings of "six" are now reconciled: five in that file, six in the tree. (PR #14677 would make it six in the file; it has not landed.)

The argument

Let f(t, o) = organizationIdForMetaRead(t, o). f answers either o or undefined, so f(t, f(t, o)) === f(t, o) for every t and o — a second application over the same type is an algebraic no-op. The load-bearing half is therefore "the same type", and it holds for both sites that reach the method:

  • Site 2 gates on canonicalMetaUrlType(req.params.type) and then passes type: req.params.type, the raw segment. The first statement of getMetaItems folds that segment through canonicalizeMetaRequestType, which iscanonicalMetaUrlType — the identical map, so request.type inside is the identical string the door gated on.
  • Site 6 gates on 'page' and passes 'page'.

The proof is executable, not prose

packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts §3 measures both halves over the complete accepted-spelling population — 61 spellings, derived from META_URL_TO_SINGULAR unioned with the registry, so a new type or a changed fold cannot slip past:

  • §3a (61 cases) — the algebra, per spelling.
  • §3b (61 cases) — the behavioural half. For every spelling, a caller that gates first receives partitions equal to what the pre-change implementation read for that same argument (which it spent verbatim, so the expectation is the old behaviour written out).
  • §3c (61 cases) — the complement: an ungated caller is narrowed. Without it the suite would pass on an implementation that changed nothing.

§3b is green on both sides of the ablation, and that is its positive content — a case green with and without the change is exactly the demonstration the tripwire asked for.


What the change DOES move — and the semver derivation

Callers that pass a raw active organization are narrowed for non-overridable types. Enumerated mechanically from call sites rather than from the card's table:

callermulti-type?today
runtime/domains/meta.ts:921 — dispatcher GET /metadata/:typeone per request, any typeraw org ⇒ narrowed
runtime/domains/packages.ts:1160assemblePackageManifest export sweepyes, every plural keyraw org ⇒ narrowed
runtime/domains/packages.ts:603 — ADR-0045 publish visibility flip readapp onlyraw org ⇒ narrowed
getMetaDiagnostics untyped sweepyes, whole registryno org passed by REST today ⇒ correct when one is forwarded
findReferencesToMetayes, per matcher.fromTypeno org passed by REST today ⇒ correct when one is forwarded

Semver: patch — derived from this change's own lineage. A published /meta read door's row set changing is not a new class here; it is the class this predicate was born in, and all three landed instances shipped patch:

commitwhat changedlevel
b6c769019 (#9454 / #9727)the row set every /meta read door returns — org rows addedmetadata-core, metadata-protocol, rest: all patch
26f3588fb (#10340 / #10519)which partition two spellings read — rows movedrest, metadata-core: patch
67ceb9aef (#11553)the same fold-before-scope repair on the dispatcher doorruntime: patch

The first is the commit that introduced organizationIdForMetaRead itself. Adding the org partition to every read door was patch; moving which partition two spellings read was patch; withholding the org partition from types that never had a read channel for it is the same class one verb further in, and takes the same level.

Not minor.scripts/check-changeset-no-major.mjs refuses major outright, so during the launch window a genuinely breaking change ships as minor (pre-1.0, whole-stack lockstep) — #13925 is exactly that: "@objectstack/core": minor, carrying a bolded incompatibility banner and an adr-0087: marker for a narrowed published accept set.

But the implication runs one way only, and the gate's own header is explicit that it does: during the window minor is the union of ordinary new-functionality bumps and banner-marked breaking ones — 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps carrying no banner at all — so the bump level "tells a consumer nothing about whether the release breaks them". The carriers of breaking-ness are the bolded banner in the changeset body and the ADR-0087 disposition: "during the window they are the only signal there is".

⇒ So minor here would not claim an incompatibility — it would claim nothing about compatibility, which is precisely the cost the header names. This change carries neither carrier because it owes neither: nothing is retired, no accept set narrows, and check-adr-0087-registration reads it as non-breaking. The level is patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted.

⚠️ The previous revision of this paragraph asserted that minormeans "breaking" here and that claiming it would signal an incompatibility. That affirmed the consequent — breaking ⇒ minor holds, minor ⇒ breaking does not — and contradicted the very file it cited. Corrected above; the conclusion (patch) is unchanged, only the reason it rests on.

Nothing here is incompatible, and the reason is what the withheld rows are. They are the #6190 phantoms: org-scoped rows of types with no per-org read channel. The platform has refused to mint them since ac244ad09 / 6155c3c24, boot hydration skips them, reportUnhydratableOrgScopedRows audits them, and every REST /meta read door has already withheld them since b6c769019. The only doors still serving them were the dispatcher list (runtime/src/domains/meta.ts:921) and the runtime manifest and publish-flip reads (packages.ts:1160, :603) — so this change aligns those three with the published /meta surface rather than departing from it. A consumer reading those rows was reading through a door inconsistent with /meta, on data the platform had already ruled dead.

⛔ Not "only a refactor of where the predicate lives" either — triage refused that sentence and it is not the reason for the level. The predicate's new position does change which rows three doors serve; that is why this is a behaviour entry rather than an internal note, and the lineage above is why its level is patch.

⚠️Corrected from the first revision of this body, which claimed minor on the strength of a precedent that does not exist: issue 13973 (named without a link so this PR does not pull an unrelated blocked card into its timeline — the withdrawal is checkable as written) is still open and blocked, never landed on main, and its subject is driver date materialisation. It says nothing about read doors at any level. That citation is withdrawn rather than replaced; the three landed commits above are the real lineage. The same revision also mis-stated #13925 as carrying a major bump, which check-changeset-no-major.mjs forbids outright; it shipped minor, as above.


Reverse verification

Mutation: the gate line replaced by const orgId = request.organizationId; — i.e. the pre-change behaviour restored, in packages/metadata-protocol/src/protocol.ts.

Predicted in writing before mutating: 54 red / 140 green, named — §1 phantom case (1), §2 draft case (1), §3c for the 50 spellings folding to a non-overridable type, §4 sweep (1), §5 sources (1).

Observed: exactly 54 failed / 140 passed (194), and the named set matched — §1×1, §2×1, §3×50, §4×1, §5×1, with zero §3b failures.

Discipline on both legs:

  • Mutation confirmed on disk, not by an editor exit code: the gated line went 1 -> 0 and the injected line 0 -> 1, asserted before the run; a miss aborts and voids the reading. ⚠️ To be exact about that second anchor: the injected string carried an // ABLATION: gate removed suffix, and it is that exact string which went 0 -> 1. The bare statement const orgId = request.organizationId; already occurs twice in protocol.ts (getMetaItem at :7348 and getMetaItemLayered at :7760), so counted bare it went 2 -> 3. The suffix is what made the anchor unambiguous; the first revision of this body quoted the count without saying so.
  • Resolution path measured, not assumed. The test imports ./protocol.js — same-package relative, so it reads source. Demonstrated positively: dist/index.js still contained the gate throughout the mutated run (grep count 1), so a dist-mediated test could not have gone red. It did.
  • Restore proved by state, not exit code: git checkout HEAD -- PATH with PATH absolute (naming HEAD so a written index cannot hand the mutation back), then git hash-object compared against the HEAD blob — 13b37b5d468e9bcff6c9edd588a2699bac36b157, matched — plus git diff HEAD empty. An empty hash is read as failure, never as "nothing to compare".
  • trap ... EXIT INT TERM with an absolute repo root resolved via git rev-parse --show-toplevel, so a foreground-cap SIGTERM mid-mutation cannot leave the tree mutated.

Fixture triage

Two existing cases asserted a union the platform must no longer perform. Both were re-spelled, not deleted — the invariant each pins is unchanged and simply has to be measured on a type that has an org partition:

  • packages/metadata-protocol/src/meta-overlay-cache.test.ts §7 "an org-scoped read does not answer from the env-wide entry" — object to view. The key-separation invariant survives; on object it would now be asserting a separation the platform deliberately does not have.
  • packages/objectql/src/protocol-meta.test.ts "getMetaItems unions env-wide and org-specific rows" — app to view. This is the read-side twin of the [#6190] re-spelling three cases up in the same file. Its two getMetaItem (singular) siblings keep app on purpose: that verb is untouched here.

Scanned by the rule's consumption radius rather than by the edited package: every *.test.ts in the tree naming both getMetaItems and organizationId (22 files) was enumerated and the affected packages run.

Verification

origin/main merge base 5258b63f8. Union re-run at 3bbeb09f0 (git rev-parse --short HEAD at the time of the run) — the commit carrying the whole code tree. The commits after it are changeset prose only and touch nothing under packages/**, so the runs below still describe this PR's code exactly.

runresult
@objectstack/metadata-protocol full suite157 files / 2335 passed, 2 skipped
new pin file get-meta-items-org-read-gate.test.ts194 passed
@objectstack/objectql (4 affected files)169 passed
@objectstack/rest (6 affected files)190 passed
@objectstack/runtime (2 affected files)267 passed
@objectstack/metadata-protocol typecheckclean — and it does reach the new test file (it reported TS2345 there before the fix, so this is not a phantom pass)
@objectstack/objectql typecheckclean
pnpm lint (repo-wide eslint . --no-inline-config)clean — run whole, no narrowing claimed

Gate families re-derived on the FINAL file list with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed — the script takes its own change set from the merge base): 38 families, harvested with --commands so neither spelling nor the convention block could be dropped. The claim-time seed named 23; the extra families come from the changeset and from the "adds or edits a test file" convention block.

36 of 38 green. Two remain NOT MEASURED, in the gates' own words — neither is a pass and neither is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved turbo run test log; "running the family locally, record this gate as NOT MEASURED".
  • node scripts/pm/check-half-states.mjs — exit 3: the instrument refuses to run in an agent container.

Two others exited 3 on the first pass and were converted into real readings by building the workspace closure (turbo run build --filter='./packages/*' --filter='./packages/*/*', 71/71) rather than left unmeasured:

  • pnpm check:type-check-debtgreen: 22 ledger entries re-measured in 141.2s, 755 raw tsc errors, none above its recorded number, "surplus: none — every entry sits exactly at its measurement, so any new error is red". This is the ratchet family the new test file moves.
  • pnpm check:dual-build-cjs-loadsgreen: 102 require entry points across 66 packages load, 610 CJS files parse.

One gate went red and was repaired: check-adr-0087-registration read this changeset's prose citation of #13925's marker as this changeset's own breaking declaration (its detector is /\*\*BREAKING/i plus a line-initial BREAKING[ -]CHANGE). The change retires nothing and declares nothing breaking, so the repair is the wording — ⛔ not an adr-0087: disposition marker for a question this change does not raise.

The changeset has since been re-levelled to patch and its semver reasoning corrected twice (the two ⚠️ notes in the semver section above). After each, the three changeset-sensitive families were re-run on the final tree: node scripts/check-empty-changeset.mjsexit 0, node scripts/check-changeset-no-major.mjsexit 0, node scripts/check-adr-0087-registration.mjsexit 0. Nothing else was re-run, and nothing else needed to be: every commit after 3bbeb09f0 touches only .changeset/getmetaitems-org-read-gate.md, and packages/metadata-protocol/src/protocol.ts is still blob 13b37b5d468e9bcff6c9edd588a2699bac36b157 — the same blob the runs and the ablation above measured.

⚠️ Spelling note for anyone re-running these: there is nocheck:changeset-no-major npm script (root package.json has only check:changeset-gate-self-tests, which runs the --self-tests and not the gate itself). pnpm check:changeset-no-major therefore exits 254 as a missing script, which is not a gate reading. The direct node scripts/check-changeset-no-major.mjs spelling — the one dispatch-gates --commands emits — is the gate.

Scope

packages/rest/** is not touched — the fence held, and it turned out to be a proof obligation rather than an edit, exactly as claimed. packages/spec/** and packages/objectql/src/engine.ts are not touched either. The one file outside packages/metadata-protocol is packages/objectql/src/protocol-meta.test.ts, a forced fixture re-spelling.

The half this PR does not do, hence Part of rather than a closing keyword: the two doors named on the card — GET /meta/diagnostics with no ?type=, and GET /meta/:type/:name/references — pass noorganizationId at all on origin/main today, so nothing about their answers changes here. The gate is now in the right place for whenever a caller does forward one; the forwarding itself lives in packages/rest, which this card fences off. #13753 and #14677 are where that half belongs, and neither is addressed here.

Generated by Claude Code

Snapshot before verification. `organizationIdForMetaRead(request.type,
request.organizationId)` is resolved once, after the canonical fold, and
spent by both the active-overlay arm and the previewDrafts arm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Includes the idempotence proof the direction-A ruling was conditional on,
mechanised over the complete accepted-spelling population.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…n overridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…ridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… signal tokens (#14683)
The body cites #13925's major-plus-marker precedent to REFUSE it. Spelled with
the literal markers, check-adr-0087-registration reads the citation as this
changeset's own declaration and demands an ADR-0087 disposition for a change
that retires nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…] for the probe sites (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 9 documentable anchor(s).

31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 9 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 224f8ea4a0776d72de0003a77695bfb98c2206cdpackageMentionDocs.

Which tree this was computed on

This run read content/docs from cb52c281cb890f79d76a18fdc7e227be37f4b66e — the merge of head fc986acdab8dddc2e351409cd0ade81d34d20893 into base 224f8ea4a0776d72de0003a77695bfb98c2206cd, 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 cb52c281cb890f79d76a18fdc7e227be37f4b66e && git checkout cb52c281cb890f79d76a18fdc7e227be37f4b66e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 224f8ea4a0776d72de0003a77695bfb98c2206cd fc986acdab8dddc2e351409cd0ade81d34d20893 && git checkout -B drift-repro 224f8ea4a0776d72de0003a77695bfb98c2206cd && git merge --no-ff fc986acdab8dddc2e351409cd0ade81d34d20893
node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd

⚠️ 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 224f8ea4a0776d72de0003a77695bfb98c2206cd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — FAIL, adopted verbatim. Narrow: semver only, and it is repairable in prose.

Performed by an isolated reviewer at the configured tier in its own worktree cut from 3bbeb09f0, with no access to this seat's reasoning. This seat is off CONTRACT_REVIEW_TIER, so the verdict is adopted whole. The reviewer's own summary: "a patch round that only touches the changeset text and the PR body would pass." Rework is dispatched; ⛔ the PR stays draft.

The blocking finding — minor is over-declared, and both supporting citations are wrong

#13973 is not a landed precedent. This seat flagged it as a doubt in the review brief rather than letting it pass, and the reviewer confirmed it with evidence: state open, labels priority:p2, pm:blocked, domain:engine, body opens Blocked-by: #14078; git log origin/main --grep=13973 returns nothing; and its subject is Date-materialising drivers vs ISO-text drivers — it says nothing about read doors at any level. The changeset itself cites no number at all, only "the minor precedent", which does not exist.

The landed precedents for exactly this class all shipped patch — and they are this change's own lineage:

commitwhat changed on a published read doorlevel
b6c769019 (#9454/#9727)the row set every /meta read door returns (org rows added)metadata-protocol: patch, rest: patch, metadata-core: patch
26f3588fb (#10340/#10519)which partition two spellings read (rows moved)rest: patch, metadata-core: patch
67ceb9aef (#11553)the same, on the dispatcher doorruntime: patch

Against the other rungs: minor ⇔ a new export (87ad30c10, 3c1bbd2a8); minor + **BREAKING** + adr-0087: ⇔ a published type narrowing (d8024f050). This PR is fix(, adds no export, narrows no type. ⇒ patch.

A second factual error, introduced by the wording-repair commit 9f3244830: the changeset now says #13925's shape "would carry a major bump plus an explicit incompatibility marker". #13925 shipped as "@objectstack/core": **minor** with **BREAKING** and an adr-0087: marker, and scripts/check-changeset-no-major.mjs forbids major outright under pre-1.0 semantics. ⚠️ The pre-repair wording at cd9d3ee89 was accurate — the gate-driven repair broke a claim it was not aimed at. Worth recording as a class: a repair that satisfies a gate can damage a neighbouring sentence the gate does not read.

No BREAKING banner is owed, and the reviewer's reasoning is stronger than the changeset's: the rows a raw-org caller loses are the #6190 phantoms — refused at write since ac244ad09/6155c3c24, skipped by boot hydration, audited by reportUnhydratableOrgScopedRows, and already withheld by every REST /meta read door since b6c769019. The only doors still serving them were the dispatcher list and the runtime manifest/flip reads; this PR aligns them with REST. check-adr-0087-registration is green on the final tree — the repair laundered nothing, because nothing is owed.

What the review verified rather than accepted — the parts that PASS

  • ⭐ The idempotence tripwire is genuinely discharged, not circular. The reviewer read the pre-change protocol.ts (5258b63f8: :6811const orgId = (request as any).organizationId, :6873-6874 the two queryByOrg calls) and confirmed §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate, OVERRIDABLE, or the post-fold type. The "same type" half verified too: canonicalizeMetaRequestTypecanonicalMetaTypecanonicalMetaUrlType, the identical map the door gated on.
  • The ablation reproduced independently: 194 total, 54 failed / 140 passed, per section §1:1 §2:1 §3c:50 §4:1 §5:1, and §3b 0 of 61 on both legs — exactly the shape the tripwire asked for. Restore proved by blob hash 13b37b5d… and empty git diff HEAD.
  • ⭐ Both re-spellings are legitimate, and this was measured rather than argued: the reviewer ran the pre-change versions of both test files against the new protocol.ts and got exactly one failure each — the case that was re-spelled — and nothing else. app really did roll back to allowOrgOverride: false in ee58392e1 (ADR-0005:57 白名单表与注册表的分歧不止 flow:page/app/action/permission/tool/skill 六类 ADR 写 ❌ 而 allowOrgOverride:true,另有 dataset/book/position 三类表里没有却默认成了 true #6483/fix(spec): ADR-0005 白名单强制 —— 九类未获批 allowOrgOverride:true 回滚为 false (#6483) #6608). No coverage lost.
  • Call-site enumeration confirmed: five non-test organizationIdForMetaRead sites in rest-server.ts, a sixth at protocol.ts:11117, exactly two reaching getMetaItems; the other seven getMetaItems callers in rest-server.ts pass no organizationId. ⚠️ One caveat stated honestly: the reachability grep was this\.getMetaItems(, so an (this as any).getMetaItems( spelling would have evaded it — none seen, absence not exhaustively proven.
  • Third sweep verified (packages.ts:1160, assemblePackageManifest) and covered by the callee-side gate with no runtime edit. Part of is honest: neither door forwards an organization on origin/main today, so neither door's answer changes with this PR.
  • packages/rest/** and packages/spec/** untouched — the fence held.

Advisory findings


Generated by Claude Code

Contract review returned FAIL on the semver level alone. `minor` was
derived from #13973, which is open, `pm:blocked`, never landed, and is
about date-materialising drivers — it says nothing about read doors.
The real precedents are this predicate's own lineage, and all three
shipped `patch`: b6c7690 (#9454/#9727) added the org partition to every
`/meta` read door; 26f3588 (#10340/#10519) moved which partition two
spellings read; 67ceb9a (#11553) repaired the same on the dispatcher.
Withholding a partition from types that never had a read channel is the
same class.
Also corrects the #13925 claim this file's previous wording-repair commit
introduced: #13925 shipped `"@objectstack/core": minor` with a bolded
banner and an `adr-0087:` marker, not a major bump —
`check-changeset-no-major.mjs` refuses `major` outright, so `minor` in
this repo MEANS breaking, which is why claiming it here would be wrong.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Delta re-review — FAIL again, on one sentence, and it vindicates sending it back

The rework met every mechanical part of the tier's PASS condition — this seat verified that independently before asking (git diff --name-only 3bbeb09f0..000a9861c → one file; -- 'packages/**' → empty; level patch; 13973 × 0). ⛔ It still did not clear the fuse, because a FAIL is not a PASS and this seat is off tier. The rework was sent to the same reviewer as a scoped delta rather than adopted here.

That call is what caught this. The reworked changeset added an argument nobody asked for, and it is false.

The false sentence, and the file that refutes it

.changeset/getmetaitems-org-read-gate.md:59-60:

minor therefore means "breaking" here, and claiming it for this change would signal an incompatibility that does not exist.

scripts/check-changeset-no-major.mjs header lines 44-52 — the source the sentence cites — says the opposite:

Until then it is NOT the carrier, and that is the whole cost of the window: a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them. The mandatory information carriers for breaking-ness in the meantime are the BREAKING banner … and the ADR-0087 migration-ledger disposition …

Affirming the consequent.breaking ⇒ minor is true (header lines 25-27); minor ⇒ breaking is not, and the landed record carries the counter-examples: 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps with zeroBREAKING tokens. So claiming minor would signal nothing about compatibility — exactly the cost the header names — not a false incompatibility.

A false claim about the repo's versioning convention, in a release-notes input, contradicted by its own citation: the same class as the first FAIL. The conclusion (patch) is right; the sentence would teach the next author that every minor is breaking. Round 3 is dispatched to replace lines 59-60 and the PR body's mirror; ⛔ lines 54-58 stay as written.

Everything else in the delta — confirmed by the tier

  • The lineage table is accurate row by row, and the added claim that b6c769019"is the commit that introduced organizationIdForMetaRead itself" was verified: git show b6c769019 -- packages/metadata-core/src/meta-write-org-scope.ts shows +export function organizationIdForMetaRead(, absent from the parent.
  • The [finding] Plugin.type is typed string on a published surface — the eight-value set is enforced by prose only, though packages/core already depends on spec #13925 sentence is accurate and was not blunted by avoiding the literal token. d8024f050 is "@objectstack/core": minor, body line 7 carries the banner, line 35 the adr-0087: marker. Both detector regexes evaluate false on the current body and check-adr-0087-registration is exit 0.
  • The Clause-② restatement matches the tier's own finding — spec-described-semantics limb named, rows/tenancy limb withdrawn with the correct reason.
  • The ablation anchor arithmetic holds, and is subtler than either party first put it. A suffixed injection contains the bare statement as a prefix, so a -cF count on the bare string reads 3 after mutation while the suffixed string reads 1. 2→3 and 0→1 are both correct and consistent, as is 1→0 on the gate line. The dev's precision here was better than the correction it was given.
  • Code byte-identical: protocol.ts's blob at 000a9861c is still 13b37b5d…, the blob the tier ablated.

One advisory

The #13973 withdrawal states accurate facts but is not self-verifiable without naming the card. Suggested: spell it issue 13973 without the #, so a reader can check the withdrawal without this PR auto-linking an unrelated blocked card into its timeline. The dev's call, with a stated reason either way.

⭐ Platform fact, confirmed and now recorded

Root package.json has nocheck:changeset-no-major script — only check:changeset-gate-self-tests (line 120), which runs the three --self-tests and not the gate. So pnpm check:changeset-no-major exiting 254 (ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL … Did you mean "pnpm check:changeset-gate-self-tests"?) is a missing script, not a gate reading. The real gate is the direct node scripts/check-changeset-no-major.mjs, which is also the spelling dispatch-gates --commands emits. The dev reported its own 254 as a spelling error rather than banking it as a verdict — the right reading, and the reason this is now a fact rather than a mystery.


Generated by Claude Code

#14683)
I wrote that `minor` "means breaking" here, and that claiming it would
signal an incompatibility. That affirms the consequent: breaking => minor
holds, minor => breaking does not. check-changeset-no-major.mjs says so in
the header I cited — during the window the bump level "tells a consumer
nothing about whether the release breaks them", and the carriers are the
banner and the ADR-0087 disposition, "the only signal there is".
The landed record carries the other half: 87ad30c ('@objectstack/types':
minor) and 3c1bbd2 ("@objectstack/metadata": minor) are new-export bumps
with zero banner tokens. So `minor` is the union, not a breaking marker.
Corrected to what the gate actually says: the level carries nothing either
way, this change owes neither carrier, and `patch` follows from the lineage
and from no export being added. Lines 54-58 unchanged.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim (round 3)

The isolated reviewer at the configured tier returns PASS on the replacement sentence, judged on that and nothing else. Its words: "The fuse can be cleared in one stroke."

⚠️The fuse is not cleared in this comment. The label writes are read-modify-write with a compare read-back, and the label read path (GraphQL) is rate-limited right now while REST is up. ⛔ Writing a label set without a fresh read is how a concurrent write gets silently stripped, so the clearing waits for the read channel — and nothing is gated on it, because CI on fc986acda is still running (Lint & Repo Gates in progress at the time of writing).

What the tier verified on the final head

Re-derived at fc986acda in a throwaway worktree, not accepted from the seat or the dev:

git diff --name-only 000a9861c..fc986acda → .changeset/getmetaitems-org-read-gate.md
git diff --name-only … -- 'packages/**' → (empty)
protocol.ts blob → 13b37b5d… (still the blob it ablated)
level → "@objectstack/metadata-protocol": patch
'therefore *means* "breaking"' → 0 · 'ONE WAY ONLY' → 1 · '13973' → 0
both ADR-0087 detector regexes → false
check-empty-changeset · check-changeset-no-major · check-adr-0087-registration → exit 0

check-adr-0087-registration --base 5258b63f8 reports "1 non-breaking changeset(s) seen".

⭐ The replacement was checked line by line against its own source

This is the part worth recording, because the two earlier FAILs were both citations that contradicted the file they cited:

the new text sayscheck-changeset-no-major.mjs says
"the implication runs ONE WAY ONLY, and the gate's own header is explicit that it does"lines 46-47: "a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them"
minor is the union of new-functionality and banner-marked breaking bumpsverified last round: 87ad30c10, 3c1bbd2a8 are minor with zeroBREAKING tokens
"tells a consumer nothing about whether the release breaks them" / "during the window they are the only signal there is"verbatim at header lines 46-47 and 52
"which is precisely the cost the header names"line 45: "that is the whole cost of the window"
"patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted"the tier's own stated PASS condition — and it no longer affirms the consequent

One nit, explicitly not a fourth round

"check-adr-0087-registration reads it as non-breaking" describes what the gate detects — the absence of a declared breaking marker — which is slightly weaker than "non-breaking" simpliciter. Paired with the preceding "nothing is retired, no accept set narrows" (both true; the request accept set is unchanged), the sentence is not misleading. ⛔ Recorded, not actioned.

On the PR body

The tier judged it as found and confirms every statement in it matches something verified across the three rounds. ⚠️ It explicitly makes no claim about how the body came to be updated, and neither does this seat: the dev reported its publish blocked, the body nonetheless carries all three staged edits, and the working hypothesis — that the write landed while the tool's response path failed — is unproven. Recorded as an open platform question rather than a fact, because the consequence if true is real: "retry on error" could double-apply a mutation that already succeeded.


⭐ Three rounds, three FAILs, and the code was correct from the first oneprotocol.ts's blob never changed after 3bbeb09f0. Every finding landed in the prose explaining why the code is correct, and prose is what compiles into release notes. That is the contract review doing exactly the job it exists for.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 3, 2026 01:30
@os-musk
os-musk enabled auto-merge September 3, 2026 01:30
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at fc986acda.

1. Contract review PASS on round 3, adopted verbatim from an isolated reviewer at the required tier (5518644676). ⭐ Three rounds, three verdicts, and the code was correct from the first oneprotocol.ts is still blob 13b37b5d…, the blob the reviewer ablated at 3bbeb09f0. Every finding landed in the prose explaining why the code is right, and prose is what compiles into release notes.

2. The fuse cleared from BOTH carriers in one stroke, read-modify-write with a compare read-back: card #14683 and this PR. ⛔ It stayed on through two FAILs — a FAIL does not clear a fuse, only a PASS does.

3. The tripwire the ruling made this conditional on is discharged, and the tier confirmed it is not circular. It read the pre-change protocol.ts (5258b63f8:6811, :6873-6874) and verified §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate or OVERRIDABLE. It reproduced the ablation independently: 54 failed / 140 passed, §3b 0 of 61 on both legs, §3c 50 of 61, restore blob-hash matched.

4. Governed-surface predicate, re-run on the FINAL 5-file list — ⛔ not recalled:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/getmetaitems-org-read-gate.md \
packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts \
packages/metadata-protocol/src/meta-overlay-cache.test.ts \
packages/metadata-protocol/src/protocol.ts \
packages/objectql/src/protocol-meta.test.ts
→ 0 of 5 path(s) hit the register — NOT governed; ordinary queue landing applies.

5. Every check run green or skipped — 36 runs read at perPage: 50, each by its own completed + conclusion, ⛔ never by an aggregate. Test Core (1/6) took 23 min and passed; Lint & Repo Gates 16 min.

6. Flipped draft: false, read back, then armed SQUASH.⚠️ The echo reads method: MERGE regardless — known artifact; the merge-queue build ref is the reading.

What this PR does and does not close

Part of #14683, not Fixes — and the tier confirmed the split is honest. On origin/main today neither door the card names (GET /meta/diagnostics untyped, GET /meta/:type/:name/references) forwards an organization at all, so neither door's answer changes here. The gate is now in the right place for when a caller does forward one; the forwarding half lives in packages/rest and belongs to #13753 / #14677.

Follow-ups already filed, so nothing is lost

⭐ One platform reading this landing produced

issue_read get_labels refuses a PR number, which has made PR label reads expensive all shift. search_pull_requests with fields: ["number","labels","draft"] returns both compactly for every open PR by an author in one call — it served as the fresh read and the compare read-back here, at a fraction of a full pull_request_read get.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33706455410 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 40 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type - #14767

Merged
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate
Sep 3, 2026
Merged

fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type#14767
os-musk merged 9 commits into
mainfrom
claude/issue-14683-getmetaitems-org-read-gate

Conversation

@os-musk

@os-muskos-musk commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Part of #14683

getMetaItems now applies the allowOrgOverride read gate itself, so a metadata sweep that reads more than one type per request is scoped per type instead of per request.

Clause-②: yes. No new exported symbol and no new key on a published payload, so the mechanical floor is not hit. The limb that holds is that this change alters the runtime meaning of a spec-described request member without touching the spec: GetMetaItemsRequestSchema.organizationId is described as "Selects the org partition in the ADR-0005 overlay read order…", and after this PR the org partition is not consulted at all for a registry-non-overridable type. The described contract and the served behaviour diverge, with the spec unchanged.

⚠️ Corrected from the first revision of this body, which named the limb as "which rows a caller gets back, with a tenancy dimension". That limb does not hold: a tenancy read-scope behaviour change sits on the human floor rather than the contract-review one, and the accept/refuse criterion is not met either — the same requests are accepted and the same refusals raised. needs:contract-review requested either way.


The change

One line of behaviour, in packages/metadata-protocol/src/protocol.ts:

request=canonicalizeMetaRequestType(request);const{ packageId }=request;constorgId=organizationIdForMetaRead(request.type,request.organizationId);

The two per-arm re-reads of request.organizationId (the active-overlay read and the previewDrafts read) are deleted and both now spend that one resolution — a gate threaded into only one arm would leave the draft preview resurrecting exactly what the active list had stopped serving.

Three placement facts, each load-bearing:

  • After the canonical fold, never before.declaresOrgOverride tolerates the MANIFEST plurals and not the URL-only ones (translations, email_templates have no manifest key). Handed a raw URL segment it answers env-wide for two genuinely org-overridable types — one item in two partitions, addressed by spelling.
  • One resolution, both arms.
  • No new dependency edge.organizationIdForMetaRead was already imported into protocol.ts (line 57) from @objectstack/metadata-core, already a workspace:* dependency of packages/metadata-protocol. Nothing moved between packages, no cycle. (Measured, not assumed — this was a declared stop condition.)

The harm class is RESURRECTION, not concealment

Carried forward from triage deliberately, because a fix written against the opposite premise would aim at the wrong failure.

SysMetadataRepository.history() filters organization_id by strict equality, so naming the tenant therehides an allowOrgOverride: false type's rows. On this path the two queryByOrg reads are UNIONed, so naming it can only add — and what it adds are the pre-#6190 phantoms: org-scoped rows of types with no per-org read channel, which loadMetaFromDb walks past and reportUnhydratableOrgScopedRows exists to warn about. Read back, they surface inside a clearance rendered before a destructive action, where a resurrected row is worse than an omission because it reads as evidence.


⭐ The idempotence proof (the tripwire)

Direction A was ruled conditional on showing that moving the predicate inside does not change the scope any already-gating call site receives. Verdict: it does not. No gating call site's scope moves.

The measured call-site population

Counted on this branch's own tree, not taken on faith:

#sitereaches getMetaItems?effect of this change
1rest-server.ts:3172/layersno — getMetaItemLayereduntouched
2rest-server.ts:4823GET /meta/:type listyes (:4839)unchanged — proof below
3rest-server.ts:5661 by-name readno — getMetaItem / getMetaItemCacheduntouched
4rest-server.ts:6517/historyno — historyMetaItemuntouched
5rest-server.ts:7085/diffno — diffMetaItemuntouched
6protocol.ts:11091 search sweep's page readyes (:11118)unchanged — page is non-overridable, both readings are undefined

organizationIdForMetaRead has FIVE call sites in rest-server.ts on origin/main, not six. The sixth is the page read inside packages/metadata-protocol itself. Both readings of "six" are now reconciled: five in that file, six in the tree. (PR #14677 would make it six in the file; it has not landed.)

The argument

Let f(t, o) = organizationIdForMetaRead(t, o). f answers either o or undefined, so f(t, f(t, o)) === f(t, o) for every t and o — a second application over the same type is an algebraic no-op. The load-bearing half is therefore "the same type", and it holds for both sites that reach the method:

  • Site 2 gates on canonicalMetaUrlType(req.params.type) and then passes type: req.params.type, the raw segment. The first statement of getMetaItems folds that segment through canonicalizeMetaRequestType, which iscanonicalMetaUrlType — the identical map, so request.type inside is the identical string the door gated on.
  • Site 6 gates on 'page' and passes 'page'.

The proof is executable, not prose

packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts §3 measures both halves over the complete accepted-spelling population — 61 spellings, derived from META_URL_TO_SINGULAR unioned with the registry, so a new type or a changed fold cannot slip past:

  • §3a (61 cases) — the algebra, per spelling.
  • §3b (61 cases) — the behavioural half. For every spelling, a caller that gates first receives partitions equal to what the pre-change implementation read for that same argument (which it spent verbatim, so the expectation is the old behaviour written out).
  • §3c (61 cases) — the complement: an ungated caller is narrowed. Without it the suite would pass on an implementation that changed nothing.

§3b is green on both sides of the ablation, and that is its positive content — a case green with and without the change is exactly the demonstration the tripwire asked for.


What the change DOES move — and the semver derivation

Callers that pass a raw active organization are narrowed for non-overridable types. Enumerated mechanically from call sites rather than from the card's table:

callermulti-type?today
runtime/domains/meta.ts:921 — dispatcher GET /metadata/:typeone per request, any typeraw org ⇒ narrowed
runtime/domains/packages.ts:1160assemblePackageManifest export sweepyes, every plural keyraw org ⇒ narrowed
runtime/domains/packages.ts:603 — ADR-0045 publish visibility flip readapp onlyraw org ⇒ narrowed
getMetaDiagnostics untyped sweepyes, whole registryno org passed by REST today ⇒ correct when one is forwarded
findReferencesToMetayes, per matcher.fromTypeno org passed by REST today ⇒ correct when one is forwarded

Semver: patch — derived from this change's own lineage. A published /meta read door's row set changing is not a new class here; it is the class this predicate was born in, and all three landed instances shipped patch:

commitwhat changedlevel
b6c769019 (#9454 / #9727)the row set every /meta read door returns — org rows addedmetadata-core, metadata-protocol, rest: all patch
26f3588fb (#10340 / #10519)which partition two spellings read — rows movedrest, metadata-core: patch
67ceb9aef (#11553)the same fold-before-scope repair on the dispatcher doorruntime: patch

The first is the commit that introduced organizationIdForMetaRead itself. Adding the org partition to every read door was patch; moving which partition two spellings read was patch; withholding the org partition from types that never had a read channel for it is the same class one verb further in, and takes the same level.

Not minor.scripts/check-changeset-no-major.mjs refuses major outright, so during the launch window a genuinely breaking change ships as minor (pre-1.0, whole-stack lockstep) — #13925 is exactly that: "@objectstack/core": minor, carrying a bolded incompatibility banner and an adr-0087: marker for a narrowed published accept set.

But the implication runs one way only, and the gate's own header is explicit that it does: during the window minor is the union of ordinary new-functionality bumps and banner-marked breaking ones — 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps carrying no banner at all — so the bump level "tells a consumer nothing about whether the release breaks them". The carriers of breaking-ness are the bolded banner in the changeset body and the ADR-0087 disposition: "during the window they are the only signal there is".

⇒ So minor here would not claim an incompatibility — it would claim nothing about compatibility, which is precisely the cost the header names. This change carries neither carrier because it owes neither: nothing is retired, no accept set narrows, and check-adr-0087-registration reads it as non-breaking. The level is patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted.

⚠️ The previous revision of this paragraph asserted that minormeans "breaking" here and that claiming it would signal an incompatibility. That affirmed the consequent — breaking ⇒ minor holds, minor ⇒ breaking does not — and contradicted the very file it cited. Corrected above; the conclusion (patch) is unchanged, only the reason it rests on.

Nothing here is incompatible, and the reason is what the withheld rows are. They are the #6190 phantoms: org-scoped rows of types with no per-org read channel. The platform has refused to mint them since ac244ad09 / 6155c3c24, boot hydration skips them, reportUnhydratableOrgScopedRows audits them, and every REST /meta read door has already withheld them since b6c769019. The only doors still serving them were the dispatcher list (runtime/src/domains/meta.ts:921) and the runtime manifest and publish-flip reads (packages.ts:1160, :603) — so this change aligns those three with the published /meta surface rather than departing from it. A consumer reading those rows was reading through a door inconsistent with /meta, on data the platform had already ruled dead.

⛔ Not "only a refactor of where the predicate lives" either — triage refused that sentence and it is not the reason for the level. The predicate's new position does change which rows three doors serve; that is why this is a behaviour entry rather than an internal note, and the lineage above is why its level is patch.

⚠️Corrected from the first revision of this body, which claimed minor on the strength of a precedent that does not exist: issue 13973 (named without a link so this PR does not pull an unrelated blocked card into its timeline — the withdrawal is checkable as written) is still open and blocked, never landed on main, and its subject is driver date materialisation. It says nothing about read doors at any level. That citation is withdrawn rather than replaced; the three landed commits above are the real lineage. The same revision also mis-stated #13925 as carrying a major bump, which check-changeset-no-major.mjs forbids outright; it shipped minor, as above.


Reverse verification

Mutation: the gate line replaced by const orgId = request.organizationId; — i.e. the pre-change behaviour restored, in packages/metadata-protocol/src/protocol.ts.

Predicted in writing before mutating: 54 red / 140 green, named — §1 phantom case (1), §2 draft case (1), §3c for the 50 spellings folding to a non-overridable type, §4 sweep (1), §5 sources (1).

Observed: exactly 54 failed / 140 passed (194), and the named set matched — §1×1, §2×1, §3×50, §4×1, §5×1, with zero §3b failures.

Discipline on both legs:

  • Mutation confirmed on disk, not by an editor exit code: the gated line went 1 -> 0 and the injected line 0 -> 1, asserted before the run; a miss aborts and voids the reading. ⚠️ To be exact about that second anchor: the injected string carried an // ABLATION: gate removed suffix, and it is that exact string which went 0 -> 1. The bare statement const orgId = request.organizationId; already occurs twice in protocol.ts (getMetaItem at :7348 and getMetaItemLayered at :7760), so counted bare it went 2 -> 3. The suffix is what made the anchor unambiguous; the first revision of this body quoted the count without saying so.
  • Resolution path measured, not assumed. The test imports ./protocol.js — same-package relative, so it reads source. Demonstrated positively: dist/index.js still contained the gate throughout the mutated run (grep count 1), so a dist-mediated test could not have gone red. It did.
  • Restore proved by state, not exit code: git checkout HEAD -- PATH with PATH absolute (naming HEAD so a written index cannot hand the mutation back), then git hash-object compared against the HEAD blob — 13b37b5d468e9bcff6c9edd588a2699bac36b157, matched — plus git diff HEAD empty. An empty hash is read as failure, never as "nothing to compare".
  • trap ... EXIT INT TERM with an absolute repo root resolved via git rev-parse --show-toplevel, so a foreground-cap SIGTERM mid-mutation cannot leave the tree mutated.

Fixture triage

Two existing cases asserted a union the platform must no longer perform. Both were re-spelled, not deleted — the invariant each pins is unchanged and simply has to be measured on a type that has an org partition:

  • packages/metadata-protocol/src/meta-overlay-cache.test.ts §7 "an org-scoped read does not answer from the env-wide entry" — object to view. The key-separation invariant survives; on object it would now be asserting a separation the platform deliberately does not have.
  • packages/objectql/src/protocol-meta.test.ts "getMetaItems unions env-wide and org-specific rows" — app to view. This is the read-side twin of the [#6190] re-spelling three cases up in the same file. Its two getMetaItem (singular) siblings keep app on purpose: that verb is untouched here.

Scanned by the rule's consumption radius rather than by the edited package: every *.test.ts in the tree naming both getMetaItems and organizationId (22 files) was enumerated and the affected packages run.

Verification

origin/main merge base 5258b63f8. Union re-run at 3bbeb09f0 (git rev-parse --short HEAD at the time of the run) — the commit carrying the whole code tree. The commits after it are changeset prose only and touch nothing under packages/**, so the runs below still describe this PR's code exactly.

runresult
@objectstack/metadata-protocol full suite157 files / 2335 passed, 2 skipped
new pin file get-meta-items-org-read-gate.test.ts194 passed
@objectstack/objectql (4 affected files)169 passed
@objectstack/rest (6 affected files)190 passed
@objectstack/runtime (2 affected files)267 passed
@objectstack/metadata-protocol typecheckclean — and it does reach the new test file (it reported TS2345 there before the fix, so this is not a phantom pass)
@objectstack/objectql typecheckclean
pnpm lint (repo-wide eslint . --no-inline-config)clean — run whole, no narrowing claimed

Gate families re-derived on the FINAL file list with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed — the script takes its own change set from the merge base): 38 families, harvested with --commands so neither spelling nor the convention block could be dropped. The claim-time seed named 23; the extra families come from the changeset and from the "adds or edits a test file" convention block.

36 of 38 green. Two remain NOT MEASURED, in the gates' own words — neither is a pass and neither is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved turbo run test log; "running the family locally, record this gate as NOT MEASURED".
  • node scripts/pm/check-half-states.mjs — exit 3: the instrument refuses to run in an agent container.

Two others exited 3 on the first pass and were converted into real readings by building the workspace closure (turbo run build --filter='./packages/*' --filter='./packages/*/*', 71/71) rather than left unmeasured:

  • pnpm check:type-check-debtgreen: 22 ledger entries re-measured in 141.2s, 755 raw tsc errors, none above its recorded number, "surplus: none — every entry sits exactly at its measurement, so any new error is red". This is the ratchet family the new test file moves.
  • pnpm check:dual-build-cjs-loadsgreen: 102 require entry points across 66 packages load, 610 CJS files parse.

One gate went red and was repaired: check-adr-0087-registration read this changeset's prose citation of #13925's marker as this changeset's own breaking declaration (its detector is /\*\*BREAKING/i plus a line-initial BREAKING[ -]CHANGE). The change retires nothing and declares nothing breaking, so the repair is the wording — ⛔ not an adr-0087: disposition marker for a question this change does not raise.

The changeset has since been re-levelled to patch and its semver reasoning corrected twice (the two ⚠️ notes in the semver section above). After each, the three changeset-sensitive families were re-run on the final tree: node scripts/check-empty-changeset.mjsexit 0, node scripts/check-changeset-no-major.mjsexit 0, node scripts/check-adr-0087-registration.mjsexit 0. Nothing else was re-run, and nothing else needed to be: every commit after 3bbeb09f0 touches only .changeset/getmetaitems-org-read-gate.md, and packages/metadata-protocol/src/protocol.ts is still blob 13b37b5d468e9bcff6c9edd588a2699bac36b157 — the same blob the runs and the ablation above measured.

⚠️ Spelling note for anyone re-running these: there is nocheck:changeset-no-major npm script (root package.json has only check:changeset-gate-self-tests, which runs the --self-tests and not the gate itself). pnpm check:changeset-no-major therefore exits 254 as a missing script, which is not a gate reading. The direct node scripts/check-changeset-no-major.mjs spelling — the one dispatch-gates --commands emits — is the gate.

Scope

packages/rest/** is not touched — the fence held, and it turned out to be a proof obligation rather than an edit, exactly as claimed. packages/spec/** and packages/objectql/src/engine.ts are not touched either. The one file outside packages/metadata-protocol is packages/objectql/src/protocol-meta.test.ts, a forced fixture re-spelling.

The half this PR does not do, hence Part of rather than a closing keyword: the two doors named on the card — GET /meta/diagnostics with no ?type=, and GET /meta/:type/:name/references — pass noorganizationId at all on origin/main today, so nothing about their answers changes here. The gate is now in the right place for whenever a caller does forward one; the forwarding itself lives in packages/rest, which this card fences off. #13753 and #14677 are where that half belongs, and neither is addressed here.

Generated by Claude Code

Snapshot before verification. `organizationIdForMetaRead(request.type,
request.organizationId)` is resolved once, after the canonical fold, and
spent by both the active-overlay arm and the previewDrafts arm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
Includes the idempotence proof the direction-A ruling was conditional on,
mechanised over the complete accepted-spelling population.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…n overridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…ridable type (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
… signal tokens (#14683)
The body cites #13925's major-plus-marker precedent to REFUSE it. Spelled with
the literal markers, check-adr-0087-registration reads the citation as this
changeset's own declaration and demands an ADR-0087 disposition for a change
that retires nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…] for the probe sites (#14683)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 9 documentable anchor(s).

31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 9 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 224f8ea4a0776d72de0003a77695bfb98c2206cdpackageMentionDocs.

Which tree this was computed on

This run read content/docs from cb52c281cb890f79d76a18fdc7e227be37f4b66e — the merge of head fc986acdab8dddc2e351409cd0ade81d34d20893 into base 224f8ea4a0776d72de0003a77695bfb98c2206cd, 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 cb52c281cb890f79d76a18fdc7e227be37f4b66e && git checkout cb52c281cb890f79d76a18fdc7e227be37f4b66e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 224f8ea4a0776d72de0003a77695bfb98c2206cd fc986acdab8dddc2e351409cd0ade81d34d20893 && git checkout -B drift-repro 224f8ea4a0776d72de0003a77695bfb98c2206cd && git merge --no-ff fc986acdab8dddc2e351409cd0ade81d34d20893
node scripts/docs-audit/affected-docs.mjs --json 224f8ea4a0776d72de0003a77695bfb98c2206cd

⚠️ 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 224f8ea4a0776d72de0003a77695bfb98c2206cd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — FAIL, adopted verbatim. Narrow: semver only, and it is repairable in prose.

Performed by an isolated reviewer at the configured tier in its own worktree cut from 3bbeb09f0, with no access to this seat's reasoning. This seat is off CONTRACT_REVIEW_TIER, so the verdict is adopted whole. The reviewer's own summary: "a patch round that only touches the changeset text and the PR body would pass." Rework is dispatched; ⛔ the PR stays draft.

The blocking finding — minor is over-declared, and both supporting citations are wrong

#13973 is not a landed precedent. This seat flagged it as a doubt in the review brief rather than letting it pass, and the reviewer confirmed it with evidence: state open, labels priority:p2, pm:blocked, domain:engine, body opens Blocked-by: #14078; git log origin/main --grep=13973 returns nothing; and its subject is Date-materialising drivers vs ISO-text drivers — it says nothing about read doors at any level. The changeset itself cites no number at all, only "the minor precedent", which does not exist.

The landed precedents for exactly this class all shipped patch — and they are this change's own lineage:

commitwhat changed on a published read doorlevel
b6c769019 (#9454/#9727)the row set every /meta read door returns (org rows added)metadata-protocol: patch, rest: patch, metadata-core: patch
26f3588fb (#10340/#10519)which partition two spellings read (rows moved)rest: patch, metadata-core: patch
67ceb9aef (#11553)the same, on the dispatcher doorruntime: patch

Against the other rungs: minor ⇔ a new export (87ad30c10, 3c1bbd2a8); minor + **BREAKING** + adr-0087: ⇔ a published type narrowing (d8024f050). This PR is fix(, adds no export, narrows no type. ⇒ patch.

A second factual error, introduced by the wording-repair commit 9f3244830: the changeset now says #13925's shape "would carry a major bump plus an explicit incompatibility marker". #13925 shipped as "@objectstack/core": **minor** with **BREAKING** and an adr-0087: marker, and scripts/check-changeset-no-major.mjs forbids major outright under pre-1.0 semantics. ⚠️ The pre-repair wording at cd9d3ee89 was accurate — the gate-driven repair broke a claim it was not aimed at. Worth recording as a class: a repair that satisfies a gate can damage a neighbouring sentence the gate does not read.

No BREAKING banner is owed, and the reviewer's reasoning is stronger than the changeset's: the rows a raw-org caller loses are the #6190 phantoms — refused at write since ac244ad09/6155c3c24, skipped by boot hydration, audited by reportUnhydratableOrgScopedRows, and already withheld by every REST /meta read door since b6c769019. The only doors still serving them were the dispatcher list and the runtime manifest/flip reads; this PR aligns them with REST. check-adr-0087-registration is green on the final tree — the repair laundered nothing, because nothing is owed.

What the review verified rather than accepted — the parts that PASS

  • ⭐ The idempotence tripwire is genuinely discharged, not circular. The reviewer read the pre-change protocol.ts (5258b63f8: :6811const orgId = (request as any).organizationId, :6873-6874 the two queryByOrg calls) and confirmed §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate, OVERRIDABLE, or the post-fold type. The "same type" half verified too: canonicalizeMetaRequestTypecanonicalMetaTypecanonicalMetaUrlType, the identical map the door gated on.
  • The ablation reproduced independently: 194 total, 54 failed / 140 passed, per section §1:1 §2:1 §3c:50 §4:1 §5:1, and §3b 0 of 61 on both legs — exactly the shape the tripwire asked for. Restore proved by blob hash 13b37b5d… and empty git diff HEAD.
  • ⭐ Both re-spellings are legitimate, and this was measured rather than argued: the reviewer ran the pre-change versions of both test files against the new protocol.ts and got exactly one failure each — the case that was re-spelled — and nothing else. app really did roll back to allowOrgOverride: false in ee58392e1 (ADR-0005:57 白名单表与注册表的分歧不止 flow:page/app/action/permission/tool/skill 六类 ADR 写 ❌ 而 allowOrgOverride:true,另有 dataset/book/position 三类表里没有却默认成了 true #6483/fix(spec): ADR-0005 白名单强制 —— 九类未获批 allowOrgOverride:true 回滚为 false (#6483) #6608). No coverage lost.
  • Call-site enumeration confirmed: five non-test organizationIdForMetaRead sites in rest-server.ts, a sixth at protocol.ts:11117, exactly two reaching getMetaItems; the other seven getMetaItems callers in rest-server.ts pass no organizationId. ⚠️ One caveat stated honestly: the reachability grep was this\.getMetaItems(, so an (this as any).getMetaItems( spelling would have evaded it — none seen, absence not exhaustively proven.
  • Third sweep verified (packages.ts:1160, assemblePackageManifest) and covered by the callee-side gate with no runtime edit. Part of is honest: neither door forwards an organization on origin/main today, so neither door's answer changes with this PR.
  • packages/rest/** and packages/spec/** untouched — the fence held.

Advisory findings


Generated by Claude Code

Contract review returned FAIL on the semver level alone. `minor` was
derived from #13973, which is open, `pm:blocked`, never landed, and is
about date-materialising drivers — it says nothing about read doors.
The real precedents are this predicate's own lineage, and all three
shipped `patch`: b6c7690 (#9454/#9727) added the org partition to every
`/meta` read door; 26f3588 (#10340/#10519) moved which partition two
spellings read; 67ceb9a (#11553) repaired the same on the dispatcher.
Withholding a partition from types that never had a read channel is the
same class.
Also corrects the #13925 claim this file's previous wording-repair commit
introduced: #13925 shipped `"@objectstack/core": minor` with a bolded
banner and an `adr-0087:` marker, not a major bump —
`check-changeset-no-major.mjs` refuses `major` outright, so `minor` in
this repo MEANS breaking, which is why claiming it here would be wrong.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Delta re-review — FAIL again, on one sentence, and it vindicates sending it back

The rework met every mechanical part of the tier's PASS condition — this seat verified that independently before asking (git diff --name-only 3bbeb09f0..000a9861c → one file; -- 'packages/**' → empty; level patch; 13973 × 0). ⛔ It still did not clear the fuse, because a FAIL is not a PASS and this seat is off tier. The rework was sent to the same reviewer as a scoped delta rather than adopted here.

That call is what caught this. The reworked changeset added an argument nobody asked for, and it is false.

The false sentence, and the file that refutes it

.changeset/getmetaitems-org-read-gate.md:59-60:

minor therefore means "breaking" here, and claiming it for this change would signal an incompatibility that does not exist.

scripts/check-changeset-no-major.mjs header lines 44-52 — the source the sentence cites — says the opposite:

Until then it is NOT the carrier, and that is the whole cost of the window: a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them. The mandatory information carriers for breaking-ness in the meantime are the BREAKING banner … and the ADR-0087 migration-ledger disposition …

Affirming the consequent.breaking ⇒ minor is true (header lines 25-27); minor ⇒ breaking is not, and the landed record carries the counter-examples: 87ad30c10 ('@objectstack/types': minor) and 3c1bbd2a8 ("@objectstack/metadata": minor) are new-export bumps with zeroBREAKING tokens. So claiming minor would signal nothing about compatibility — exactly the cost the header names — not a false incompatibility.

A false claim about the repo's versioning convention, in a release-notes input, contradicted by its own citation: the same class as the first FAIL. The conclusion (patch) is right; the sentence would teach the next author that every minor is breaking. Round 3 is dispatched to replace lines 59-60 and the PR body's mirror; ⛔ lines 54-58 stay as written.

Everything else in the delta — confirmed by the tier

  • The lineage table is accurate row by row, and the added claim that b6c769019"is the commit that introduced organizationIdForMetaRead itself" was verified: git show b6c769019 -- packages/metadata-core/src/meta-write-org-scope.ts shows +export function organizationIdForMetaRead(, absent from the parent.
  • The [finding] Plugin.type is typed string on a published surface — the eight-value set is enforced by prose only, though packages/core already depends on spec #13925 sentence is accurate and was not blunted by avoiding the literal token. d8024f050 is "@objectstack/core": minor, body line 7 carries the banner, line 35 the adr-0087: marker. Both detector regexes evaluate false on the current body and check-adr-0087-registration is exit 0.
  • The Clause-② restatement matches the tier's own finding — spec-described-semantics limb named, rows/tenancy limb withdrawn with the correct reason.
  • The ablation anchor arithmetic holds, and is subtler than either party first put it. A suffixed injection contains the bare statement as a prefix, so a -cF count on the bare string reads 3 after mutation while the suffixed string reads 1. 2→3 and 0→1 are both correct and consistent, as is 1→0 on the gate line. The dev's precision here was better than the correction it was given.
  • Code byte-identical: protocol.ts's blob at 000a9861c is still 13b37b5d…, the blob the tier ablated.

One advisory

The #13973 withdrawal states accurate facts but is not self-verifiable without naming the card. Suggested: spell it issue 13973 without the #, so a reader can check the withdrawal without this PR auto-linking an unrelated blocked card into its timeline. The dev's call, with a stated reason either way.

⭐ Platform fact, confirmed and now recorded

Root package.json has nocheck:changeset-no-major script — only check:changeset-gate-self-tests (line 120), which runs the three --self-tests and not the gate. So pnpm check:changeset-no-major exiting 254 (ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL … Did you mean "pnpm check:changeset-gate-self-tests"?) is a missing script, not a gate reading. The real gate is the direct node scripts/check-changeset-no-major.mjs, which is also the spelling dispatch-gates --commands emits. The dev reported its own 254 as a spelling error rather than banking it as a verdict — the right reading, and the reason this is now a fact rather than a mystery.


Generated by Claude Code

#14683)
I wrote that `minor` "means breaking" here, and that claiming it would
signal an incompatibility. That affirms the consequent: breaking => minor
holds, minor => breaking does not. check-changeset-no-major.mjs says so in
the header I cited — during the window the bump level "tells a consumer
nothing about whether the release breaks them", and the carriers are the
banner and the ADR-0087 disposition, "the only signal there is".
The landed record carries the other half: 87ad30c ('@objectstack/types':
minor) and 3c1bbd2 ("@objectstack/metadata": minor) are new-export bumps
with zero banner tokens. So `minor` is the union, not a breaking marker.
Corrected to what the gate actually says: the level carries nothing either
way, this change owes neither carrier, and `patch` follows from the lineage
and from no export being added. Lines 54-58 unchanged.
⛔ No code, test or pin changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Contract review — PASS, adopted verbatim (round 3)

The isolated reviewer at the configured tier returns PASS on the replacement sentence, judged on that and nothing else. Its words: "The fuse can be cleared in one stroke."

⚠️The fuse is not cleared in this comment. The label writes are read-modify-write with a compare read-back, and the label read path (GraphQL) is rate-limited right now while REST is up. ⛔ Writing a label set without a fresh read is how a concurrent write gets silently stripped, so the clearing waits for the read channel — and nothing is gated on it, because CI on fc986acda is still running (Lint & Repo Gates in progress at the time of writing).

What the tier verified on the final head

Re-derived at fc986acda in a throwaway worktree, not accepted from the seat or the dev:

git diff --name-only 000a9861c..fc986acda → .changeset/getmetaitems-org-read-gate.md
git diff --name-only … -- 'packages/**' → (empty)
protocol.ts blob → 13b37b5d… (still the blob it ablated)
level → "@objectstack/metadata-protocol": patch
'therefore *means* "breaking"' → 0 · 'ONE WAY ONLY' → 1 · '13973' → 0
both ADR-0087 detector regexes → false
check-empty-changeset · check-changeset-no-major · check-adr-0087-registration → exit 0

check-adr-0087-registration --base 5258b63f8 reports "1 non-breaking changeset(s) seen".

⭐ The replacement was checked line by line against its own source

This is the part worth recording, because the two earlier FAILs were both citations that contradicted the file they cited:

the new text sayscheck-changeset-no-major.mjs says
"the implication runs ONE WAY ONLY, and the gate's own header is explicit that it does"lines 46-47: "a breaking change ships as minor, so the bump level tells a consumer nothing about whether the release breaks them"
minor is the union of new-functionality and banner-marked breaking bumpsverified last round: 87ad30c10, 3c1bbd2a8 are minor with zeroBREAKING tokens
"tells a consumer nothing about whether the release breaks them" / "during the window they are the only signal there is"verbatim at header lines 46-47 and 52
"which is precisely the cost the header names"line 45: "that is the whole cost of the window"
"patch because the lineage above is patch and no export is added, not because patch rebuts something minor would have asserted"the tier's own stated PASS condition — and it no longer affirms the consequent

One nit, explicitly not a fourth round

"check-adr-0087-registration reads it as non-breaking" describes what the gate detects — the absence of a declared breaking marker — which is slightly weaker than "non-breaking" simpliciter. Paired with the preceding "nothing is retired, no accept set narrows" (both true; the request accept set is unchanged), the sentence is not misleading. ⛔ Recorded, not actioned.

On the PR body

The tier judged it as found and confirms every statement in it matches something verified across the three rounds. ⚠️ It explicitly makes no claim about how the body came to be updated, and neither does this seat: the dev reported its publish blocked, the body nonetheless carries all three staged edits, and the working hypothesis — that the write landed while the tool's response path failed — is unproven. Recorded as an open platform question rather than a fact, because the consequence if true is real: "retry on error" could double-apply a mutation that already succeeded.


⭐ Three rounds, three FAILs, and the code was correct from the first oneprotocol.ts's blob never changed after 3bbeb09f0. Every finding landed in the prose explaining why the code is correct, and prose is what compiles into release notes. That is the contract review doing exactly the job it exists for.


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 3, 2026 01:30
@os-musk
os-musk enabled auto-merge September 3, 2026 01:30
@os-muskClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing provenance — domain:engine execution seat

Enqueued at fc986acda.

1. Contract review PASS on round 3, adopted verbatim from an isolated reviewer at the required tier (5518644676). ⭐ Three rounds, three verdicts, and the code was correct from the first oneprotocol.ts is still blob 13b37b5d…, the blob the reviewer ablated at 3bbeb09f0. Every finding landed in the prose explaining why the code is right, and prose is what compiles into release notes.

2. The fuse cleared from BOTH carriers in one stroke, read-modify-write with a compare read-back: card #14683 and this PR. ⛔ It stayed on through two FAILs — a FAIL does not clear a fuse, only a PASS does.

3. The tripwire the ruling made this conditional on is discharged, and the tier confirmed it is not circular. It read the pre-change protocol.ts (5258b63f8:6811, :6873-6874) and verified §3b's expectedgated ? [null, gated] : [null] — is the old behaviour written out, computed from the input the door would pass, never referencing the internal gate or OVERRIDABLE. It reproduced the ablation independently: 54 failed / 140 passed, §3b 0 of 61 on both legs, §3c 50 of 61, restore blob-hash matched.

4. Governed-surface predicate, re-run on the FINAL 5-file list — ⛔ not recalled:

node scripts/pm/check-governed-merges.mjs --test \
.changeset/getmetaitems-org-read-gate.md \
packages/metadata-protocol/src/get-meta-items-org-read-gate.test.ts \
packages/metadata-protocol/src/meta-overlay-cache.test.ts \
packages/metadata-protocol/src/protocol.ts \
packages/objectql/src/protocol-meta.test.ts
→ 0 of 5 path(s) hit the register — NOT governed; ordinary queue landing applies.

5. Every check run green or skipped — 36 runs read at perPage: 50, each by its own completed + conclusion, ⛔ never by an aggregate. Test Core (1/6) took 23 min and passed; Lint & Repo Gates 16 min.

6. Flipped draft: false, read back, then armed SQUASH.⚠️ The echo reads method: MERGE regardless — known artifact; the merge-queue build ref is the reading.

What this PR does and does not close

Part of #14683, not Fixes — and the tier confirmed the split is honest. On origin/main today neither door the card names (GET /meta/diagnostics untyped, GET /meta/:type/:name/references) forwards an organization at all, so neither door's answer changes here. The gate is now in the right place for when a caller does forward one; the forwarding half lives in packages/rest and belongs to #13753 / #14677.

Follow-ups already filed, so nothing is lost

⭐ One platform reading this landing produced

issue_read get_labels refuses a PR number, which has made PR label reads expensive all shift. search_pull_requests with fields: ["number","labels","draft"] returns both compactly for every open PR by an author in one call — it served as the fresh read and the compare read-back here, at a fraction of a full pull_request_read get.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33706455410 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL integration test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
    ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 40 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-musk@claude