Skip to content

rest/meta: state the caller's organization on the /meta/diagnostics ?type= sweep - #14677

Queued
os-trump wants to merge 10 commits into
mainfrom
claude/issue-13753-meta-read-doors-org-forwarding
Queued

rest/meta: state the caller's organization on the /meta/diagnostics ?type= sweep#14677
os-trump wants to merge 10 commits into
mainfrom
claude/issue-13753-meta-read-doors-org-forwarding

Conversation

@os-trump

@os-trump os-trump commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Part of #13753

⚠️ Deliberately Part of, not Fixes. 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 from
the 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

getMetaDiagnostics and findReferencesToMeta both declare organizationId and both
forward it into getMetaItems. Neither REST door supplied one, so both resolved at
environment 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. getMetaItems
applies no registry gate of its own — whatever organization it is handed is used for
whatever type it is handed. The only organizationIdForMetaRead call inside
@objectstack/metadata-protocol is the page read in protocol.ts; nothing on this path.
So the scope of a sweep is decided per type, by the caller, and a request that carries
one organizationId can only be correct when it sweeps one type.

⚠️ This paragraph was true of the tree this branch was written against and is superseded
on the merged tree
— see "What the merge brought in" below. It is left standing as the
record of the measurement, not silently rewritten.

⚠️ And the direction of the harm is not what the card and the dispatch assumed. Both said
naming the tenant unconditionally would hide an allowOrgOverride: false type's rows.
That is true of /history (SysMetadataRepository.history() filters organization_id by
strict equality) and it is not true here: getMetaItems UNIONs queryByOrg(null) with
queryByOrg(orgId) under org-wins precedence, so naming an organization can only add
rows. 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

Door Before After Env rows
GET /meta/diagnostics?type=view (org session, one org-authored view) 200, stats.view.count = 0, scannedItems = 0 — a clean tile over a partition never read 200, stats.view.count = 1, scannedItems = 1 unchanged — an org session still sees env-wide items (pinned)
GET /meta/diagnostics?type=object (org session, one env-wide object + a planted phantom org row) 200, count 1 200, count 1 — unchanged; the phantom is not resurrected unchanged
GET /meta/diagnostics (no ?type=, org session) 200, stats.view.count = 0 200, stats.view.count = 0unchanged by decision, pinned as a recorded gap unchanged
GET /meta/:type/:name/references (org session) 200, org-scoped referrers invisible unchanged — see below unchanged

No status code moves, no response field is added or removed, and an anonymous or
organization-less caller reads byte-identically to before.

Why /references is reported instead of repaired

The card's fix direction rests on "/references takes one type". Measured, it does not:
req.params.type is the target, and findReferencesToMeta spends the organization on
the sources. It resolves REFERENCE_SITES.byTarget.get(target), groups the sites by
fromType, and reads each one with
getMetaItems({ 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:

  • target allowOrgOverride: true (view, dashboard, report, translation,
    email_template) ⇒ the organization is named for every source type, object /
    flow / app included — the unconditional tenant, unioning phantom rows into a
    destructive-action clearance;
  • target allowOrgOverride: false (object, flow, app, page, …) ⇒ nothing is named,
    so an org-scoped view referencing 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 fences
protocol.ts off):
apply organizationIdForMetaRead(request.type, request.organizationId)
inside getMetaItems. It is idempotent for the six call sites in rest-server.ts that
already gate, it makes every multi-type sweep correct with one request-level organization
(both doors here, /references included), and it puts read scope and write scope on one
predicate so they cannot drift for any caller. The narrower alternative — the predicate
per matcher.fromType inside findReferencesToMeta and per t inside
getMetaDiagnostics — fixes these two doors and leaves the next sweep to rediscover it.

⚠️ This proposal has since landed on main as #14767 — see "What the merge brought
in" below.

The door enumeration, re-run on this branch

Every GET door under the meta path in packages/rest/src/rest-server.ts, against
/audit's parameter passing. Derived mechanically from the register calls, not by eye.

Door Reads a metadata document Organization stated Expression
/meta, /meta/types no — protocol.getMetaTypes() n/a
/meta/diagnostics yes, getMetaItems per swept type ?type= arm: yes (this PR); untyped: no organizationIdForMetaRead(canonicalMetaUrlType(type), ctx.tenantId)
/meta/_drafts yes yes organizationId: ctx?.tenantId ?? undefined (raw)
/meta/:type yes yes organizationIdForMetaRead(canonicalMetaUrlType(req.params.type), …)
/meta/:type/:name yes yes same predicate (readOrganizationId)
/meta/:type/:name/references yes, getMetaItems per source type no reported, not repairable call-side
/meta/:type/:name/layers yes yes, via serveMetaItemLayered same predicate
/meta/:type/:name/history yes, strict equality yes same predicate
/meta/:type/:name/diff yes yes same predicate
/meta/:type/:name/audit yes, $or union yes ctx?.tenantId ?? null (raw — a union can only add rows)
/meta/:type/:name/published yes, getMetaItemLayered yes ctx.tenantId (raw — org-first then env-wide)
/meta/book/:name/tree yes (book, doc) no correct: both declare allowOrgOverride: false
/meta/object/:name/state/:field yes (object) no correct: object declares allowOrgOverride: false

13 GET doors, no third door with the same omission — the population the card described
holds, with one correction: /references is a multi-type sweep, not a single-type read.

⚠️ A count in the claim comment does not hold: organizationIdForMetaRead( had 5
call sites in this file, not 10 (they are serveMetaItemLayered, the list door, the
single-item read, /history, /diff). This PR makes it 6. Recorded because the PM's
premise 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 REST
routes over a real ObjectStackProtocolImplementation over a partitioned stub engine, one
boot, 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 by
counting the removed and the injected marker, and each restore is proved by git diff HEAD
being empty plus a git hash-object match — not by an exit code. No rebuild is owed for
either leg: the pin imports the mutated module relatively (./rest-server.js), so
vitest transforms this source directly and no dist sits between the edit and the run.

Ablation Mutation Predicted Measured (pre-merge — B is superseded below)
A — drop the organization organizationIdForMetaRead(canonicalMetaUrlType(diagnosticsType), undefined) the six ?type= repair cases red, every control green 6 failed / 24 passed — the five org-overridable types plus the plural-fold case, and nothing else
B — raw tenant instead of the predicate the predicate replaced by an identity that returns the tenant only the phantom-row control red 1 failed / 29 passed?type=object stays env-wide and does NOT resurrect a phantom org row, and nothing else

⚠️ Both ablations have been re-run on each merged tree since. A is unchanged at every
reading; B moved to 0 failed / 30 passed at the first sync and stays there. The re-takes
and the reason are in the two merge sections below.

Verification

Run on the merged tree at 6fbc0f3787 (this branch's head), after the second sync
to main — every number below was taken at that head, after the final commit. origin/main
at 5bc2f2727a was merged in — 65 commits — via scripts/pm/os-regen-merge.sh, whose
step 1 again stopped on the one MIXED conflict; the generated census page was 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 captured before any pipe, and every verdict below is the gate's own printed line.
⛔ Nothing was carried forward from the previous round — each number was re-taken.

Stage Result
pnpm --filter '@objectstack/rest^...' build VERDICT command-exit 0
pnpm --filter @objectstack/rest typecheck VERDICT command-exit 0 — including 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)", so the pins really were type-checked
pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 (whole package) VERDICT command-exit 0Test Files 175 passed (175), Tests 2946 passed (2946)
whole-repo pnpm lint (eslint . --no-inline-config) VERDICT command-exit 0 — no narrowing
gate union, re-derived at this head by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands 66 commands: 61 pass, 5 NOT MEASURED, 0 red

The gate union grew from 61 commands to 66 with the tree; it was re-derived, never reused.

The package suite moved from 173 files / 2896 tests to 175 / 2946: the merge brings in
packages/rest/src/error-response-structured-arm-door-parity.test.ts and
packages/rest/src/package-door-producer-key-carry.test.ts, both new on main. 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 both previous runs, re-measured rather than carried forward:

  • check-test-completeness (exit 3) — "Nothing was measured: this gate exited before
    parsing a single summary line … ⛔ It is NOT a finding"
    ; it grades a saved
    turbo run test log and none was named.
  • pm/check-half-states (exit 3) — an unread instrument: this container cannot make a
    repo-scoped GitHub request (measured this round — the whole repo-scoped REST class
    answers 403 for this session).
  • check:dual-build-cjs-loads (exit 3) — "PREREQUISITE NOT MET — this gate reads built
    output, and some package has no dist/. … ⛔ This is NOT a pass: nothing was measured."

    It needs a whole-repo build; this branch built the @objectstack/rest closure.
  • check:type-check-debt (exit 3) — re-measure mode, "--re-measure cannot run: 8 workspace
    dependenc(ies) … have no built type entry point on disk"
    ; raising a ledger entry is a
    maintainer's act.
  • @objectstack/spec check:skill-examples (exit 1, refusal not finding) — "A verdict now
    would be computed against a build that no longer matches src … a FALSE GREEN"
    ; it wants
    packages/client-react built, one package outside the closure. Its subject is untouched
    here.

Unlike the previous round, @objectstack/spec check:docs,
@objectstack/lint check:doc-formula-expressions and check:doc-security-posture passed
on the first sweep, because the closure build ran before the gate union rather than
after it. They are counted in the 61.

What the merge brought in, and the one number it moved

The first sync's window was 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.

#14767 landed the proposal this PR made. getMetaItems now applies
organizationIdForMetaRead(request.type, request.organizationId) itself, after the
canonical type fold. That is, almost verbatim, the "Proposal (for the seat that owns
metadata-protocol; not applied here)" section above. Two consequences, both measured:

  1. ⚠️ Two sentences in this body are now superseded, and are deliberately left standing as
    the record of what was true when the work was done.
    The measurement "getMetaItems
    applies no registry gate of its own"
    — and the call-site comment in
    rest-server.ts that 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: organizationIdForMetaRead answers either its argument or undefined,
    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.

  2. /references may no longer need the call site at all. This PR reported that door as
    not repairable call-side because one request-level organization is spent on a set of
    source types. #14767 fixes exactly that class from inside getMetaItems, and its own
    changeset names findReferencesToMeta as one of the three callers it repairs. The
    second 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 on main. This PR still says
    Part of, not Fixes — whether the card is now closable is the PM's call, not a merge
    resolution's.

The one number that moved is ablation B, and it moved for that reason. A is unchanged;
B no longer discriminates:

Ablation Predicted (pre-merge) Measured after the first sync
A — drop the organization 6 failed / 24 passed 6 failed / 24 passed — unchanged. Passing no organization at all cannot be repaired one layer down, so the six ?type= repair cases stay red and every control stays green
B — raw tenant instead of the predicate 1 failed / 29 passed 0 failed / 30 passed — moved. The single control it used to redden, ?type=object stays env-wide and does NOT resurrect a phantom org row, is now held by getMetaItems' own gate, so the door-side predicate is no longer the only thing producing that behaviour

⚠️ B going green is not the control weakening. The control still runs and still passes;
what 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.

The second sync to main, and what it changed

main moved 65 further commits (f116b8f8d1 to 5bc2f2727a) and GitHub reported the PR
dirty. Merged, never rebased: merge commit 74e75445dc, regeneration commit 6fbc0f3787.

One conflicted path, the same one as last time
content/docs/permissions/system-context.mdx, the MIXED os-regen artifact (generated
anchors plus hand-written prose) the driver deliberately refuses to auto-merge. It was
one hunk, row 50, and the two sides' prose was byte-identical: normalising every
:NNN token to a placeholder made the two lines diff-clean, so nothing hand-written was
at stake. Main's side was taken per step 2's both-sides rule and
pnpm gen:system-context-census re-derived the anchors from the merged tree — which moved
row 50's five rest-server.ts citations to 4789, 6203, 6451, 6882, 7075,
neither side's numbers: main had 4715… and this branch had 4790…. That is the
standing reason this file is regenerated and never hand-resolved. Its verdict:
"check-system-context-census: OK — 106 elevation read sites in 20 packages across 45
files, all anchored; 140 anchors resolve, 27 declared non-read."
The staged diff was
inspected with git diff --cached per the driver's runbook sentence: one line, five
anchors, nothing else.

packages/rest/src/rest-server.ts auto-merged with no conflict, but — unlike the first
sync — main did change it this window, in two commits: #14868 (retiring ten inert
RestServerConfig keys under ADR-0049) and #14801 (deleting inert eslint-disable family
comments). Both intents stack; the branch's delta against merged main is still exactly
the same five files, 391 insertions / 8 deletions it was before, and ablation A still
reddens exactly the six repair cases and nothing else, which is the evidence that the
repair survived the merge intact rather than merely re-merging cleanly.

A second inner gate arrived. #14908 ("gate getMetaItem's overlay read on the
metadata registry"
) added a second
organizationIdForMetaRead(request.type, request.organizationId) site in protocol.ts,
alongside #14767's. It extends the same one-layer-down direction to the singular
getMetaItem; it does not change anything this PR does, and the design question recorded
in point 1 above — defence in depth or redundant — now spans two inner sites rather than
one. Still not resolved here, for the same reason.

Both ablations re-run at 6fbc0f3787, on the committed tree, each mutation proved on
disk by marker counts plus a distinct blob hash and each restore proved by git hash-object
equality against the HEAD blob plus an empty git diff HEAD, under an absolute-path
trap … EXIT INT TERM:

Ablation After the first sync At 6fbc0f3787 Reading
A — drop the organization 6 failed / 24 passed 6 failed / 24 passed unchanged, and the same six named cases (the five org-overridable types plus the plural-fold case)
B — raw tenant instead of the predicate 0 failed / 30 passed 0 failed / 30 passed unchanged — still non-discriminating for the reason recorded above, not newly so

Baseline and restore legs both read 30 passed (30). The restore blob is
08cdde8c779ca487721fd69ad4d59d18e69e794b — a different blob from the first sync's
6ad0773ecb…, and correctly so: main edited rest-server.ts this window, so HEAD's
blob for that file is new. The restore is proved against this head's blob, not against a
remembered one.

Changeset level

@objectstack/rest: patch. A restoration, not a feature: the protocol already declared and
read organizationId; the door now supplies it on the arm where one organization is the
whole 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

  1. Part of #13753, not Fixes — one of the two doors is out of call-side reach, so a
    merge must not close the card.
  2. /references is reported, not repaired — the dispatch's premise for it ("takes one
    type") is falsified above; applying the dispatch's own rule to the measured facts puts
    it in the same class as the untyped sweep: stop and report with a proposal.
  3. The pins live in the existing org-scope harness rather than in a new file, to reuse
    its ledger-pinned engine double instead of adding one to
    scripts/engine-double-contract.pinned.json.
  4. content/docs/permissions/system-context.mdx is in the diff:
    check-system-context-census.mjs re-anchored five ctx.isSystem citations whose line
    numbers moved. Gate-mandated regeneration, its own commit.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

`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
@github-actions github-actions Bot added the size/m label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 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 5bc2f2727aef5d697530c63d64dd5592df445776packageMentionDocs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@os-trump
os-trump marked this pull request as ready for review September 2, 2026 18:39
@os-trump
os-trump enabled auto-merge September 2, 2026 18:39
@os-trump
os-trump added this pull request to the merge queue Sep 2, 2026
Any commits made after this event will not be merged.
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 2, 2026
…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-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

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-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

os-dev-report

⚠️ This supersedes the previous os-dev-report comment on this PR: that one's JSON was malformed (the second open_questions object was missing its closing brace), and the PM parses this block mechanically. The content is otherwise unchanged. This payload was validated with JSON.parse before posting.

{
  "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

@os-trump
os-trump enabled auto-merge September 3, 2026 05:47

os-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

ACCEPT — conflict resolved, both open questions ruled. Flipped ready and armed.

Merge 669f8ac2f4 (66 commits of main), regeneration 6d49f74f83 as the pushed head, clean fast-forward — ⛔ no rebase, amend or force-push. mergeable_state dirty → blocked.

⚠️ Two of my dispatch's predictions were wrong, and measurement said so

  1. I predicted the conflict would be in packages/rest/src/rest-server.ts ("it is a hot file"). It was not: git log 2aa8456cf2..origin/main -- packages/rest/src/rest-server.ts is emptymain changed no line of it in the merge window, so it merged cleanly. The sole conflict was the generated census page.
  2. I did not anticipate that the merge would bring in fix(metadata-protocol): apply the allowOrgOverride read gate inside getMetaItems, so multi-type sweeps are scoped per type #14767 — which is almost verbatim the proposal this PR wrote up and deliberately did not apply, landing organizationIdForMetaRead inside getMetaItems (protocol.ts:6842). Card Where does the allowOrgOverride read gate belong for metadata sweeps that read MORE THAN ONE type per request? getMetaItems applies none of its own #14683, the split-out this PR filed for exactly that question, was graded and closed by the engine seat at 04:28 with option A.

⇒ Both corrections came from the seat re-measuring rather than executing my expectations. That is the behaviour I want; a landing task that had merely obeyed the dispatch would have gone looking for a conflict that did not exist.

⭐ The finding of this round: ablation B moved, and the seat read it correctly

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.ts already 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

✅ 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

@os-project-manager
os-project-manager added this pull request to the merge queue Sep 3, 2026
Any commits made after this event will not be merged.
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Director seat — queue check-in 14 (12:44Z): this PR is dirty against main again, so auto-merge cannot enqueue it

Read at 12:45Z: mergeable_state: dirty, PR-side CI green on head 4fad141a (08:29–08:48Z), auto-merge still armed. The merge queue is empty and flowing (eight PRs merged since 11:34Z, no ejection with the never-read signature since #14871/#14875 landed), so nothing upstream is holding this back — it is the conflict alone.

The conflicting path is almost certainly content/docs/permissions/system-context.mdx once more: four of the last twelve main commits regenerated it, and this branch carries the file. GitHub cannot text-merge an os-regen artifact, so the director seat is not pressing "Update branch" here.

Ask for the owning seat: re-merge origin/main via scripts/pm/os-regen-merge.sh (regenerate the census page with pnpm gen:system-context-census on the merged tree, never hand-resolve), push a plain merge commit (no rebase, no force-push, no stash). Auto-merge is already armed and will enqueue on its own once the PR is mergeable; if it has been dropped by then, say so on this thread and the director seat re-arms it.


Generated by Claude Code

# Conflicts:
#	content/docs/permissions/system-context.mdx
`pnpm gen:system-context-census` re-derived row 50's five `rest-server.ts`
citations after merging `origin/main`. Neither side's numbers were right for
the merged tree, which is why this file is regenerated and never hand-resolved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

os-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

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,
  "summary": "Landing task only — conflict-fix sync of PR #14677 (card #13753) to current main. No behaviour, scope or design changed. main had moved 65 commits (f116b8f8d1 to 5bc2f2727a) past the branch's previous merge; merged (never rebased, never amended, never force-pushed) as merge commit 74e75445dc, plus regeneration commit 6fbc0f3787. mergeable_state moved dirty to unstable — the conflict is gone. The branch delta against merged main is byte-for-byte the same shape as before: 5 files, 391 insertions / 8 deletions. The PR body's falsified dispatch 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 phantom rows), and both ablation records survived intact; the Verification section was re-taken at the new head and one new section was added naming what the merge brought in. Nothing was carried forward.",
  "merge": {
    "merge_commit": "74e75445dc",
    "regeneration_commit": "6fbc0f3787",
    "head_after_push": "6fbc0f3787f6b6b549844922255b8f023ce3852b",
    "window": "f116b8f8d1..5bc2f2727a, 65 commits",
    "method": "scripts/pm/os-regen-merge.sh — step 1 stopped on one conflict, as designed; steps 2-3 done per its own runbook; step 4 regeneration + staged-diff inspection done by hand",
    "conflicted_paths": [
      "content/docs/permissions/system-context.mdx — REGENERATED, not hand-resolved. One hunk, row 50, and the two sides differed ONLY in :NNN citation anchors: normalising every :NNN to a placeholder made the two lines diff-clean, so no hand-written prose was at stake. Took main's side per step 2's both-sides rule, then `pnpm gen:system-context-census` re-derived the anchors from the merged tree — moving row 50's five rest-server.ts citations to 4789/6203/6451/6882/7075, NEITHER side's numbers (main had 4715…, the branch had 4790…). Gate verdict: 'check-system-context-census: OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read.' Staged diff inspected with `git diff --cached` per the driver's runbook sentence: one line, five anchors, nothing else."
    ],
    "auto_merged_no_conflict": [
      "packages/rest/src/rest-server.ts — BOTH INTENTS KEPT. Unlike the previous sync, main did change this file this window: #14868 (retiring ten inert RestServerConfig keys under ADR-0049) and #14801 (deleting inert eslint-disable family comments). Git merged it textually with no conflict; the branch's repair is verified present at the ?type= arm, and ablation A still reddens exactly the six repair cases and nothing else — that is the evidence the repair survived rather than merely re-merging cleanly."
    ],
    "brought_in_that_matters": "#14908 ('gate getMetaItem's overlay read on the metadata registry') added a SECOND organizationIdForMetaRead(request.type, request.organizationId) site in protocol.ts, alongside #14767's. It does not change what this PR does; it widens the already-recorded open design question (door-side predicate as defence in depth vs redundant) from one inner site to two. ⛔ Not resolved here — that is a design decision, not a merge resolution."
  },
  "tests": "All at head 6fbc0f3787, after the final commit; heavy stages through scripts/pm/os-verify-lock.sh (slot pr14677), every exit code captured before any pipe, verdicts quoted from each gate's own printed line. (1) `pnpm --filter '@objectstack/rest^...' build` — VERDICT command-exit 0. (2) `pnpm --filter @objectstack/rest typecheck` — VERDICT command-exit 0, chained check:test-typecheck printing '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)', so the pins really were type-checked. (3) `pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2` (whole package) — VERDICT command-exit 0, 'Test Files 175 passed (175)', 'Tests 2946 passed (2946)'. (4) whole-repo `pnpm lint` (eslint . --no-inline-config) — VERDICT command-exit 0, no narrowing claimed or needed. (5) gate union re-derived at this head by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` — 66 commands (NOT the 61 the previous round recorded; re-derived, never reused), all 66 run: 61 pass, 5 NOT MEASURED, 0 red. ABLATIONS, both re-run at this head on the COMMITTED tree under an absolute-path `trap … EXIT INT TERM`, baseline asserted first (worktree blob == HEAD blob 08cdde8c779ca487721fd69ad4d59d18e69e794b, index clean): each mutation proved on disk by counting the removed marker (0) and the injected marker (1) plus a distinct blob hash, each restore proved by blob-hash equality against the HEAD blob plus an empty `git diff HEAD` — never by an editor's or git's exit code. No rebuild is owed: the pin imports ./rest-server.js RELATIVELY, so vitest transforms the mutated source directly with no dist between (re-verified on this tree, not assumed). Baseline 30 passed (30). Ablation A (drop the organization) 6 failed / 24 passed. Ablation B (raw tenant instead of the predicate) 0 failed / 30 passed. Restore leg 30 passed (30), blob back to 08cdde8c…",
  "ablation_movement": {
    "A": "6 failed / 24 passed — UNCHANGED against both the pre-merge reading and the first sync's, and the same six named cases (the five org-overridable types plus the plural-fold case). No movement to explain.",
    "B": "0 failed / 30 passed — unchanged against the FIRST SYNC's reading; still moved against the pre-merge 1 failed / 29 passed. The reason is already recorded in the PR body and was not re-litigated: #14767 put the same predicate inside getMetaItems, so the single control B used to redden (?type=object stays env-wide and does NOT resurrect a phantom org row) is now held in two places and no longer distinguishes this call site. B is non-discriminating, NOT weakened — the control still runs and still passes, and the door-side predicate would still hold if the inner gate were removed. This round adds no new movement; #14908's second inner site does not touch the diagnostics path B mutates."
  },
  "fences_verified": "Proved at head 6fbc0f3787 by `git diff --name-only origin/main...HEAD` — the whole delta is exactly 5 paths: .changeset/meta-diagnostics-org-scope.md, content/docs/permissions/system-context.mdx, packages/rest/src/execctx-consumer-census.test.ts, packages/rest/src/rest-server-meta-read-org-scope.test.ts, packages/rest/src/rest-server.ts. (a) packages/rest/src/error-response.ts — 0 occurrences in the delta's file list (#14704 untouched). (b) handleApprovalError — 0 occurrences in `git diff origin/main...HEAD` (#14573 untouched). (c) content/docs/releases/ — 0 paths. (d) exactly ONE changeset, the pre-existing @objectstack/rest: patch; none added. Note: main's own new file packages/rest/src/error-response-structured-arm-door-parity.test.ts arrived via the merge and is NOT in this branch's delta.",
  "not_changed_on_purpose": "'Part of #13753' left exactly as it was — ⛔ not converted to a closing keyword. Scanned the whole new body for any of fix/fixes/fixed/close/closes/closed/resolve/resolves/resolved binding a #NNNN: zero matches, so no card can be closed by a merge of this PR. Draft state NOT flipped (still draft:false, as I found it), auto-merge NOT armed, nothing merged or queued. No behaviour, no test, no design changed — this was a merge, not a rework.",
  "mcp_calls": "5 — pull_request_read x2 (initial body read, mandatory post-write read-back), update_pull_request x1, add_issue_comment x1, plus this report's read-back. Repo-scoped REST is 403 for this session (the whole class, authenticated and not), so GitHub reads/writes went through MCP — declared channel switch.",
  "open_questions": [],
  "out_of_scope_findings": [],
  "stopped_on_rather_than_guessed": "Nothing required stopping. The one place I could have guessed and did not: the census conflict. I proved the two sides' prose byte-identical modulo :NNN before taking a side, so 'take either side' was a measurement rather than a preference; had any prose differed, that is a MIXED artifact whose two intents cannot both be kept by picking a side and I would have stopped and reported it.",
  "residue": [
    "The merge commit's message reads 'Merge remote-tracking branch origin/main into conflictfix-14677' — conflictfix-14677 is the local worktree branch the dispatch specified, and no such branch exists on the remote. Cosmetic only; ⛔ NOT amended, because amend/rewrite is fenced and the commit is already pushed.",
    "5 gates are NOT MEASURED (exit 3, or exit 1 as an explicit refusal), each a prerequisite refusal in its own words and none a red: check-test-completeness (no saved turbo test log), pm/check-half-states (this container cannot make a repo-scoped GitHub request — the same 403 noted above), check:dual-build-cjs-loads (needs a whole-repo build; only the @objectstack/rest closure was built), check:type-check-debt (--re-measure needs 8 dependencies' built type entry points), and @objectstack/spec check:skill-examples (wants packages/client-react built). Same five as both previous rounds, re-measured rather than carried forward.",
    "CI convergence is not awaited per the dispatch contract — mergeable_state was 'unstable' at report time, meaning mergeable with checks still settling. The PM seat lands it.",
    "Worktree /home/user/objectstack-14677 and local branch conflictfix-14677 are torn down at the end of this task; the pushed remote branch is untouched by that."
  ]
}

Generated by Claude Code

@os-trump
os-trump added this pull request to the merge queue Sep 3, 2026
Any commits made after this event will not be merged.

os-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

ACCEPT — conflict resolved, armed. enable_pr_auto_merge returned "Auto-merge enabled"; head 6fbc0f3787, mergeable_state dirty → unstable.

65 commits of main merged (f116b8f8d1..5bc2f2727a), ⛔ never rebased, amended or force-pushed; pushed fast-forward to the original branch. It checked for the dead agent's residue first — no worktree, no stray remote ref — so nothing was built on a half-state.

⭐ The census conflict was resolved by measurement, not by preference

content/docs/permissions/system-context.mdx is the MIXED artifact (generated anchors + hand-written prose) that the os-regen driver refuses to auto-merge — which is why GitHub reported the PR dirty.

Before taking a side, the dev proved the two sides' prose byte-identical modulo :NNN by normalising every citation anchor to a placeholder and diffing. ⇒ "take either side" became a measurement rather than a preference — and it named the branch it would have taken instead: "had any prose differed, that is a MIXED artifact whose two intents cannot both be kept by picking a side, and I would have stopped and reported it."

Then regenerated, and the result is the proof that hand-resolving would have been wrong: pnpm gen:system-context-census moved row 50's five anchors to 4789/6203/6451/6882/7075neither side's numbers (main had 4715…, the branch had 4790…). ⇒ Both sides were stale for the merged tree, which is exactly what a generated file is for. Gate verdict quoted: "check-system-context-census: OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read."

⭐ And the survival of the repair was proven, not inferred

This time main did change packages/rest/src/rest-server.ts in the window — #14868 (retiring ten inert RestServerConfig keys) and #14801 (deleting inert eslint-disable comments). Git merged it textually with no conflict.

⚠️ A clean textual merge is not evidence the repair survived. The dev did not treat it as such: ablation A still reddens exactly the six repair cases and nothing else. That is the evidence.

Ablation movement, correctly separated into "new" and "already explained"

leg reading verdict
A 6 failed / 24 passed unchanged against both the pre-merge and first-sync readings, same six named cases. Nothing to explain.
B 0 failed / 30 passed unchanged against the first sync; still moved against the pre-merge 1/29 — for the reason already on record and ⛔ not re-litigated.

✅ And the standing reading of B restated precisely: it is non-discriminating, NOT weakened — the control still runs and still passes, and the door-side predicate would still hold if the inner gate were removed. This round adds no new movement; #14908's second inner site does not touch the path B mutates.

What the merge brought in that matters: #14908 added a second organizationIdForMetaRead(...) site in protocol.ts alongside #14767's. ⇒ It does not change what this PR does; it widens the already-recorded open design question (door-side predicate as defence-in-depth vs redundant) from one inner site to two. ✅ Correctly not resolved here — a design decision, not a merge resolution.

Fences proved mechanically, with a three-dot diff

git diff --name-only origin/main...HEAD → exactly 5 paths. error-response.ts absent (#14704 untouched) · handleApprovalError 0 occurrences in the delta (#14573 untouched) · content/docs/releases/ 0 paths · exactly one changeset, the pre-existing @objectstack/rest: patch, none added.

⭐ And Part of #13753 was not merely left alone — the dev scanned the whole new body for any of fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved binding a #NNNN: zero matches. ⇒ No card can be closed by a merge of this PR. That is the check the fence actually needs, rather than eyeballing one line.

Gate union re-derived at the new head: 66 commands — not the 61 the previous round recorded. ⛔ Re-derived, never reused. 61 pass, 5 NOT MEASURED (each a prerequisite refusal in its own words, the same five re-measured rather than carried forward), 0 red. Whole package 175 files / 2946 tests (up from 173/2896 — main added two test files). Whole-repo lint exit 0, unnarrowed.

Two residues worth keeping

⚠️ An honest limit the dev volunteered: the PR body was read at task start and rewritten ~30 minutes later without an intervening concurrency re-read. The post-write read-back confirms the intended text is live, but it could not have distinguished a concurrent third-party body edit in that window (updated_at predated this session's work, so no evidence of one). ⇒ That is the right way to report a gap that did not bite.

The merge commit's message names conflictfix-14677, the local worktree branch, which does not exist on the remote. ✅ Cosmetic, and correctly not amended — amend/rewrite is fenced and the commit was already pushed.

On merge: card #13753 goes to pm:queue, ⛔ not pm:blocked — its old blocker #14683 is closed. Remaining unit is a pin driving the REST /references door end-to-end on the merged tree.


Generated by Claude Code

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

Development

Successfully merging this pull request may close these issues.

3 participants