rest/meta: state the caller's organization on the /meta/diagnostics ?type= sweep - #14677
rest/meta: state the caller's organization on the /meta/diagnostics ?type= sweep#14677os-trump wants to merge 8 commits into
/meta/diagnostics ?type= sweep#14677Conversation
`GET /api/v1/meta/diagnostics` never supplied an `organizationId`, so an organization's own overlays were absent from the Studio governance sweep — clean tiles over a partition it never read. `getMetaDiagnostics` already declares and reads the member; the gap was entirely at the REST call site. The `?type=` arm now resolves the memoised exec ctx and passes the registry-gated `organizationIdForMetaRead(canonicalMetaUrlType(type), …)`, matching the five sibling call sites in this file. The untyped whole-registry sweep is left env-wide on purpose and the reason is recorded at the call site: it spans types with different `allowOrgOverride` while the request carries one `organizationId`, and `getMetaItems` applies no registry gate of its own — so a tenant named there would union pre-#6190 phantom rows into a governance report. `GET /meta/:type/:name/references` is likewise left unchanged with its measurement recorded in place: `req.params.type` is the TARGET, while `findReferencesToMeta` spends the organization on the SOURCE types it sweeps, so no value this call site can pass expresses the correct scope. Pins extend the existing meta read org-scope harness, including the phantom-row control that discriminates the registry-gated predicate from a raw tenant id, and the recorded gap on the untyped arm. Part of #13753 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…r edit `node scripts/check-system-context-census.mjs --fix` — five `ctx.isSystem` anchors on the permissions page moved because the diagnostics call site grew. Pure line rot; no elevation behaviour changed. The gate reads OK afterwards: 109 elevation read sites in 20 packages across 45 files, all anchored. Part of #13753 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…g, and count it Two coupled corrections found by running the package suite: 1. `execctx-consumer-census` counts every `this.resolveExecCtx(environmentId, req)` site in this file and pins the totals. The diagnostics door adds one, so 75 -> 76 sites, 95 -> 97 mentions (one call site plus one prose mention), 22 -> 23 locally caught. The bare/inline splits are unchanged. 2. The site is now resolved inside an `if (diagnosticsType)` block instead of a ternary. A ternary puts `.catch(rethrowAuthzStoreUnavailable)` on a continuation line with no trailing `;`, which is a THIRD layout the census's `catchArguments()` reader cannot see — it counted 23 caught sites and found 22 arguments, reddening the section 7 CONTROL that exists precisely so a reader which stops matching cannot look like a clean pass. Conforming to the spelling the sibling doors already use was preferred over teaching the reader a new layout. Part of #13753 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…reshape `check-system-context-census.mjs --fix` again — the same five `ctx.isSystem` anchors moved by 3 lines when the diagnostics exec-ctx resolution became a statement. Pure line rot; the gate reads OK afterwards. Part of #13753 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…ta-read-doors-org-forwarding
📓 Docs Drift Check
What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
…ollows) The only conflicted path was content/docs/permissions/system-context.mdx, a MIXED os-regen artifact. Both sides' prose was byte-identical; the six differing tokens were all `:NNN` citation anchors. Main's side taken (the both-sides rule in scripts/pm/os-regen-merge.sh step 2); the anchors are re-derived from the merged tree by pnpm gen:system-context-census in the following commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
pnpm gen:system-context-census, run after the merge commit. Row 50's five rest-server.ts citations move back to the branch's positions (the branch adds lines above them); rows 48/49 keep main's action-execution.ts anchors. Neither side's numbers were correct for the merged tree, which is why this is regenerated rather than hand-resolved. check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
|
os-dev-report {
"issue": 14677,
"status": "done",
"branch": "claude/issue-13753-meta-read-doors-org-forwarding",
"pr": "https://github.com/objectstack-ai/objectstack/pull/14677",
"premise_still_valid": true,
"merge_commit": "669f8ac2f4 (merge origin/main 2d40f91468 into the branch at 678c626de7); regeneration committed separately as 6d49f74f83, which is the pushed head",
"summary": "Landing task only: no behaviour added, no scope widened. Merged origin/main (66 commits) into the branch via scripts/pm/os-regen-merge.sh; its step 1 stopped on exactly one conflict and the script refused to continue by design, so steps 2-4 were completed by hand following its own runbook. mergeable_state moved dirty -> blocked. The PR's falsified /references premise, its reversed harm direction (getMetaItems UNIONs, so naming an org can only ADD rows; the hazard is resurrection, not concealment) and both ablation narratives all survive intact and were re-measured rather than carried forward. Two dispatch expectations were corrected by measurement: (1) the conflict was NOT in packages/rest/src/rest-server.ts - main changed no line of that file in the merge window, so it merged cleanly; the sole conflict was the generated census page; (2) the merge brought in #14767, which is verbatim the proposal this PR wrote up and deliberately did not apply, landing organizationIdForMetaRead INSIDE getMetaItems.",
"conflicts": [
{
"path": "content/docs/permissions/system-context.mdx",
"resolution": "regenerated, not hand-resolved",
"detail": "MIXED os-regen artifact. The os-regen driver refuses to auto-merge it (generated anchors plus hand-written prose) and text-merges instead, which is why GitHub reported dirty. Both sides' prose was proved byte-identical by blanking every :NNN token and diffing (IDENTICAL_AFTER_ANCHOR_BLANKING); the only 6 differing tokens were citation anchors. Took main's side per step 2's both-sides rule - the hand resolution's blob b26e78e827 equals origin/main's blob for that path exactly - then ran pnpm gen:system-context-census on the merged tree. The generator moved row 50's five rest-server.ts anchors BACK to the branch's positions (4716->4790, 6079->6204, 6327->6452, 6758->6883, 6951->7076) while rows 48/49 keep main's action-execution.ts:138/:401. Neither side's numbers were correct for the merged tree, which is exactly why this file is regenerated and never hand-picked. Staged diff inspected with git diff --cached per the script's runbook sentence, not git diff."
}
],
"no_other_conflicts": "packages/rest/src/rest-server.ts did NOT conflict. git log 2aa8456cf2..origin/main -- packages/rest/src/rest-server.ts is empty. The branch's delta against merged main is still exactly the same 5 files, 391 insertions / 8 deletions, byte-identical to the pre-merge diffstat.",
"fenced_files_not_reached": "Confirmed mechanically: packages/rest/src/error-response.ts (PR #14713) is absent from git diff --name-only 678c626de7..HEAD, and handleApprovalError (card #14573) has 0 occurrences in the whole delta. Neither was touched.",
"tests": "All heavy stages via scripts/pm/os-verify-lock.sh; verdicts read from its VERDICT line, exit codes captured by redirect-first (cmd > file 2>&1; rc=$?), never after a pipe. Two earlier attempts returned exit 99 queue-timeout (NOT MEASURED, not a pass) behind a 23-minute holder and were retried on the same OS_VERIFY_LOCK_SLOT until acquired. || ABLATIONS, both re-run on the merged tree, each leg: baseline asserted (worktree blob == HEAD blob 6ad0773ecb) -> mutate -> prove on disk -> run -> restore -> prove by blob hash, all under trap restore EXIT INT TERM with absolute paths. A (drop the organization): removed-marker 0 / injected-marker 1, mutated blob f9c0c97b44, result 'Tests 6 failed | 24 passed (30)' - UNCHANGED from the predicted 6/24. B (raw tenant instead of the predicate): removed-marker 0 / injected-marker 1, mutated blob 4da1289a2f, result 'Tests 30 passed (30)' - MOVED from the predicted 1 failed / 29 passed to 0 failed / 30 passed. Restore proved each time: 'restored: blob 6ad0773ecb6ecc8701b78ea3ca5d98e420de0d78 == HEAD blob, git diff HEAD empty'. Restore leg on the unmutated tree: 30 passed (30). || WHY B MOVED: #14767 now applies organizationIdForMetaRead(request.type, request.organizationId) inside getMetaItems (protocol.ts:6842), so the single control B used to redden - '?type=object stays env-wide and does NOT resurrect a phantom org row' - is now held one layer down. B going green is NOT the control weakening: it still runs and still passes; it simply no longer discriminates THIS call site, because the behaviour is now enforced in two places. || typecheck: pnpm --filter @objectstack/rest typecheck exit 0, its own verdict line 'check:test-typecheck: OK - @objectstack/rest's test layer compiles under packages/rest/tsconfig.test.json; 0 file(s) / 0 error(s) / 0 pinned signature(s)'. || whole package: pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 exit 0, 'Test Files 173 passed (173)', 'Tests 2896 passed (2896)' - up from the recorded 172/2886 because the merge adds main's packages/rest/src/package-registry-item-projection.test.ts and its rewrite of meta-state-route-doc-spelling.test.ts. Nothing this branch owns changed count. || whole-repo pnpm lint (eslint . --no-inline-config): exit 0, no narrowing - re-run rather than carried forward. || GATES: union re-derived at the new head by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (stderr confirms 'derived from the tree ... at commit 6d49f74f83' and '--repo ... it holds'); 61 commands, each run with its exit code captured before any pipe: 56 pass, 5 NOT MEASURED, 0 red. The 5 NOT MEASURED are the same five as pre-merge, re-measured not copied: check-test-completeness (3, needs a saved turbo run test log), pm/check-half-states (3, repo-scoped reads refused in this container), check:dual-build-cjs-loads (3, PREREQUISITE NOT MET - needs a whole-repo build), check:type-check-debt (3, re-measure mode, a maintainer's act), @objectstack/spec check:skill-examples (1, refusal - packages/client-react/dist holds no .d.ts). Three that refused on the first sweep (spec check:docs, lint check:doc-formula-expressions, check:doc-security-posture) pass on re-run after the closure build and are counted in the 56. || check-nul-bytes: OK, 8071 files, 0 control bytes; the edited file self-scanned separately with grep -naP, 0 hits.",
"cli_e2e_flake": "packages/cli/test/run-dev-unbuilt-workspace.e2e.test.ts - its fix accb9231c7 is confirmed an ancestor of the merged head (git merge-base --is-ancestor accb9231c7 HEAD passes). Not exercised directly: the verification scope for this landing task is @objectstack/rest, so the cli package was not run and I make NO claim either way about whether it still flakes. CI will read it.",
"mcp_calls": "4",
"stopped_on_rather_than_guessing": [
"The call-site comment block in packages/rest/src/rest-server.ts (around line 4574) asserts 'getMetaItems applies NO registry gate of its own ... the only organizationIdForMetaRead call inside metadata-protocol is the page read in protocol.ts, nothing on this path'. That is FALSE on the merged tree after #14767. I did NOT rewrite it: the correct replacement text depends on whether the door-side predicate should now be kept as defence in depth or removed as redundant, which is a design decision, not a merge resolution. Recorded in the PR body instead.",
"Correspondingly, protocol.ts:6822-6826 (main's own comment, from #14767) enumerates 'the four remaining organizationIdForMetaRead call sites in rest-server.ts' and states they never reach getMetaItems. This branch adds a fifth that DOES reach it (via getMetaDiagnostics). The claim is not wrong about the four it names, but the enumeration is incomplete once this PR lands. Also left alone - it is main's file and outside this PR's scope."
],
"open_questions": [
{
"question": "#14767 landed the exact proposal this PR wrote up. Should the door-side predicate added here be kept, or removed as redundant?",
"options": [
"A - keep it: it is idempotent over the same type (both applications fold the identical string), it is defence in depth if the inner gate is ever moved, and ablation A still shows the six repair cases go red without it",
"B - remove it and let getMetaItems' own gate carry the typed arm, shrinking this PR to its pins plus the recorded-gap documentation"
],
"recommendation": "A, keep it, and land this PR as-is. Long-term soundness leads: the predicate at the door is the spelling every sibling meta read door in rest-server.ts already uses, so keeping it holds one convention across 6 call sites rather than making this one door the exception that reads scope differently from its neighbours - and ablation A measured, on the merged tree, that removing the organization at the door still reddens 6 cases, so the call site is doing real work that the inner gate cannot do for it. Real business need is neutral-to-positive (the ?type= arm is Studio's per-type drill-down, and it is repaired either way). AI-error-resistance favours A: two independent gates on the same predicate is the fail-closed direction, and B would make correctness depend on a remote invariant a reader of this door cannot see. Startup scope discipline is the only axis pointing at B, and it points weakly - A adds no surface, no export and no accept set. ⛔ Note this is a recommendation about a DESIGN question raised by the merge, not something I acted on."
},
{
"question": "#13753 names two doors. #14767 appears to have addressed the second one (/references) from inside getMetaItems - its changeset names findReferencesToMeta as one of the three callers it repairs. Is the card now closable once this PR lands?",
"options": [
"A - PM re-triages #13753 against the merged tree and closes it if /references and the untyped sweep are both genuinely covered",
"B - keep the card open pending a pin that measures /references end-to-end through the REST door rather than trusting the changeset's claim"
],
"recommendation": "B, then A. #14767's changeset asserts the repair for findReferencesToMeta but I did not measure the REST /references door end-to-end on the merged tree - that was outside this landing task's scope and I will not report an unmeasured green. A short pin through the door would convert the assertion into a measurement, after which closing the card is safe. ⛔ I deliberately did NOT change Part of to Fixes; a merge must still not auto-close #13753."
],
"out_of_scope_findings": [
"NOT filed as issues - reported here for PM, because both are tightly coupled to the #13753 / #14767 triage decision the PM already owns and filing them separately would fragment it. (1) Stale factual claim in packages/rest/src/rest-server.ts around line 4574, superseded by #14767 - see stopped_on_rather_than_guessing. (2) Incomplete call-site enumeration in packages/metadata-protocol/src/protocol.ts:6822-6826, which will be one short the moment this PR lands.",
"Shared verify-lock contention is severe on this container: a single holder held the lock 1925s (32 min) across my two queue timeouts, and three of my locked stages reported waits of 415s, 540s and 399s. Not a defect in this PR; noted because it is the dominant cost of a landing task and other seats are hitting it simultaneously (parked slots for issue-14642, issue-14758, issue-14657 were all visible)."
],
"not_done_deliberately": "Did NOT flip draft state (still draft:false as found), did NOT arm auto-merge, did NOT force-push or rebase (the push was a clean fast-forward 678c626de7..6d49f74f83), did NOT touch content/docs/releases/, did NOT add a second changeset (@objectstack/rest: patch already present and untouched), did NOT git stash, and did NOT change the Part of #13753 line."
}Generated by Claude Code Generated by Claude Code |
|
os-dev-report
{
"issue": 14677,
"status": "done",
"supersedes": "the earlier os-dev-report comment on this PR, whose JSON was malformed (the second open_questions object was missing its closing brace). Content is otherwise the same; this one parses.",
"branch": "claude/issue-13753-meta-read-doors-org-forwarding",
"pr": "https://github.com/objectstack-ai/objectstack/pull/14677",
"premise_still_valid": true,
"merge_commit": "669f8ac2f4 (merge of origin/main 2d40f91468 into the branch at 678c626de7); the regeneration is a separate commit 6d49f74f83, which is the pushed head",
"summary": "Landing task only: no behaviour added, no scope widened, nothing redesigned. Merged origin/main (66 commits) into the branch via scripts/pm/os-regen-merge.sh; its step 1 stopped on exactly one conflict and refused to continue by design, so steps 2-4 were completed by hand following that script's own runbook. mergeable_state moved dirty -> blocked. The PR's falsified /references premise, its reversed harm direction (getMetaItems UNIONs queryByOrg(null) with queryByOrg(orgId), so naming an org can only ADD rows -- the hazard is resurrecting pre-#6190 phantoms, not hiding rows) and both ablation narratives survive intact, and every number was re-measured rather than carried forward. Two dispatch expectations were corrected by measurement: (1) the conflict was NOT in packages/rest/src/rest-server.ts -- main changed no line of that file in the merge window, so it merged cleanly, and the sole conflict was the generated census page; (2) the merge brought in #14767, which is almost verbatim the proposal this PR wrote up and deliberately did not apply, landing organizationIdForMetaRead INSIDE getMetaItems.",
"conflicts": [
{
"path": "content/docs/permissions/system-context.mdx",
"resolution": "regenerated by the repo's own tooling, not hand-resolved",
"detail": "MIXED os-regen artifact (generated anchors plus hand-written prose). The os-regen driver deliberately refuses to auto-merge it and text-merges instead, which is exactly why GitHub reported dirty -- GitHub has no custom merge driver. Both sides' prose was proved byte-identical by blanking every :NNN token and diffing; the only 6 differing tokens were citation anchors. Took main's side per step 2's both-sides rule -- the hand resolution's blob b26e78e827 equals origin/main's blob for that path exactly -- then ran pnpm gen:system-context-census on the merged tree. The generator moved row 50's five rest-server.ts anchors BACK to this branch's positions (4716->4790, 6079->6204, 6327->6452, 6758->6883, 6951->7076) while rows 48/49 keep main's action-execution.ts:138/:401. Neither side's numbers were right for the merged tree, which is precisely why this file is regenerated and never hand-picked. The staged diff was inspected with git diff --cached per the script's runbook sentence, not git diff. check:system-context-census passes on the result."
}
],
"no_other_conflicts": "packages/rest/src/rest-server.ts did NOT conflict; git log 2aa8456cf2..origin/main -- packages/rest/src/rest-server.ts is empty. The branch's delta against merged main is still exactly the same 5 files, 391 insertions / 8 deletions, identical to the pre-merge diffstat.",
"fenced_files_not_reached": "Confirmed mechanically: packages/rest/src/error-response.ts (PR #14713) is absent from git diff --name-only 678c626de7..HEAD, and handleApprovalError (card #14573) has 0 occurrences anywhere in the delta. Neither was touched.",
"tests": "All heavy stages went through scripts/pm/os-verify-lock.sh, verdicts read from its VERDICT line; every exit code captured redirect-first (cmd > file 2>&1; rc=$?), never after a pipe. Two attempts returned exit 99 queue-timeout (NOT MEASURED, not a pass) behind a 32-minute holder and were retried on the same OS_VERIFY_LOCK_SLOT until acquired. ABLATIONS, both re-run on the merged tree; each leg asserts the baseline (worktree blob == HEAD blob 6ad0773ecb) then mutate -> prove on disk -> run -> restore -> prove by blob hash, all under trap restore EXIT INT TERM with absolute paths. A (drop the organization): removed-marker 0 / injected-marker 1, mutated blob f9c0c97b44, 'Tests 6 failed | 24 passed (30)' -- UNCHANGED from the predicted 6/24. B (raw tenant instead of the predicate): removed-marker 0 / injected-marker 1, mutated blob 4da1289a2f, 'Tests 30 passed (30)' -- MOVED from the predicted 1 failed / 29 passed to 0 failed / 30 passed. Each restore proved: 'restored: blob 6ad0773ecb6ecc8701b78ea3ca5d98e420de0d78 == HEAD blob, git diff HEAD empty'. Restore leg on the unmutated tree: 30 passed (30). WHY B MOVED: #14767 now applies organizationIdForMetaRead(request.type, request.organizationId) inside getMetaItems (protocol.ts:6842), so the single control B used to redden -- '?type=object stays env-wide and does NOT resurrect a phantom org row' -- is now held one layer down. B going green is NOT the control weakening: it still runs and still passes; it simply no longer discriminates THIS call site, because the behaviour is now enforced in two places. TYPECHECK: pnpm --filter @objectstack/rest typecheck exit 0, own verdict line 'check:test-typecheck: OK -- @objectstack/rest's test layer compiles under packages/rest/tsconfig.test.json; 0 file(s) / 0 error(s) / 0 pinned signature(s)'. WHOLE PACKAGE: pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 exit 0, 'Test Files 173 passed (173)', 'Tests 2896 passed (2896)' -- up from the recorded 172/2886 because the merge adds main's packages/rest/src/package-registry-item-projection.test.ts and its rewrite of meta-state-route-doc-spelling.test.ts; nothing this branch owns changed count. LINT: whole-repo pnpm lint (eslint . --no-inline-config) exit 0, no narrowing, re-run rather than carried forward. GATES: union re-derived at the new head by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (stderr confirms 'derived from the tree ... at commit 6d49f74f83' and that the --repo assertion holds); 61 commands, each with its exit code captured before any pipe: 56 pass, 5 NOT MEASURED, 0 red. The 5 NOT MEASURED are the same five as pre-merge, re-measured not copied: check-test-completeness (3, needs a saved turbo run test log), pm/check-half-states (3, repo-scoped reads refused in this container), check:dual-build-cjs-loads (3, PREREQUISITE NOT MET, needs a whole-repo build), check:type-check-debt (3, re-measure mode, a maintainer's act), and @objectstack/spec check:skill-examples (1, refusal: packages/client-react/dist holds no .d.ts). Three that refused on the first sweep for the prerequisite reason (spec check:docs, lint check:doc-formula-expressions, check:doc-security-posture) pass on re-run after the closure build and are counted in the 56. check-nul-bytes: OK, 8071 files, 0 control bytes; the edited file self-scanned separately with grep -naP, 0 hits.",
"cli_e2e_flake": "packages/cli/test/run-dev-unbuilt-workspace.e2e.test.ts -- its fix accb9231c7 is confirmed an ancestor of the merged head (git merge-base --is-ancestor accb9231c7 HEAD passes), so the merge does bring it in. NOT exercised directly: the verification scope for this landing task is @objectstack/rest, so the cli package was not run and I make NO claim either way about whether it still flakes. CI reads it.",
"mcp_calls": "7",
"stopped_on_rather_than_guessing": [
"The call-site comment block in packages/rest/src/rest-server.ts (around line 4574) asserts that getMetaItems applies NO registry gate of its own, and that the only organizationIdForMetaRead call inside metadata-protocol is the page read in protocol.ts. That is FALSE on the merged tree after #14767. I did NOT rewrite it: the correct replacement text depends on whether the door-side predicate should now be kept as defence in depth or removed as redundant, which is a design decision and not a merge resolution. Recorded in the PR body instead.",
"Correspondingly, packages/metadata-protocol/src/protocol.ts:6822-6826 (main's own comment, from #14767) enumerates 'the four remaining organizationIdForMetaRead call sites in rest-server.ts' and states they never reach getMetaItems. This branch adds one that DOES reach it, via getMetaDiagnostics. The claim is not wrong about the four it names, but the enumeration is incomplete the moment this PR lands. Also left alone: main's file, outside this PR's scope."
],
"open_questions": [
{
"question": "#14767 landed the exact proposal this PR wrote up. Should the door-side predicate added here be kept, or removed as redundant?",
"options": [
"A - keep it: idempotent over the same type (both applications fold the identical string), defence in depth if the inner gate ever moves, and ablation A measured on the merged tree that the six repair cases still go red without it",
"B - remove it and let getMetaItems' own gate carry the typed arm, shrinking this PR to its pins plus the recorded-gap documentation"
],
"recommendation": "A, keep it, and land this PR as-is. Long-term soundness leads and is not outvoted: the door-side predicate is the spelling every sibling meta read door in rest-server.ts already uses, so keeping it holds one convention across six call sites instead of making this one door the exception that reads scope differently from its neighbours; and ablation A measured, on the merged tree, that dropping the organization at the door still reddens six cases, so the call site does real work the inner gate cannot do for it. Real business need is neutral-to-positive: the ?type= arm is Studio's per-type drill-down and is repaired either way. AI-error-resistance favours A, since two independent applications of one predicate is the fail-closed direction, whereas B makes this door's correctness depend on a remote invariant its reader cannot see. Startup scope discipline is the only axis pointing at B and it points weakly, because A adds no surface, no export and no accept set. This is a recommendation about a design question the merge raised; I did not act on it."
},
{
"question": "#13753 names two doors. #14767 appears to have addressed the second (/references) from inside getMetaItems -- its changeset names findReferencesToMeta as one of the three callers it repairs. Is the card now closable once this PR lands?",
"options": [
"A - PM re-triages #13753 against the merged tree and closes it if /references and the untyped sweep are both genuinely covered",
"B - keep the card open pending a pin that measures /references end-to-end through the REST door rather than trusting the changeset's claim"
],
"recommendation": "B, then A. #14767's changeset asserts the repair for findReferencesToMeta, but I did not measure the REST /references door end-to-end on the merged tree -- that was outside this landing task's scope, and I will not report an unmeasured green. A short pin through the door converts the assertion into a measurement, after which closing the card is safe. I deliberately did NOT change 'Part of' to 'Fixes'; a merge must still not auto-close #13753."
}
],
"out_of_scope_findings": [
"NOT filed as separate issues, reported here for the PM instead, because both are tightly coupled to the #13753 / #14767 triage decision the PM already owns and separate cards would fragment it. (1) Stale factual claim in packages/rest/src/rest-server.ts around line 4574, superseded by #14767. (2) Incomplete call-site enumeration in packages/metadata-protocol/src/protocol.ts:6822-6826, which goes one short the moment this PR lands.",
"Shared verify-lock contention is severe on this container: one holder held the lock 1925s (32 min) across my two queue timeouts, and my locked stages reported waits of 415s, 540s, 399s and 133s. Not a defect in this PR; noted because it was the dominant cost of this landing task, and parked slots for issue-14642, issue-14657 and issue-14758 were visible at the same time."
],
"not_done_deliberately": "Did NOT flip draft state (unchanged, as found), did NOT arm auto-merge, did NOT force-push, rebase or amend (the push was a clean fast-forward 678c626de7..6d49f74f83), did NOT touch content/docs/releases/, did NOT add a second changeset (@objectstack/rest: patch already present and untouched), did NOT git stash, and did NOT change the 'Part of #13753' line."
}Generated by Claude Code Generated by Claude Code |
|
ACCEPT — conflict resolved, both open questions ruled. Flipped ready and armed. Merge
|
| leg | predicted | measured on the merged tree |
|---|---|---|
| A — drop the organization | 6 failed / 24 passed | 6 failed / 24 passed — unchanged |
| B — raw tenant instead of the predicate | 1 failed / 29 passed | 0 failed / 30 passed — MOVED |
The obvious reading of B is "the control weakened." ⛔ That reading is wrong and the seat did not take it. #14767 now applies the same predicate one layer down, so B's single discriminating case — "?type=object stays env-wide and does NOT resurrect a phantom org row" — is held inside getMetaItems. The control still runs and still passes; it simply no longer discriminates this call site, because the behaviour is now enforced in two places.
⇒ A moved ablation number is information about the tree, not automatically a defect in the test. Distinguishing "my control stopped working" from "the world changed underneath my control" is the whole job here, and it was done by naming the commit that changed it.
Ruling on open question 1 — A: keep the door-side predicate. Landing as-is.
This is mine to rule, not the maintainer's: no contract moves, no accept set, no export, no wire shape — it is where one already-agreed predicate is spelled.
- Long-term soundness leads. The door-side predicate is the spelling every sibling meta-read door in
rest-server.tsalready uses. Keeping it holds one convention across six call sites instead of making this one door the exception that reads scope differently from its neighbours. - It does real work, measured: ablation A on the merged tree still reds six cases without it. So "redundant" is not established — it is redundant only while the inner gate stays where fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type #14767 put it.
- Fail-closed direction. Two independent applications of one idempotent predicate (both fold the identical string) beats one application plus a remote invariant this file's reader cannot see.
- Scope discipline is the only axis pointing at B, and it points weakly: A adds no surface.
Ruling on open question 2 — B, then A. ⛔ #13753 does NOT close yet.
#14767's changeset asserts it repairs findReferencesToMeta. Nobody has measured the REST /references door end-to-end on the merged tree, and the seat declined to report an unmeasured green. ✅ Correct — and this card's own history is the argument: #13753's premise about the harm direction was backwards (the union can only ADD rows; the hazard is resurrecting pre-#6190 phantoms, not concealment), which is exactly what happens when a door's behaviour is inferred from a description rather than driven.
⇒ Card-state correction, which I am recording before it becomes a half-state. My earlier plan was: on this PR's merge, hang pm:blocked + Blocked-by: #14683 on #13753. That is now wrong — #14683 is CLOSED (completed, 04:28, by PR #14767). On merge, #13753 goes back to pm:queue, with its remaining unit being a short pin through the /references door on the merged tree, and the untyped /meta/diagnostics sweep re-checked against #14767's inner gate. ⛔ A merge must still not auto-close it — Part of, not Fixes, correctly left alone.
Two stale factual claims, correctly left alone
rest-server.ts~:4574assertsgetMetaItemsapplies no registry gate of its own — false on the merged tree after fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type #14767.protocol.ts:6822-6826(main's own comment) enumerates "the four remainingorganizationIdForMetaReadcall sites inrest-server.ts" and says they never reachgetMetaItems; this branch adds one that does, so the enumeration goes one short the moment this lands.
✅ Neither was rewritten. The first's correct replacement text depends on the design decision I just ruled above — a decision, not a merge resolution — and the second is another lane's file. Both reported instead of guessed. I will fold the first into a follow-up rather than let this PR grow a rider; the second belongs to whoever owns metadata-protocol.
Verification accepted
The census page was regenerated by the repo's own tooling, never hand-resolved — and the seat proved why it conflicted at all: both sides' prose is byte-identical (verified by blanking every :NNN token and diffing); only 6 citation anchors differed. It took main's side per the script's both-sides rule (hand-resolution blob b26e78e827 equals origin/main's blob exactly), then regenerated — and the generator moved five rest-server.ts anchors back to this branch's positions while keeping main's for two others. ⇒ Neither side's numbers were right for the merged tree, which is precisely why that file is regenerated and never hand-picked. check:system-context-census passes.
Fences confirmed mechanically, not by assertion: packages/rest/src/error-response.ts (PR #14713) absent from git diff --name-only 678c626de7..HEAD, and handleApprovalError (card #14573) has 0 occurrences in the delta. The branch's delta against merged main is still exactly 5 files, 391/8 — identical to the pre-merge diffstat.
Whole package re-run rather than carried forward: 173 files / 2896 tests (up from 172/2886 because the merge adds main's new rest tests; nothing this branch owns changed count). Whole-repo lint exit 0, unnarrowed. Gate union re-derived at the new head: 61 commands, 56 pass, 5 NOT MEASURED, 0 red — the same five as pre-merge, re-measured rather than copied, and three that refused on the first sweep pass on re-run after the closure build.
Lock contention noted, not acted on: one holder at 1925 s (32 min) across two of this seat's queue timeouts; waits of 415/540/399/133 s. exit 99 recorded as NOT MEASURED, never a pass.
Generated by Claude Code
|
Director seat — queue check-in 14 (12:44Z): this PR is Read at 12:45Z: The conflicting path is almost certainly Ask for the owning seat: re-merge Generated by Claude Code |
Part of #13753
Part of, notFixes. The card names two doors. One of them(
GET /meta/diagnostics, on its?type=arm) is repaired here. The other(
GET /meta/:type/:name/references) turned out on measurement not to be repairable fromthe call site at all, and the untyped diagnostics sweep has the same shape — so merging
this must not close the card. The measurement and a proposal are below and in the report
comment.
What the two doors do with the caller's organization
getMetaDiagnosticsandfindReferencesToMetaboth declareorganizationIdand bothforward it into
getMetaItems. Neither REST door supplied one, so both resolved atenvironment scope only. Confirmed by symbol on the merged tree, three merges newer than
the tree the card was filed against.
The measurement the card asked for, and the one thing it changes.
getMetaItemsapplies no registry gate of its own — whatever organization it is handed is used for
whatever type it is handed. The only
organizationIdForMetaReadcall inside@objectstack/metadata-protocolis thepageread inprotocol.ts; nothing on this path.So the scope of a sweep is decided per type, by the caller, and a request that carries
one
organizationIdcan only be correct when it sweeps one type.on the merged tree — see "What the merge brought in" below. It is left standing as the
record of the measurement, not silently rewritten.
naming the tenant unconditionally would hide an
allowOrgOverride: falsetype's rows.That is true of
/history(SysMetadataRepository.history()filtersorganization_idbystrict equality) and it is not true here:
getMetaItemsUNIONsqueryByOrg(null)withqueryByOrg(orgId)under org-wins precedence, so naming an organization can only addrows. What an unconditional tenant does on this path is resurrect — it reads back the
pre-#6190 phantom org-scoped rows on types with no per-org read channel, which boot
hydration walks past. That is the hazard
organizationIdForMetaRead's own docblock argues,and it is why the registry-gated predicate is still the right one here even though the
strict-equality argument does not apply.
Per door — before, after
GET /meta/diagnostics?type=view(org session, one org-authoredview)200,stats.view.count = 0,scannedItems = 0— a clean tile over a partition never read200,stats.view.count = 1,scannedItems = 1GET /meta/diagnostics?type=object(org session, one env-wideobject+ a planted phantom org row)200, count1200, count1— unchanged; the phantom is not resurrectedGET /meta/diagnostics(no?type=, org session)200,stats.view.count = 0200,stats.view.count = 0— unchanged by decision, pinned as a recorded gapGET /meta/:type/:name/references(org session)200, org-scoped referrers invisibleNo status code moves, no response field is added or removed, and an anonymous or
organization-less caller reads byte-identically to before.
Why
/referencesis reported instead of repairedThe card's fix direction rests on "
/referencestakes onetype". Measured, it does not:req.params.typeis the target, andfindReferencesToMetaspends the organization onthe sources. It resolves
REFERENCE_SITES.byTarget.get(target), groups the sites byfromType, and reads each one withgetMetaItems({ type: matcher.fromType, ...(organizationId ? { organizationId } : {}) }).So one request-level organization is applied to a set of types the target's own registry
flag says nothing about, and gating on the target answers a question about the wrong type
in both directions:
allowOrgOverride: true(view,dashboard,report,translation,email_template) ⇒ the organization is named for every source type,object/flow/appincluded — the unconditional tenant, unioning phantom rows into adestructive-action clearance;
allowOrgOverride: false(object,flow,app,page, …) ⇒ nothing is named,so an org-scoped
viewreferencing the object being deleted stays invisible and the"Used by" panel still renders "Nothing in the metadata graph points at this item. Safe
to delete." — the card's own false clearance, on the most common delete there is.
Half-repairing it would advertise an org-awareness the door cannot deliver, on the exact
surface Prime Directive #10 is about. The reasoning is recorded at the call site so the
next reader does not add the one-liner that looks obviously missing.
Proposal (for the seat that owns
metadata-protocol; not applied here — the card fencesprotocol.tsoff): applyorganizationIdForMetaRead(request.type, request.organizationId)inside
getMetaItems. It is idempotent for the six call sites inrest-server.tsthatalready gate, it makes every multi-type sweep correct with one request-level organization
(both doors here,
/referencesincluded), and it puts read scope and write scope on onepredicate so they cannot drift for any caller. The narrower alternative — the predicate
per
matcher.fromTypeinsidefindReferencesToMetaand pertinsidegetMetaDiagnostics— fixes these two doors and leaves the next sweep to rediscover it.mainas#14767— see "What the merge broughtin" below.
The door enumeration, re-run on this branch
Every
GETdoor under the meta path inpackages/rest/src/rest-server.ts, against/audit's parameter passing. Derived mechanically from the register calls, not by eye./meta,/meta/typesprotocol.getMetaTypes()/meta/diagnosticsgetMetaItemsper swept type?type=arm: yes (this PR); untyped: noorganizationIdForMetaRead(canonicalMetaUrlType(type), ctx.tenantId)/meta/_draftsorganizationId: ctx?.tenantId ?? undefined(raw)/meta/:typeorganizationIdForMetaRead(canonicalMetaUrlType(req.params.type), …)/meta/:type/:namereadOrganizationId)/meta/:type/:name/referencesgetMetaItemsper source type/meta/:type/:name/layersserveMetaItemLayered/meta/:type/:name/history/meta/:type/:name/diff/meta/:type/:name/audit$orunionctx?.tenantId ?? null(raw — a union can only add rows)/meta/:type/:name/publishedgetMetaItemLayeredctx.tenantId(raw — org-first then env-wide)/meta/book/:name/treebook,doc)allowOrgOverride: false/meta/object/:name/state/:fieldobject)objectdeclaresallowOrgOverride: false13
GETdoors, no third door with the same omission — the population the card describedholds, with one correction:
/referencesis a multi-type sweep, not a single-type read.organizationIdForMetaRead(had 5call sites in this file, not 10 (they are
serveMetaItemLayered, the list door, thesingle-item read,
/history,/diff). This PR makes it 6. Recorded because the PM'spremise check cited the 10.
Red-first, and the two ablations
The pins extend
packages/rest/src/rest-server-meta-read-org-scope.test.ts— real RESTroutes over a real
ObjectStackProtocolImplementationover a partitioned stub engine, oneboot, write-then-read agreement. It already carries this gate family's pinned engine
double, so no new double and no ledger row.
Every read assertion is gated on a fixture proof that the org-scoped row exists and
that nothing landed env-wide; the phantom control plants its row directly, because the
write door cannot produce one (that is #6190).
Both legs below ran on the committed tree (baseline asserted: the file's blob equals
HEAD's before anything is measured), the mutation was proved to have landed on disk bycounting the removed and the injected marker, and each restore is proved by
git diff HEADbeing empty plus a
git hash-objectmatch — not by an exit code. No rebuild is owed foreither leg: the pin imports the mutated module relatively (
./rest-server.js), sovitest transforms this source directly and no
distsits between the edit and the run.organizationIdForMetaRead(canonicalMetaUrlType(diagnosticsType), undefined)?type=repair cases red, every control green?type=object stays env-wide and does NOT resurrect a phantom org row, and nothing else0 failed / 30 passed. The re-take and the reason are in "What the merge brought in".Verification
Run on the merged tree at
6d49f74f83(this branch's head).origin/mainat2d40f91468was merged in — 66 commits — viascripts/pm/os-regen-merge.sh, whose step 1stopped on one conflict; the generated census page was then regenerated from the merged
tree by the repo's own generator rather than hand-resolved. All heavy stages went through
the shared verify lock (
scripts/pm/os-verify-lock.sh); every exit code was capturedbefore any pipe, and every verdict below is the gate's own printed line.
pnpm --filter '@objectstack/rest^...' buildpnpm --filter @objectstack/rest typecheckcheck:test-typecheck: "@objectstack/rest's test layer compiles under packages/rest/tsconfig.test.json; 0 file(s) / 0 error(s) / 0 pinned signature(s)", so the pins really were type-checkedpnpm --filter @objectstack/rest exec vitest run --maxWorkers=2(whole package)Test Files 173 passed (173),Tests 2896 passed (2896)pnpm lint(eslint . --no-inline-config)node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsThe package suite moved from
172 files / 2886 teststo173 / 2896: the merge brings inpackages/rest/src/package-registry-item-projection.test.ts(new on main) and main'srewrite of
meta-state-route-doc-spelling.test.ts. Nothing this branch owns changed count.The five NOT MEASURED, in each gate's own words — none is a pass and none is a red. It
is the same five as the pre-merge run, re-measured rather than carried forward:
check-test-completeness(exit 3) — "PREREQUISITE NOT MET — this gate grades a savedturbo run testlog, and no log was named … the local reading for this gate is NOTMEASURED".
pm/check-half-states(exit 3) — "PREREQUISITE NOT MET — the transport authenticatesbut repo-scoped reads are refused"; this container cannot make a repo-scoped request.
check:dual-build-cjs-loads(exit 3) — "PREREQUISITE NOT MET — this gate reads builtoutput, and some package has no dist/." It needs a whole-repo build; this branch built
the
@objectstack/restclosure.check:type-check-debt(exit 3) — re-measure mode, "PREREQUISITE NOT MET"; raising aledger entry is a maintainer's act.
@objectstack/spec check:skill-examples(exit 1, refusal not finding) —"packages/client-react/dist holds no .d.ts declarations — the package is not built".
Same prerequisite class, one package outside the closure; its subject is untouched here.
Three gates that refused on the first sweep for the prerequisite reason —
@objectstack/spec check:docs,@objectstack/lint check:doc-formula-expressionsandcheck:doc-security-posture— pass on re-run after the closure build, and are counted inthe 56.
What the merge brought in, and the one number it moved
The merge window is 66 commits. One of them changes what this PR's own text says about the
world, so it is called out rather than left for a reader to notice.
#14767landed the proposal this PR made.getMetaItemsnow appliesorganizationIdForMetaRead(request.type, request.organizationId)itself, after thecanonical type fold (
packages/metadata-protocol/src/protocol.ts:6842). That is, almostverbatim, the "Proposal (for the seat that owns
metadata-protocol; not applied here)"section above. Two consequences, both measured:
the record of what was true when the work was done. The measurement "
getMetaItemsapplies no registry gate of its own" — and the call-site comment in
rest-server.tsthat repeats it — described the tree this branch was written against.On the merged tree the gate exists one layer down. The repair here is unaffected and
still correct:
organizationIdForMetaReadanswers either its argument orundefined,so a second application over the same type is a no-op, and both applications fold
the identical string. ⛔ Nothing was rewritten to match, because deciding whether the
door-side predicate should now be kept as defence in depth or removed as redundant is a
design question, not a merge resolution.
/referencesmay no longer need the call site at all. This PR reported that door asnot repairable call-side because one request-level organization is spent on a set of
source types.
#14767fixes exactly that class from insidegetMetaItems, and its ownchangeset names
findReferencesToMetaas one of the three callers it repairs. Thesecond door named by rest/meta: two more read doors (
/meta/diagnostics,/meta/:type/:name/references) never forward the caller's organization — the "Used by" graph tells an operator an org-referenced item is safe to delete #13753 therefore looks addressed onmain. This PR still saysPart of, notFixes— whether the card is now closable is the PM's call, not a mergeresolution's.
The one number that moved is ablation B, and it moved for that reason. Both ablations
were re-run on the merged tree; A is unchanged, B no longer discriminates:
?type=repair cases stay red and every control stays green?type=objectstays env-wide and does NOT resurrect a phantom org row, is now held bygetMetaItems' own gate, so the door-side predicate is no longer the only thing producing that behaviourwhat changed is that it no longer distinguishes this call site, because the behaviour it
pins is now enforced in two places. The door-side predicate remains the correct expression
and would still hold if the inner gate were removed — but this branch can no longer claim
the phantom-row control as evidence for the call site specifically. Recorded here rather
than quietly restated.
Restore leg after both:
30 passed (30), blob back to6ad0773ecb6ecc8701b78ea3ca5d98e420de0d78— the same blob the pre-merge run restored to.Each mutation was proved to have landed on disk by counting the removed and the injected
marker (and by its distinct blob hash); each restore is proved by
git hash-objectequality against the
HEADblob plus an emptygit diff HEAD, never by an exit code, withan absolute-path
trap … EXIT INT TERMaround both legs.Merge mechanics, for the record. One conflicted path:
content/docs/permissions/system-context.mdx, a MIXED os-regen artifact (generated anchorsplus hand-written prose), which the
os-regendriver deliberately refuses to auto-merge.Both sides' prose was byte-identical — the six differing tokens were all
:NNNcitationanchors — so main's side was taken per step 2's both-sides rule and
pnpm gen:system-context-censusthen re-derived the anchors from the merged tree. Thatregeneration moved row 50's five
rest-server.tscitations back to this branch's positionswhile rows 48/49 keep main's
action-execution.tspositions: neither side's numbers wereright for the merged tree, which is why this file is regenerated and never hand-resolved.
check:system-context-censuspasses on the result.packages/rest/src/rest-server.tsdidnot conflict — main changed no line of it in the window — and this branch's delta
against merged
mainis still exactly the same five files, 391 insertions / 8 deletions.Changeset level
@objectstack/rest: patch. A restoration, not a feature: the protocol already declared andread
organizationId; the door now supplies it on the arm where one organization is thewhole truth. No accept set moves, no wire shape moves, nothing that resolved today stops
resolving, and the direction is fail-closed (the caller's own organization was
under-served; there is no cross-org read either way, and the controls pin that).
Deviations from the dispatch
Part of #13753, notFixes— one of the two doors is out of call-side reach, so amerge must not close the card.
/referencesis reported, not repaired — the dispatch's premise for it ("takes onetype") is falsified above; applying the dispatch's own rule to the measured facts putsit in the same class as the untyped sweep: stop and report with a proposal.
its ledger-pinned engine double instead of adding one to
scripts/engine-double-contract.pinned.json.content/docs/permissions/system-context.mdxis in the diff:check-system-context-census.mjsre-anchored fivectx.isSystemcitations whose linenumbers moved. Gate-mandated regeneration, its own commit.
🤖 Generated with Claude Code
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza