Skip to content

fix(rest): the untyped /meta/diagnostics sweep forwards the caller's organization, so a governance summary stops undercounting its own drill-down (#15622) - #15726

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-15622-diagnostics-sweep-org-forwarding
Sep 5, 2026
Merged

fix(rest): the untyped /meta/diagnostics sweep forwards the caller's organization, so a governance summary stops undercounting its own drill-down (#15622)#15726
hotlong merged 2 commits into
mainfrom
claude/issue-15622-diagnostics-sweep-org-forwarding

Conversation

@hotlong

@hotlong hotlong commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15622

GET /api/v1/meta/diagnostics has two arms. The ?type= arm has stated the caller's organization since #13753; the untyped whole-registry sweep passed nothing. So the Studio governance summary reported clean tiles over a partition it never read, and undercounted relative to the per-type drill-down screen you reach by clicking into it. A summary whose whole job is surfacing problems, and which structurally cannot see a class of them while its own drill-down can, issues a false all-clear.

Ruled on the card (comment 5549255080): the untyped arm forwards the caller's organization, raw.

The four premises, re-verified on origin/main by this seat before anything was written

All four hold, measured at merge base 1c1421401:

  1. getMetaDiagnostics (packages/metadata-protocol/src/protocol.ts:6293) loops for (const t of targetTypes) and calls getMetaItems({ type: t, organizationId: request.organizationId, packageId: request.packageId }). The organization is spent per type, not once.
  2. getMetaItems computes organizationIdForMetaRead(request.type, request.organizationId) at protocol.ts:6958, on its own request.type, one statement after request = canonicalizeMetaRequestType(request) — i.e. after the canonical fold.
  3. organizationIdForMetaRead(type, org) (packages/metadata-core/src/meta-write-org-scope.ts:184) returns undefined when activeOrganizationId is undefined, and otherwise declaresOrgOverride(type) ? activeOrganizationId : undefined. It returns the org only for an overridable type.
  4. The REST arm computed diagnosticsOrganizationId only inside if (diagnosticsType) — the untyped sweep passed nothing at all.

⇒ One organizationId handed to the untyped arm is already narrowed per type by the callee. The obstacle written in the tree ("one org id cannot express a per-type scope") is gone.

The change

packages/rest/src/rest-server.ts — the untyped arm resolves the memoised exec ctx and passes organizationId: ctx?.tenantId RAW.

⛔ Deliberately not pre-folded with organizationIdForMetaRead(...) the way the ?type= arm folds: there is no single type to fold on for a whole-registry sweep, and folding on any one of them would suppress the organization for every type at once. The per-type decision belongs to the callee's loop. Identical in shape to the /references repair that landed as e13ede817 (#13753 / PR #15689), including its narrowness control, which was read first and used as the model.

The resolveExecCtx resolution is hoisted, not duplicated: still 77 sites / 98 mentions, still the same single locally-caught continuation-line spelling execctx-consumer-census reads. That census file is unmodified and green.

⛔ Untouched, as scoped: the ?type= arm; protocol.ts; any new parameter, spec key, response field, status code or contract surface. ADR-0131 D6/D7 retires this partition in v18 (#15206, C5), so nothing new is built on it — the door's comment says so in place.

The pin was REPLACED, not deleted

an org-scoped item is absent from the whole-registry sweep carried an explicit "if this reddens, read the card before making it green" note. #15622 is that card, so its assertion is inverted in place, with the comment still naming the card and explaining the flip.

Beside it stands the phantom control the card named as missing — there was none on the untyped arm (the pre-existing one covers only ?type=object). In one request: a planted pre-#6190 org-scoped row on the non-overridable object type does not appear, while the overridable view's org-authored row does. Neither assertion states that fact alone.

Controls also pin that org B is not served org A's items, that an organization-less caller reads exactly what it read before, that an env-wide item stays visible to an org caller, and that the wire shape is unchanged (same five response keys, same stats row keys — count / locked / packages — and 200 either way).

Ablations — both legs proven on disk, never from an editor's exit code

M1 — can the new pins go red? Restored only rest-server.ts to base and left everything else in place.

  • mutate leg: on-disk git hash-object moved 1f1ee559ad595fa6524eddf78310aa2b392afc3b (HEAD blob) → 2ad2be6257cb005e0d48b58ed761c61fc5396019 (base blob), and the marker string [#15622] The whole-registry arm went from 1 occurrence to 0.
  • result: 2 failed / 39 passed, and exactly the two intended cases — ⭐ THE CARD: an org-authored item on an overridable type IS counted (expected +0 to be 1) and the narrowness control's org-visibility half (the overridable type lost its org scope on the same request — the gate is not per type). The phantom-exclusion half stayed green, correctly: the base door names no organization at all.
  • restore leg: on-disk hash back to 1f1ee559ad..., marker back to 1, git diff HEAD for that path empty. The restore used git checkout HEAD -- "$ABS_PATH" with an absolute path resolved from git rev-parse --show-toplevel, under a trap ... EXIT INT TERM.

M2 — does the narrowness control have teeth against an unconditional tenant? M1 cannot answer that, because the door cannot bypass the callee. So the callee's gate was ablated instead: protocol.ts:6958 organizationIdForMetaRead(request.type, request.organizationId) replaced by the raw request.organizationId.

⚠️ Resolution caveat honoured: packages/rest's vitest config does not alias @objectstack/metadata-protocol, so it resolves through dist/. Every leg rebuilt the package and confirmed the artifact before any result was read.

  • The first attempt of M2 was VOID and is reported as such. Its marker sat inside a /* … */ comment, which tsup strips, and scripts/ablation-dist-preflight.mjs refused it: "marker found ONLY in 2 sourcemap files and in no executable output … Treat this run as void." It was re-run with a marker that survives into emitted code.
  • mutate leg (valid run): on-disk blob moved off the HEAD blob, marker present exactly once in source, then after pnpm --filter @objectstack/metadata-protocol build the preflight reported "marker present in 2 built files" (dist/index.js, dist/index.cjs).
  • result: 2 failed / 39 passed — the untyped narrowness control (expected 2 to be 1: the phantom was resurrected into stats.object.count) and the sibling /references narrowness control. ⭐ The pre-existing ?type=object phantom control did not redden, because the typed arm pre-folds at the door and the callee never receives an org. That is precisely why the new untyped control is the one with teeth on the callee's gate.
  • restore leg: source blob back to the HEAD blob, marker 0 occurrences, git diff HEAD empty, rebuilt, and ablation-dist-preflight.mjs --absent reported "marker absent from all 24 built files" plus "working tree clean against HEAD".

Whole-tree git status --porcelain was empty after both ablations, and the restored pin file re-ran 41 passed (41).

Verification

Union re-run at final commit 7ce299574, after the last commit, not before it.

  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — let it derive its own change set (3 paths, then 4 once the docs re-anchor landed). 84 families, 84 green, 0 red, 0 NOT MEASURED. Exit codes captured by redirecting first (cmd > file 2>&1; EXIT=$?), never read after a pipe.
  • Two of them were initially PREREQUISITE NOT MET (exit 3 — NOT MEASURED, not red): check:dual-build-cjs-loads and check:type-check-debt both need a built workspace. The closure was built (turbo run build --concurrency=2 --filter='./packages/*' --filter='./packages/*/*', 71/71 successful) and both then measured green: dual-build 103/66/619/1 against floors 90/58/520/1; type-check debt 12 ledger entries re-measured, 140 raw errors, none above its recorded number.
  • check:check-system-context-census reported real line rot (the door's decision record adds +32 lines). Repaired with its own --fix, which rewrote five rest-server.ts anchors on the system-context page; row content unchanged. Re-run: OK — 106 elevation read sites … 140 anchors resolve.
  • pnpm --filter @objectstack/rest test180 files / 3086 tests passed.
  • pnpm --filter @objectstack/rest typecheck — exit 0. ⚠️ Reported as NOT MEASURED for the test file: this package's tsconfig excludes **/*.test.ts, and tsc --noEmit --listFiles confirms it — rest-server.ts appears 1 time in the program, rest-server-meta-read-org-scope.test.ts 0 times. The test layer is covered instead by pnpm --filter @objectstack/rest run check:test-typecheck, run separately: OK, 0 files / 0 errors held in debt.
  • Neighbouring packages that name this route or its protocol method: @objectstack/metadata-protocol (163 files / 2387 tests), @objectstack/spec (468 files / 12672 tests), @objectstack/client (33 files / 437 tests) — all green.
  • pnpm lint — the repo-wide eslint . --no-inline-config, run in full rather than narrowed. Exit 0 in 25s, so no narrowing needs declaring.
  • grep -naP control-character self-scan over both edited source files: no matches.

⚠️ Declared, as the wrapper requires: every heavy command went through scripts/pm/os-verify-lock.sh, and every one of its VERDICT lines said UNLOCKED (declared) · no usable flock on this host, so the shared verify lock was NEVER taken and NOTHING was serialized. Wall-clock figures above are therefore shared-box readings, not quiet-machine promises.


Generated by Claude Code

hotlong and others added 2 commits September 5, 2026 12:19
…organization, so a governance summary stops undercounting its own drill-down (#15622)

`GET /api/v1/meta/diagnostics` has two arms. The `?type=` arm has stated the
organization since #13753; the untyped whole-registry sweep passed nothing, so
the Studio governance summary reported clean tiles over a partition it never
read while the per-type screen you reach by clicking into it could see the same
items. A problem-reporting surface that structurally cannot see a class of
problems issues a false all-clear.

The obstacle recorded in the tree was that one `organizationId` could not
express a per-type scope from this door without a fan-out per overridable type
and a REST-side re-aggregation of `total`/`stats`/`scannedTypes`. #14683
dissolved it and #15034 recorded that: `getMetaDiagnostics` loops
`for (const t of targetTypes)` calling `getMetaItems({ type: t, organizationId,
... })`, and the first thing `getMetaItems` does with that organization is
`organizationIdForMetaRead(request.type, ...)` on its OWN folded type. One org
id is therefore already narrowed per type by the callee.

The door now resolves the memoised exec ctx for BOTH arms and passes
`ctx?.tenantId` RAW on the untyped one. Deliberately NOT pre-folded with
`organizationIdForMetaRead(...)` the way the `?type=` arm folds: there is no
single type to fold on, and folding on any one of them would suppress the
organization for every type at once. Identical in shape to the `/references`
repair (e13ede8, #13753).

The pin `an org-scoped item is absent from the whole-registry sweep` carried an
explicit "if this reddens, read the card before making it green" note; #15622 is
that card, so the assertion is REPLACED by its inverse rather than deleted, with
the comment still pointing at the card. Beside it stands the narrowness control
#15622 named as missing: in ONE request a planted pre-#6190 org-scoped row on a
non-overridable type does NOT appear while an overridable type's org-authored
row DOES — the half that tells a per-type gate from an unconditional tenant.
Controls pin that org B is not served org A's items, that an organization-less
caller reads exactly what it read before, that an env-wide item stays visible to
an org caller, and that the wire shape is unchanged (same five response keys,
same `stats` row keys, 200 either way).

`resolveExecCtx` census numbers are unmoved: still 77 sites / 98 mentions, the
hoisted resolution being the same single locally-caught continuation-line site.

No new parameter, response field, status code or contract surface: ADR-0131
D6/D7 retires this partition in v18 (#15206, C5), so nothing is built on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s comment shifted

`check:check-system-context-census --fix` output: the five `rest-server.ts`
elevation-read anchors on the system-context page moved by the same +32 lines
the untyped arm's decision record added. Pure line rot, no row content changed.

Part of #15622

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts) — pages documenting those are invisible to this run
  • 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 — 13 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 a5cef37695130da47b6e265647ff329eb00e61f9packageMentionDocs.

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant