Repository navigation
Org-overridable metadata (view, dashboard) is accepted with a 200 state:'active' receipt but served by no read door #9454
Description
Activity
- changed the title
[-]A runtime-authored `view` is accepted with a 200 `state:'active'` receipt but is served by no read door[/-][+]Org-overridable metadata (`view`, `dashboard`) is accepted with a 200 `state:'active'` receipt but served by no read door[/+]on Aug 18, 2026 os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsTriage: lands in the metadata org-override serving path (
packages/metadata*) ⇒domain:metadata; queued as Bug — a 200state:'active'receipt with a version and sequence number on a write that no read door then serves is declared≠enforced on a published surface, and the author's work is functionally lost the moment the receipt renders.target:v17set: release-blocker class ② (public contract declared≠enforced) — the write door actively lies to authors on a shipped surface. The two-types-one-card scoping stands as filed (one mechanism, one fix); the pin must cover bothviewanddashboardand assert write-then-read agreement on the same boot. (Triage seat, sessionsession_019gCKd9EZfQ6MbGTnMHHJvW.)
Generated by Claude Code
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsTriage: re-routed
domain:metadata→domain:cli, and the cross-domain question is settled with it. (Triage seat, sessionsession_0185aJyu6TRnHbcVgSfGPSho, answering the metadata-seat ask at 6015#issuecomment-5327040864.)Rationale (anchoring rule, independently re-verified on
origin/main@ e7daea1 this round):packages/rest/src/rest-server.tsimportsorganizationIdForMetaWriteand threads an org id at its write doors only (6 call sites in the 5090–6063 region); none of its read sites thread a scope.- Its twin
packages/runtime/src/domains/meta.tsthreadsresolveActiveOrganizationId(...)on reads as well (:291/295,:536–537,:578–579,:597–602).
So the serving layer in
packages/metadata*is not the broken half — the REST read door is. The fix lands inpackages/rest⇒domain:cliowns the card.Cross-domain contingency (pre-appointed): if the shared registry-derived predicate turns out to need an export from
packages/metadata-core/metadata-protocol, the cli lane is hereby designated as the single claimant for the cross-lane PR under the cross-domain exception — claim must declare the full file surface and run the targeted in-flight check againstdomain:metadata's in-flight claims.⚠️ Correction that stands IN PLACE OF a body edit (the body reads truncated through the MCP surface — the #8813/#9557 trap — so editing it via the API would destroy it; next claimant: read the body on the web UI, and read this comment as part of the card):- The body's "What a Fix Has to Settle" section says the fix belongs "at the overlay-resolution layer". The conclusion (fix once, centrally, not per type) stands; the location is wrong — the overlay-resolution layer is measured correct and type-agnostic (
getMetaItemresolves(orgId ? findOverlay(orgId) : undefined) ?? findOverlay(null);getMetaItemsunions both scopes). The central fix point is the REST read path. - Everything else the metadata seat settled on-card stands: read-side direction, one shared predicate (⛔ not an inlined copy, ⛔ not the org-blind fallback — that one is a cross-tenant disclosure), the two-branch trap (
viewvia the cached read,dashboardbypassing it viaisDashboardType), full analysis at 5327033765.
target:v17retained — release blocker, anddomain:cli's queue should treat it at board priority.
Generated by Claude Code
domain:cliseat acknowledges ownership — and records a selection error of mine that cost this card its place in the queueSession
session_012WKSnqAaoqtW3QX7SSf1Vk. ⛔ Not claimed yet; see the serial constraint below.⛔ My error, stated first
Triage re-routed this card to
domain:cliat 11:13:51Z. At 12:16Z — an hour later — I dispatched #9426, which lands inpackages/rest/src/rest-server.ts, the same file this card's fix lands in. This card outranks it:target:v17board item and typeBugbeats a plainBugin the selection total order, and it was already in my lane when I chose.Cause: I selected from memory instead of re-reading the lane. PR #9459 merged, I knew three cards had been serialised behind it, and I dispatched the best of those three without re-reading
labels:["domain:cli"]first. This card had arrived in the meantime. That is precisely the discipline my own patrol timers open with — ⛔ 不得沿用本文任何结论 — and I skipped it while optimising for the throughput the maintainer had asked for. Speed came out of the wrong place: the selection step, not the pipeline.Consequence, and what I am doing about it
#9426 is in flight on
rest-server.ts, so this card is hard-serial behind it. I am not killing that agent to jump the queue: it has been running ~20 minutes, and the churn plus lost work is a worse trade than the delay, given this card is substantial enough (five REST read sites, a new shared predicate, a signature change on the cached door) that it would not finish sooner either way. That is a judgement about cost, ⛔ not a claim that the ordering was right.This card is the next dispatch off this lane the moment
rest-server.tsfrees — ahead of #9488 and #9487, which are the other two waiting on that file. Recorded in the patrol state so it survives a session boundary.What the next dispatch will NOT re-derive
The metadata seat and its dev left this card fully diagnosed, and I am not sending anyone to re-audit it:
- Direction settled: (a) make the read door serve it. The write door is correct as-is — the row is persisted, so the 200
state:'active'receipt is truthful and the reader is simply looking in the wrong partition. ⇒ Clause-② is no: the accept set does not change. (Had it been (b) — make the write door refuse — that would be a contract accept/reject change, fable-mandatory, and ⛔ not dispatchable under the maintainer's one-time downgrade, which named five specific cards and not this one.) - Shape settled: option A. One registry-derived predicate — a read-side twin beside
organizationIdForMetaWrite— threaded at the 5 REST read sites via the already-memoisedresolveExecCtx, plus anorganizationIdparameter ongetMetaItemCached. ⛔ B rejected (a second copy of a registry-derived decision, whichmeta-write-org-scope.ts's own header forbids in writing). ⛔ C rejected and this is the line worth keeping — an org-blind overlay fallback matches an arbitrary org's row becausematchesWhereskipsundefinedkeys, so it is a cross-tenant disclosure, not a fix; and the card's own confound control (onesys_organizationrow, and it is the session's active org) is exactly what would hide that in the reproduction while it leaks in any real multi-org deployment. - ⭐ The two-branch trap, which is not visible from the card body:
viewtakes the cached read branch anddashboardbypasses it viaisDashboardType, so a fix applied to one branch fixes exactly one of the two types. That is why triage demanded a pin covering both. - Generality measured: all five org-overridable types (
view,dashboard,report,translation,email_template) behave identically on currentmain, so this is one caller-side change, not five. ⚠️ The body's "What a Fix Has to Settle" section is wrong about location — the overlay-resolution layer is measured correct and type-agnostic. Its conclusion (fix once, centrally) stands.⚠️ And the body reads truncated through the API (the [finding] GitHub MCP issue reads return HTML-escaped bodies, so any seat that appends a line by rewriting a card body may store'literals into its code blocks #8813/GitHub MCP truncates an issue body at a literal<scripttoken — the card reads fine on the web and short through the API #9557 trap), so ⛔ the dispatch will instruct reading it on the web UI and treating triage's 11:13:51Z comment as part of the card.- Deferred, next seat's call: whether to fold
organizationIdexplicitly into the cached door's ETag hash aslocalewas for [P2] i18n: zh-CN ↔ English switch occasionally garbled — requires page refresh #1319. The dev was careful not to overclaim a leak it had not measured — there is no server-side shared cache entry keyed by type+name and the directive isprivate— but making scope a property of the validator rather than an emergent property of the body is the safer build.
⇒ ⭐ Worth naming: the metadata seat's dev returned
needs_decisionwith zero commits and no PR rather than widening past its file surface, and that is why this card is re-dispatchable with the answer attached instead of half-built in the wrong package. A stop with a complete diagnosis is a better outcome than a PR that guessed.
Generated by Claude Code
- Direction settled: (a) make the read door serve it. The write door is correct as-is — the row is persisted, so the 200
⚠️ Pre-dispatch stale-premise check — one premise on this card is overstated, and the "5 read sites" figure is staledomain:cliseat, sessionsession_012WKSnqAaoqtW3QX7SSf1Vk. ⛔ Still not claimed (rest-server.tsremains held by #9637). Re-derived onorigin/main@5f99739cb, not replayed from the tree the diagnosis was taken on.The direction and the shape are untouched by this. (a) still correct, option A still adopted, C still refused as a cross-tenant disclosure, the
view/dashboardtwo-branch trap still real. What follows corrects two factual premises, one of which a dev would trip over on its first read.① ⛔ "none of its read sites thread a scope" is false — one already does
Triage at 11:13:51Z: "none of its read sites thread a scope." The dev's report: "passes NO
organizationIdat any of its 5 READ sites." Measured, on today'smain,rest-server.ts:5845:const layered = await publishedProtocol.getMetaItemLayered({ type, name, ...(publishedCtx?.tenantId ? { organizationId: publishedCtx.tenantId } : {}),
That is the proposed fix's exact idiom —
resolveExecCtx→ conditional spread — already in this file, on the published-protocol layered read.Two consequences, and the second is the one that matters:
- Good news: the fix has an in-file precedent to mirror rather than invent. Whatever lands should read like
:5845, not like a new seam. ⚠️ Hazard: a dev told "no read site threads org" who then meets:5845has been handed a false map. The likely failure is not confusion but correction — "fixing" a site that is already right, or concluding the card's diagnosis is unreliable and re-auditing the layer the card already (correctly) says is fine.
② "5 REST read sites" is stale — I count 14 candidates
The card names
:2419,:3804,:4639,:4749,:5926. Four of those still map cleanly (→:2423,:4643,:4753,:5930);:3804→:3808. But a broad re-derivation of every meta read verb in the file returns 14 call sites::1558 getMetaItems :4348 getMetaItems :5845 getMetaItemLayered ← already org-aware :2423 getMetaItemLayered :4377 getMetaItems :5930 getMetaItem :3808 getMetaItems :4643 getMetaItemCached :7228 getMetaItem (type:'object', hardcoded) :4753 getMetaItem :4884 getMetaItems :7718 / :7782 / :8098 getMetaItems⛔ I am not asserting all 14 are in scope — several plainly are not (
:7228hardcodestype: 'object', a non-overridable type). I am asserting that "5" is not a number anyone should dispatch against, and that classifying these is the dev's first deliverable, not a detail.Why this is the same trap the card already caught, one level up
The card's best finding is that
viewanddashboardreach one mechanism through two different branches, so a fix applied to one branch fixes exactly one type. The same shape recurs at file scale: the five org-overridable types are served by more doors than the diagnosis enumerated, so a fix applied to the five named sites may leave other doors still reading the wrong partition — passing its own pin while the defect survives on a route the pin does not cover.⇒ Zone 1 for the coming dispatch: enumerate every meta read call site in
rest-server.ts, classify each as in-scope or out-of-scope with the reason, and report the classification. The pin must cover bothviewanddashboard(triage's requirement, unchanged) and the enumeration must be exhaustive, ⛔ not the inherited count of 5.On the record
Both the 11:13:51Z triage comment and my own 12:21:24Z acknowledgement state ① as fact, and I repeated it without checking. It held up for exactly as long as nobody measured it. Line numbers will shift again when #9637 lands, so the dispatch will declare its file surface by symbol and region, never by line.
Generated by Claude Code
- Good news: the fix has an in-file precedent to mirror rather than invent. Whatever lands should read like
Claim: PM loop round 19
Session:session_012WKSnqAaoqtW3QX7SSf1Vk
Branch:claude/issue-9454-org-scoped-meta-read-door
Worktree:objectstack-issue-9454
Domain:domain:cli(cross-domain single claimant, pre-appointed by the triage seat at 11:13:51Z)
File surface (region-level, ⛔ not line numbers — #9637 just landed in this file and shifted them):packages/rest/src/rest-server.ts— the meta READ call sites only, by symbol. ⛔ NotorganizationIdForMetaWrite's 8 existing write sites; ⛔ notenforceAuth; ⛔ not the audit limb The REST audit route answers a MISSINGauditMetaItemcapability with{events: []}— a compliance surface reporting "this item has no audit trail" #9426 just changed.packages/metadata-core/src/meta-write-org-scope.ts— add the read-side twin beside the existing write predicate.packages/metadata-protocol/src/protocol.ts—getMetaItemCached's signature only (it is the sole meta read verb that cannot express an org).- the new pin +
.changeset/**.
Container & model:
M–L,mode:subagent,model: opus— tier derived this round fromnode scripts/pm/dispatch-gates.mjs --tier <the three paths above>: no path-derived mandate (floor sonnet · default opus · ceiling fable), ⛔ not recalled. Held at default: the card carries real design judgment (one shared predicate vs. a second copy), which is what the default tier is for.
Clause-②: no. Direction (a) makes the read door serve what the write door already persisted — the accept set does not change.⚠️ Had it been (b) make the write door refuse, that would be a contract accept/reject change and fable-mandatory; the metadata seat's dev closed that fork by measurement (the row is persisted, so the 200 receipt is truthful). ⛔ This card is not covered by the maintainer's one-time opus downgrade (that named #9446/#9416/#9462/#9488/#9487); it sits at defaultopuson its own terms.Serial constraints cleared:
- Same file (
packages/rest/src/rest-server.ts) — now clear. fix(rest): refuse when auditMetaItem is absent instead of answering "this item has no audit trail" #9637 (The REST audit route answers a MISSINGauditMetaItemcapability with{events: []}— a compliance surface reporting "this item has no audit trail" #9426) merged; verified on two readings plus a probe:origin/mainheaded4ca5999, and the stringauditMetaItem() is not available in this kernelwent 0 → 3 files against a control of 9 for the phrase family.GET /api/v1/meta/<unknown-type>answers 200 with an empty collection while the write door refuses the same type #9488 and Every 401 answers{error, message}with nocodekey, while every other error family carries{error, code}#9487 also land in this file and are not dispatched — they stay queued behind this card, which outranks them (target:board item + type Bug). - Same package (
packages/rest,packages/metadata*) — permitted to run in parallel under the maintainer's 2026-08-18 exemption 「放开同包,同文件仍硬串行」. Replacement discipline applies and is carried into the dispatch: ① region-level surface (above) ② mergemainbefore opening the PR ③ merge again after any sibling lands ④ conflicts go to the merge queue, ⛔ PM does not hand-order. - Cross-domain targeted in-flight check (required by the pre-appointment):
domain:metadatahas exactly one card in flight — #7282's watcher-timing flake family is NOT closed — it now ejects unrelated PRs throughwatch-write-registration.test.ts, the pin PR #7336 added to close it #9339, which triage placed inpackages/metadata/metadata-fs(repository.tswatcher/suppression coupling, pinwatch-write-registration.test.ts). Disjoint from every path above. No intersection, no yield. ⚠️ Adjacent card named, not in flight:auditMetaItem's unqualifiedcatchreports ANY failed audit read as{events: []}— the compliance trail says "no entries" when the read broke #9638 (pm:queue,domain:metadata) targetspackages/metadata-protocol/src/protocol.ts—auditMetaItem's unqualifiedcatch. That is the same file as my third surface item but a different method (getMetaItemCached). It is not dispatched, so there is nothing to serialise against today; naming it here so the metadata seat can see the overlap before it dispatches. ⛔ Not mine to claim, hold, or re-order.- In-flight in this lane: tests(attachments): pin the unpinned authorization clauses of the attachments chain — coverage gaps from QA run #9401 #9483 (attachments dogfood tests) and packages/runtime's vitest alias list omits
@objectstack/spec/cloud, so any runtime test that loads a compiled artifact dies with ENOTDIR #9457 (packages/runtimevitest alias) — neither touches any path above. automation resume: a known top-level key with a type-mismatched value is silently dropped — the submission is still treated as empty #9416 (PR fix(runtime): resume refuses a type-mismatched value on an accepted key instead of dropping it (#9416) #9685) and Declared endpoints (type: 'flow'): converge onto the #9378 status table — the ruled follow-up to #9446's staged execution #9462 (PR fix(runtime): declaredtype: 'flow'endpoints answer the #9378 flow-dispatch status table, from the one shared definition #9676) are queued/landed inpackages/runtime, disjoint.
Generated by Claude Code
- added a commit that references this issue
on Aug 18, 2026 { "issue": 9454, "status": "done", "branch": "claude/issue-9454-org-scoped-meta-read-door", "pr": "https://github.com/objectstack-ai/objectstack/pull/9727", "premise_still_valid": true, "card_rendering_verified": "Web UI (https://github.com/objectstack-ai/objectstack/issues/9454) — the full rendered body including the complete defect table and the 'What a fix has to settle' section. The API body is truncated mid-table as warned. Also read as part of the card: the triage seat's 11:13:51Z comment (re-route to domain:cli + the in-place body correction) and the metadata seat's analysis at comment 5327033765, plus the 14:33:51Z stale-premise check.", "summary": "Direction (a) and shape A implemented as ruled; no re-litigation. organizationIdForMetaRead was added beside organizationIdForMetaWrite in packages/metadata-core/src/meta-write-org-scope.ts, deriving from the SAME registry flag via declaresOrgOverride, so read and write scope cannot drift. getMetaItemCached in packages/metadata-protocol gained an organizationId member (it was the only meta read verb that could not express one — it hard-coded a two-key delegation to getMetaItem) and now folds the org into its ETag. In packages/rest/src/rest-server.ts the scope is threaded at the five in-scope read doors via the already-memoised resolveExecCtx, with ONE resolution hoisted ABOVE the cached/isDashboardType fork so the two arms are structurally incapable of disagreeing — that hoist is the direct answer to the two-branch trap. The enumeration (below) found the real population is 18 call sites, not 5 and not 14: the four optional-call `?.()` forms are invisible to a plain `.getMetaItems(` scan. 5 in scope, 1 already correct (NOT touched), 12 out of scope. Pin drives real REST routes against a real ObjectStackProtocolImplementation over a stub engine: write-then-read agreement on one boot, all five org-overridable types, both arms, plus a SECOND organization as the control that would catch refused option C.", "enumeration_18_sites": [ { "n": 1, "verb": "getMetaItems", "type_arg": "'object' literal", "class": "out-of-scope", "reason": "loadObjectItems / api-exposure gate; object is allowOrgOverride:false" }, { "n": 2, "verb": "getMetaItems", "type_arg": "'book' literal", "class": "out-of-scope", "reason": "fetchAudienceBooks; book is not org-overridable" }, { "n": 3, "verb": "getMetaItemLayered", "type_arg": "req.params.type", "class": "IN-SCOPE", "reason": "serveMetaItemLayered — the Studio layered/diagnostic view; dynamic type reaches all five org-overridable types, and an unstated scope here MISREPORTS the very thing being diagnosed (overlay read as null)" }, { "n": 4, "verb": "getMetaItems", "type_arg": "'object' literal", "class": "out-of-scope", "reason": "OpenAPI {object} path expansion; not org-overridable" }, { "n": 5, "verb": "getMetaItems", "type_arg": "'api' literal", "class": "out-of-scope", "reason": "OpenAPI endpoint enrichment; api is not org-overridable" }, { "n": 6, "verb": "getMetaItems", "type_arg": "req.params.type", "class": "IN-SCOPE", "reason": "the LIST door — the card's 'scoped listing unchanged' symptom" }, { "n": 7, "verb": "getMetaItems", "type_arg": "'book' literal", "class": "out-of-scope", "reason": "book-tree route; not org-overridable" }, { "n": 8, "verb": "getMetaItems", "type_arg": "'doc' literal", "class": "out-of-scope", "reason": "book-tree docs; not org-overridable" }, { "n": 9, "verb": "getMetaItemCached", "type_arg": "req.params.type", "class": "IN-SCOPE", "reason": "the CACHED arm — view's route. Required the protocol signature widening" }, { "n": 10, "verb": "getMetaItem", "type_arg": "req.params.type", "class": "IN-SCOPE", "reason": "the UNCACHED arm — dashboard's route via isDashboardType, plus every read the cache exclusions divert here" }, { "n": 11, "verb": "getMetaItems", "type_arg": "'doc' literal", "class": "out-of-scope", "reason": "audience corpus for doc gating; not org-overridable" }, { "n": 12, "verb": "getMetaItemLayered", "type_arg": "dynamic", "class": "ALREADY-CORRECT", "reason": "/published overlay read; already threads organizationId from publishedCtx?.tenantId via conditional spread. NOT touched — it is the idiom the new sites mirror" }, { "n": 13, "verb": "getMetaItem", "type_arg": "req.params.type", "class": "IN-SCOPE", "reason": "compound-name door /:type/:section/:name — serves EVERY type through one generic getMetaItem, so left org-blind it would be the surviving wrong-partition route" }, { "n": 14, "verb": "getMetaItem", "type_arg": "'object' literal", "class": "out-of-scope", "reason": "export field metadata; not org-overridable" }, { "n": 15, "verb": "getMetaItems", "type_arg": "'view' literal", "class": "out-of-scope BY CALLER, not by type", "reason": "resolveFormBySlug backing the PUBLIC /forms/:slug route. view IS org-overridable, so this is the one site that is not excluded by its type. Excluded because the route is deliberately anonymous: it bypasses enforceAuth and resolves NO execution context anywhere in its region, so there is no session-derived active org — organizationIdForMetaRead('view', undefined) answers undefined at every call and wiring it would only add a session resolution to a public route. Filed as #9725 rather than silently skipped" }, { "n": 16, "verb": "getMetaItems", "type_arg": "'object' literal", "class": "out-of-scope", "reason": "public-form object schema narrowing; not org-overridable" }, { "n": 17, "verb": "getMetaItems", "type_arg": "'object' literal", "class": "out-of-scope", "reason": "public-form lookup referenceTo resolution; not org-overridable" }, { "n": 18, "verb": "getMetaItems", "type_arg": "'dataset' literal", "class": "out-of-scope", "reason": "analytics dataset lookup; not org-overridable" } ], "enumeration_totals": "18 total = 5 in-scope + 1 already-correct + 12 out-of-scope. The inherited '5' and the brief's '14' are BOTH short: the four optional-call forms (sites 1, 4, 5, 18) use `.getMetaItems?.(` and are missed by a `.getMetaItems(` scan.", "etag_decision": "CHOSE TO FOLD organizationId INTO THE HASH (the brief's option B), prepended and only when present so an org-less caller's validator stays byte-identical. Reasoning, stated without overclaiming: the #1319 locale precedent is analogous in SHAPE but NOT in mechanism — locale is INVISIBLE to the hash (the body is translated after the validator runs), which is why folding it in was the only way it could vary the validator; organizationId is VISIBLE, because the org-resolved document is the very thing hashed. So option A was functionally correct on today's code and I explicitly do NOT claim a measured leak: the directive is `private, no-cache` and there is no server-side cache entry keyed by type+name. B was chosen because it makes scope a DECLARED property of the validator rather than an emergent property of the body — two orgs whose documents are byte-identical today share a validator by coincidence, not by statement, and any future path that resolves an org row but falls back to the env-wide body would answer a 304 pinning the caller to a wrong-scope document with nothing in the validator to show it. Cost is one string concat plus a one-time revalidation miss for org-scoped callers.", "reverse_verification": "PREDICTED IN WRITING BEFORE RUNNING: 11 red / 4 green of 15 cases — the 4 green being the three cross-tenant/registry controls (they assert ABSENCE, which pre-fix holds trivially) and the persistence premise (the write door is unchanged by this card). MEASURED: 11 red / 4 green, membership exactly as predicted. Failure shape names the defect: 'GET /view did not serve the item: expected 404 to be 200'. ⚠️ Load-bearing detail: metadata-core and metadata-protocol are NOT vitest-aliased in packages/rest (only plugin-hono-server and service-datasource are), so they resolve through the workspace link to dist/ — a source-only ablation would have been a FALSE GREEN. Both ablation and restore were rebuilt and proven into the artifact with scripts/ablation-dist-preflight.mjs: '--absent' leg reported 'marker absent from all 18 built files', restore leg reported 'marker present in 4 built files'. Restored byte-identically via git checkout of the committed branch (git status clean); 15/15 green again.", "gate_union_derived": "Re-derived from ACTUAL changed paths with `node scripts/pm/dispatch-gates.mjs packages/metadata-core/src/meta-write-org-scope.ts packages/metadata-protocol/src/protocol.ts packages/rest/src/rest-server.ts packages/rest/src/rest-server-meta-read-org-scope.test.ts` — 8 path-matched + 5 convention-triggered (adds a test file). The dispatch brief named NONE of these, so all 13 are additions I ran on my own derivation. All results below are real echoed output on the final merged head 54b446818, never a bare exit code.", "gate_results": { "check:authz-resolver": "EXIT=0 — 'single shared authorization resolver intact; both entry points delegate.'", "check:cross-package-test-inputs": "EXIT=0 — 'OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.' (33 self-test cases passed)", "check:dispatcher-error-vocabulary": "EXIT=0 — 'OK — 17 unregistered code-stamping site(s), all classified'", "check:durability-log-level": "EXIT=0 — '29 durability-critical catch seam(s), all loud'; read-seam invention: '66 read seam(s), none invents an unreported answer'", "check:filter-alias-parity": "EXIT=0 — 'OK (4 wire spelling(s) of the where slot, identical on both sides: $filter, filter, filters, where)'", "check:route-envelope": "EXIT=0 — self-test passed", "check:query-options-erasure": "EXIT=0 — 'test surface: 240 site(s) in 47 file(s) — at the ceiling'; 'baseline key set verified against ed4ca59: no files added'", "check:engine-double-contract": "EXIT=0 — 'OK — 321 pinned, 133 in the DEBT ledger, 2 exempt.' (my new fake engine routes delete/update through assertEngineDeleteDispatch/assertEngineUpdateDispatch, copied from the sibling org-scoped-write-refused harness)", "check:where-matcher": "EXIT=0 — '0 silently-wrong and 0 unjudged matcher(s); none new' (my matchesWhere handles $or as a combinator, not a field name)", "check:nul-bytes": "EXIT=0 — 'scanned 6217 text file(s) ... no raw ASCII control bytes' (75 self-test assertions)", "check:type-check-coverage": "EXIT=0 — '64/77 workspace packages type-checked, 13 in the DEBT ledger, 1 exempt'", "check:type-check-debt (RATCHET)": "EXIT=0 — '33 ledger entr(ies) re-measured in 334.4s, 1926 raw tsc error(s) total, none above its recorded number. surplus: none'. Required and got the full workspace build (turbo: 70 successful, 70 total). ⚠️ An earlier run reported ELIFECYCLE — that was MY OWN `timeout 400` cutting a 334s+selftest run short, NOT a drift; re-run with a longer guard and it passed.", "check-affected-docs": "EXIT=0 — 'affected-docs self-test: 242 cases pass.'" }, "tests": "All measured on the FINAL head 54b446818 (after merging origin/main — 12 commits, no conflicts, none touching this surface). Dependency closure built first: `pnpm --filter '@objectstack/metadata-protocol...' --filter '@objectstack/rest^...' build` => REAL_BUILD_EXIT=0; full workspace `turbo run build` => '70 successful, 70 total' for the ratchet. Suites (SUITES_EXIT=0): packages/rest 127 files / 2077 passed; packages/metadata-protocol 122 files / 1684 passed + 10 skipped; packages/metadata-core 10 files / 162 passed. Typecheck (TYPECHECK_EXIT=0): packages/rest 'tsc --noEmit' Done, packages/metadata-core 'tsc --noEmit && tsc --noEmit -p tsconfig.test.json' Done. ⚠️ @objectstack/metadata-protocol carries NO typecheck script — confirmed by reading its package.json scripts — so NO typecheck is claimed for it; a `pnpm --filter ... typecheck` there would have been a zero-match exit-0 false green. New pin packages/rest/src/rest-server-meta-read-org-scope.test.ts: 15/15 passed. Every heavy command ran under `flock -E 99 -w <=200 /tmp/os-heavy-verify.lock` with loop-retry, gaps spent on lock-free work (PR body, enumeration, changeset); one 99 timeout was observed and retried rather than waited on.", "open_questions": [], "zone2_assumptions": { "resolveExecCtx_memoised": "CONFIRMED, not falsified. packages/rest/src/rest-server.ts holds `execCtxMemo`, a WeakMap keyed by the per-request `req` object with an inner Map keyed by environmentId, caching the IN-FLIGHT promise so concurrent callers share one resolution; its own comment records ~16 sequential queries per resolution. So threading the scope at 5 read doors adds NO new per-request org resolution — several of those handlers already resolve it further down.", "etag_needs_no_change": "Made the call rather than deferring — folded it in. See etag_decision above; option A was correct on today's code, B chosen as the safer build, and no unmeasured leak is claimed.", "no_packages_spec_change_required": "CONFIRMED — no packages/spec file was touched. The widening landed on getMetaItemCached's INLINE TypeScript parameter type in packages/metadata-protocol/src/protocol.ts, not on the spec Zod schema. ⚠️ But the assumption is true for a reason worth surfacing: the spec's GetMetaItemsRequestSchema / GetMetaItemRequestSchema / GetMetaItemCachedRequestSchema declare NO organizationId at all, while the implementation has accepted and honoured it on the first two all along. That is pre-existing declared-vs-implemented drift I did not create and did not widen into — filed as #9726 for the spec seat.", "pin_can_use_stub_engine_pattern": "CONFIRMED with two corrections the pattern alone did not supply, both harness-side: (1) the REST write door runs FULL spec validation, so `{ label: 'x' }` bodies were refused 422 INVALID_METADATA and never reached the store — each type needed a real spec-valid body, measured against the live validator rather than guessed; (2) the LIST door calls registry.isPackageDisabled, absent from the sibling's registry double, which answered 500 and read exactly like 'the listing still does not serve org rows'." }, "falsified_inherited_claims": [ "The brief's own re-derivation of '14 candidate call sites' is short by 4: the optional-call forms `.getMetaItems?.(` at sites 1, 4, 5 and 18 are invisible to a `.getMetaItems(` scan. Real population is 18. (The brief was right that '5' must not be implemented against — and right one level deeper than it knew.)", "Confirmed rather than falsified: the brief's warning that one site already threads organizationId is correct — the /published getMetaItemLayered read. It was NOT touched, and its conditional-spread idiom is what every new site mirrors." ], "out_of_scope_findings": [ "filed as #9725: public /forms/:slug reads a hardcoded type:'view' (an org-overridable type) but is deliberately anonymous with no execution context in its region, so an org's overlay of a public form view is never applied for anonymous visitors — closing it needs host/environment-derived org, a different mechanism", "filed as #9726: [finding] the spec's three meta-read request schemas omit organizationId while the protocol implementation accepts and honours it — pre-existing declared-surface drift, packages/spec belongs to another seat" ], "notes_for_pm": [ "CI convergence NOT waited on, per the 2026-08-10 ruling — draft PR #9727 is open and this report is delivered at that moment. Gate status at report time: locally green on 54b446818 for the full derived union including the ratchet; CI job conclusions are the PM's read.", "⚠️ Trailer conflict recorded rather than silently resolved: the harness system prompt specifies a Co-Authored-By trailer carrying a model identifier, while your resume message instructed 'no model identifier in any pushed artifact; keep Co-Authored-By: Claude'. I followed YOUR instruction on all pushed commits (the branch was still unpushed when the instruction arrived, so this needed no history rewrite and no force-push). Flagging so the divergence from the harness default is yours to confirm.", "Same-package replacement discipline observed: region-level surface as claimed, main merged before opening the PR (12 commits, clean), and I did not hand-order anything.", "Worktree torn down cleanly (node_modules removed, `git worktree remove` without --force); the other agents' worktrees were left untouched." ] }
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026
Extracted from Tier-2B (#9453,
view) and Tier-2C (#9467,dashboard) of the QA wave #9296. Subject shae4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd, stock showcase.Scope widened 2026-08-18. Filed first against
view; the next round found the identical shape ondashboard, independently, with its own controls. Two metadata types, one mechanism — kept as one card because splitting it would invite two fixes for one defect and let the general case escape.The defect
A runtime
PUTof an org-overridable metadata type answers 200 with a receipt reportingstate: 'active'plus a version and sequence number — the shape that tells an author "this is live". Nothing then serves it:GETof the item?object=<object>)view: nothing renders ·dashboard: "Dashboard Not Found"Draft → publish behaves identically. The platform reports success in the same breath as not delivering it — declared ≠ enforced, in the direction hardest for an author to notice, because the write path says everything worked.
The acceptance half is correct — the read side is the bug
viewanddashboardare deliberately in the org-overridable set (allowOrgOverride: true,packages/spec/src/kernel/metadata-plugin.zod.ts), which is exactly why a runtimePUTagainst a read-only showcase package is accepted rather than refused — unlikeobject,field,hookandseed, which carryallowOrgOverride: falseand are correctly locked. So the 200 is right. The anomaly is that nothing reads the overlay back.The org-overridable set is
view,dashboard,report,translation,email_template. Two of the five are now measured broken by the same shape; the other three are untested and should be assumed affected until shown otherwise — that is the general case the fix has to address.Evidence — the controls are what make this stand up
view(Tier-2B): 4 saves · 3 names · 2 objects · 2 independent boots.dashboard(Tier-2C): 5 saves · 4 names · 2 boots, one of them a fresh DB with a different org id.Controls measured on both, and they are the reason this is a defect rather than an observation:
sys_organizationrow, and it is the session's active org;dashboardadditionally re-measured against a different org id on a fresh DB.dashboard) — the write is observable where it lands.That combination separates "the write silently no-ops" from "the reader looks in the wrong place".
What a fix has to settle
Where the overlay write lands and why every read door misses it — at the overlay-resolution layer, not per type, or the remaining three types stay broken.
Pin it with a round-trip test: author at runtime, then read back through the direct GET and the scoped listing, for each org-overridable type. A test asserting only the 200 receipt passes today and proves nothing — that is precisely the gap this defect lives in, and why two rounds of a browser sweep were needed to find something the write path calls a success.
Left unlabelled for
domain:*and unlevelled — routing and priority are the triage seat's call. Filed P1 by its checklist items, though "the platform reportsactivefor metadata it will not serve, across a whole capability class" is an argument for re-levelling.