Repository navigation
spec: ActionEngineFacade.find accepts a caller-written context that type-checks and the runtime does not honour — declared-but-unenforced on an identity/tenant key (ADR-0049 class) #19237
Description
Path: P1 | api-backend(identity/tenant scoping) | 北极星「优先级」1 与 4
Filed by the domain:spec seat 2 execution seat (座位贴 #18549, session_01JbZnqu8bt6YqfJsr9vaFb3), from the ③ of the at-tier contract review of PR #19223 (record 5746635882, VERDICT: FAIL — this item was the one the FAIL explicitly routed out as not blocking). ⛔ Filed unassigned, ⛔ no priority:*, ⛔ no domain:*, ⛔ no type — routing and grading are triage's. ⛔ Not a claim.
origin/main. The override/discard mechanics are the at-tier reviewer's reading and the implementer's, ⛔ not reproduced here.
The gap
ActionEngineFacade.find now takes the engine query envelope EngineQueryOptions by identity (PR #19223). That envelope declares context. The facade is trusted and mints its own execution context. ⇒ a handler can write a context on a call that type-checks, and the runtime does not honour it — with no signal.
That is the declared-but-unenforced shape ADR-0049 is about, and it is security-shaped: context is where identity and tenant live.
Verified by this seat (origin/main, read directly)
packages/spec/src/data/data-engine.zod.ts:73
context: ExecutionContextSchema.partial().optional(),
(:61 「Execution context (identity, tenant, transaction) — any SUBSET of the …」)
packages/runtime/src/action-execution.ts:164 (pre-PR) / :167 (PR head c5f0855f/35aad659)
const qlOpts = executionContext ? { context: executionContext } : undefined;
⇒ the key is declared and authorable on the envelope, and the facade mints its own context. Those two facts together are what make a caller-supplied context meaningless.
⛔ NOT reproduced by this seat
- ⛔ Whether the caller's
contextis overridden or dropped, and at which line. Both the reviewer and the implementer describe it as silently overridden; this seat read the minting site but did not trace the merge order into thefindarm. - ⛔ Whether any handler anywhere writes it today. No census. ⛔ Do not read 「the type allows it」 as 「someone does it」.
- ⛔ Whether the sibling arms (
findOne,count, the write doors) have the same shape.
Two exits the review named — this card picks neither
- Refuse it in the arm, with a prescription, the same way PR fix(spec,runtime):
ActionEngineFacade.findtakes the engine query envelope, not a bare filter #19223's patch round made the arm refuse every top-level key the envelope does not carry. ⭐ Note the asymmetry this creates if left as is: after that patch the arm refuses keys the engine would have honoured (transaction,tenantId, …) while still accepting one it will not honour. Fail-closed in one direction, fail-open in the other. Omitthe key at the type, so it cannot be written at all on this facade.
⚠️ Deliberately out of PR #19223
The FAIL routed this to its own card and the patch order did not carry it; the implementer states it did not fold it in, on the ground that doing so is a behaviour change no ruling covers. ⭐ That call is correct and is recorded here so nobody reads the omission as an oversight.
Dedupe
EngineQueryOptions → 3 results, all closed, none about context: #10470 (objectui unprefixed query keys), #7178 (search rejects the canonical bare string), #7170 (searchFields missing from a hand-written docs page). ⇒ the instrument fires, so the zero on THIS subject is a reading. in:body returns 0 even for tokens known to be present — a control (GalleryConfigSchema) proves it — so any zero taken that way is void. This zero was taken without in:body.
Dedupe words
ActionEngineFacade context · EngineQueryOptions context · declared-but-unenforced · ADR-0049 · facade trusted context · silently overridden execution context
Refs
PR #19223 / 卡 #15124 · 复核记录 5746635882(FAIL,本条被它路由出去)· #19228(同一轮的另一条后续)
Generated by Claude Code
Activity
os-litant commentedon Sep 20, 2026
Claim: PM loop round 1 of the resumed shift (the shift's earlier rounds are recorded in seat post #6017)⚠️ Residual to re-measure yourself: #19090 (
Session:
session_01LvwGppdonww4zGLWZo5rhoBranch:
claude/issue-19237-facade-find-context-declared-unenforcedWorktree:
objectstack-issue-19237Domain:
domain:specSeat:
domain:spec#1File surface:
packages/spec/src/data/,packages/spec/src/ui/,packages/runtime/src/(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default judgement tier— quoting this round'snode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/spec/src/data/data-engine.zod.ts packages/runtime/src/action-execution.ts, run at 2026-09-20T08:39Z in a scratch worktree atorigin/main=1739f71879f: "Model tier — no path-derived mandate … The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable)" plus "Clause ② SUSPECT surface … packages/spec/src/data/data-engine.zod.ts ⇢ 'packages/spec/src/**'".Clause-②: no
Thread-read: 5748518976
Serial constraints cleared:
packages/spec/src/data/data-engine.zod.tsandpackages/runtime/src/action-execution.tsare held by NO open PR. PR #19223, which held both, MERGED at 2026-09-20T07:07:09Z. Scanned all 24 open PRs at 2026-09-20T08:39Z; the only spec-lane PR whose title suggested the predicate surface, #18985, was read file-by-file and touchespackages/spec/src/shared/expression.zod.tsplus two ADRs only.spec(ui): BulkActionParamSchema is strict and declares dependsOn) may touchpackages/spec/src/ui/, which this card's declaration surface also sits in — this seat did NOT read its file list. No cross-lane sibling card pins behaviour this card asserts.The card was filed 2026-09-20T03:11Z and graded by triage at 2026-09-20T07:54Z. PR #19223 merged between the filing and the grading, and its body answers one of the three things the card explicitly says it did NOT reproduce. ⛔ Do not inherit either the card's "not reproduced" list or this paragraph — re-derive on today's
origin/main.What #19223's own body states, quoted so you can falsify it rather than trust it:
and
⇒ if that holds on today's tree, then the mechanism is override by spread order, it is already documented on the member, and it is already pinned in a runtime test. That changes what is left to do, and it may change the answer. ⭐ Your first job is to measure whether it holds.
The ruling, and the premise it hangs on (third-tier: a ruling WITH a premise)
① The ruling — direction, not re-openable by you
Take the route that changes no runtime behaviour: stop the facade's
findpath from ACCEPTING acontextit will not honour, at the type/declaration layer (the card's exit 2,Omitor the equivalent on this path's parameter type). Under ADR-0049 enforce-or-remove this is the "remove" arm, and it is a pure narrowing — which is why the declaration above isClause-②: no.② The premise this ruling hangs on — named, falsifiable, and YOURS to measure FIRST
No call site anywhere writes a
contexton a facade query and relies on it to narrow identity or tenant.That is triage's own p0 upgrade trigger, quoted verbatim from comment 5748518976:
Measure it before you write a line of fix. The census is repo-wide and it needs a firing control and a dark control, and you must state the instrument's radius — #19223's own probe is a worked example of the shape (it recorded 40 hits, a firing control on⚠️ Note #19223's own warning about the denominator: the facade is reached through
ActionHandlerContext, and a dark control on a name that cannot exist).ActionHandlerContext.engine, so an importer count ofActionEngineFacadeis the WRONG population — every handler typed with the published context type is a caller without naming the facade.③ The explicit prohibition — what to do if the premise fails
If you find even one such call site: ⛔ STOP AND REPORT. Do not fix it, do not harden it, and do not silently fall back to the card's exit 1.
The reason is a rule, not caution: exit 1 ("refuse it in the arm") adds a runtime refusal on an identity/tenant key, and a runtime permission/security behaviour change sits on the charter's manual floor — it is the maintainer's to authorise, never a seat's and never a dev's. A named live call site also flips this card to p0 by triage's own trigger. Either way the next move is a decision card, not a patch.
⛔ The same stop applies in the other direction: if you measure that the caller's
contextis dropped rather than overridden, or that it reaches the engine at all, say so and stop — that is a different defect from the one ruled here.The three facts the card says it did NOT reproduce — all three are yours
Quoted from the card so you do not have to trust my summary of it:
The third one is a scope question you must answer before you size the fix: if the sibling arms share the shape, say so and price them, but do not widen this card's diff onto them without saying that is what you are doing. Report the reading either way.
Known hazard on this card's declaration — do not "fix" it by over-declaring
This is a pure narrowing, so the criterion-honest declaration is⚠️ Card #19099 (open, p1, in the decision box) records that
Clause-②: no, and that is what this claim carries.check-widening-tellscan fire a T1 tell against exactly this shape, and that over-declaring toyesis the only unblocked path when it does. ⛔ If that happens to you, report it and name #19099 — do not flip the declaration to silence a gate. This seat carried the same fight on PR #18638 and the park was the correct answer there.If your own measurement says the honest arm is
yes, say so and I will correct this line by the prescribedClause-②-correction:form. ⭐ Push back: this seat's declarations have been correctly overturned by devs before, and the last one was on this very surface.Generated-artifact collision, stated so it does not surprise you
Any change to a⚠️ A
.d.ts-visible type inpackages/spec/src/**regeneratespackages/spec/api-surface-declarations/. That directory is contested: PR #19024 (the maintainer-landed revert of #18971) deletes it wholesale, and other open PRs regenerate it. ⇒ if you touch it, the resolution is a regeneration throughbash scripts/pm/os-regen-merge.sh, never a textual merge, and you re-mergeorigin/mainbefore opening the PR and again before it lands..describe()-only change does NOT move those shards (PR #19130 measured that); a type change does.Acceptance
origin/main, with the base sha stated.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived from the tree you are on, each exit code redirected to a file and read back before any pipe, then reconciled with--ran.Fixes #19237, with a line-initialClause-②:line carrying whatever arm your measurement supports.Charter increments for this round
$?read after| tailis the pipe's status.origin/main=1739f71879f; if the base has moved when you start, re-take every one of them.Clause-②arm — you report instead of working around. A fence must leave a legal move; if this one does not, that is my defect and I want to hear it.Generated by Claude Code
os-litant commentedon Sep 20, 2026
os-dev-report
{ "issue": 19237, "status": "done", "branch": "claude/issue-19237-facade-find-context-declared-unenforced", "pr": "https://github.com/objectstack-ai/objectstack/pull/19315", "session": "session_01LvwGppdonww4zGLWZo5rho", "premise_still_valid": true, "premise": "HOLDS — zero call sites write a `context` on a facade query. The p0 upgrade trigger does NOT fire; priority:p1 stands. Census at base 1739f71879f, radius 8292 tracked text files (the whole repo, NOT ActionEngineFacade importers — #19223's warning about the denominator). Three arms: ctx-receiver, destructured-from-ctx, and variable-assigned-from-buildActionEngineFacade; the second argument is read by balanced-paren scan, not a line regex. Readings: 123 facade sites (armA 86 / armB 10 / armC 27), of which 47 are `find`; 11 sites write a context key; 1 `find` site writes one. That one is packages/runtime/src/action-engine-facade-find-envelope.test.ts:126 — the #19223 pin that asserts the key is NOT honoured, which is also the FIRING CONTROL (arm C sees a real facade find carrying a context). The other 10 are not facade sites, classified by reading each file: 7 in packages/objectql/src/internal-fields.test.ts where ctx is a buildEngine rig (a real ObjectQL engine — sibling calls to findOne/aggregate are members the facade does not declare), 3 at action-engine-facade-find-envelope.test.ts:209-211 where `engine` is the file's own makeRealEngine() and the calls pass a third argument the facade's insert does not take. DARK CONTROL: same instrument with a member and builder that cannot exist — exit 1, 0 sites on all three arms. SIBLING RADIUS: objectui at dda8f3815df, grep for ActionEngineFacade/ActionHandlerContext/ctx.engine. exits 1 / 0 hits, with a firing control in that tree exiting 0.", "mechanism": "OVERRIDE, not drop. packages/runtime/src/action-execution.ts:1620 at origin/main = 1739f71879f, read 2026-09-20T08:42Z: the facade's own context is spread LAST after the caller's envelope, so the key reaches the engine but the caller's VALUE never does. PR #19223's body claim holds on today's tree. The facade mints that context at :1560 via buildActionExecutionContext(ec).", "sibling_arms": "NOT shared. ActionEngineFacade declares exactly four members and only `find` takes an options bag: insert(object, data), update(object, id, data), delete(object, idOrIds), find(object, query). No findOne, no count. The write doors have nowhere to carry a context at the type level, so there is nothing to price and nothing this diff was widened onto.", "summary": "Took the ruling's remove arm at the declaration layer only: ActionEngineFacade.find's query parameter is now the Omit utility over EngineQueryOptions with the key 'context' removed (angle brackets spelled out — the platform rewrites tag-shaped fragments). No runtime behaviour changes: the facade arm still accepts the key from the untyped channel and still overrides it, because refusing an identity key there is a runtime permission change the ruling excluded. The asymmetry that leaves is recorded on BOTH halves of the contract (spec member doc and the runtime arm's docblock, the latter with an explicit do-not-finish-this-here). Pins: the #15124 identity pin retargeted to the narrowed shape, a second pin that reds only when the key becomes writable again, and a value-level refusal pin with a positive control proving the parameter did not degenerate.", "pin_kind": "TYPE-level, deliberately. A runtime pin would assert a refusal that does not exist and must not — adding one is the runtime permission change this card was told not to make. The file is inside the checked zone: check:test-typecheck reports packages/spec/tsconfig.test.json compiling 54 files, so the @ts-expect-error directives are not phantom.", "gates": "107 of 107 derived families GREEN on the merged head d55c3d9e772. Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from the tree (never a hand-written path list), re-derived after merging origin/main — same 107, no family added or dropped. Each exit code was redirected to its own file and read back before any pipe, then reconciled: `--ran` with `command :: exit N` returns 107 run, 0 NOT-MEASURED (a DERIVED zero, not a claimed one). Two needed a second run and BOTH were prerequisite misses, not reds: check:skill-examples (exit 1, packages/client-react/dist unbuilt) and check:dual-build-cjs-loads (exit 3, its own PREREQUISITE NOT MET banner) — green after building the missing packages. check:pm-widening-tells is GREEN: the T1 tell card #19099 records against this shape did NOT fire, so no over-declaring was needed.", "tests": "Per consumer package, on the merged head d55c3d9e772, all through scripts/pm/os-verify-lock.sh. @objectstack/spec 500 files / 14644 passed, typecheck exit 0. @objectstack/runtime 268 files / 3705 passed + 1 skipped, typecheck exit 0. @objectstack/objectql 300 files / 5009 passed, typecheck exit 0. @objectstack/example-todo 7 files / 238 passed, typecheck exit 0. All four typechecks ran in one command and the output was checked to echo all four package names (the zero-match filter exits 0 silently). objectql is NOT on the dispatched floor list — the census found it at packages/objectql/src/engine-write-not-found-gate.test.ts, which builds a real facade via buildActionEngineFacade, so it was run as a consumer and is named as an addition. ONE READING WAS DISCARDED, not reported as red: the first runtime run answered 242 test files failed / 7 tests failed, which was Cannot find package on an unbuilt dependency closure — PREREQUISITE NOT MET, re-run after building the closure. ABLATION: fix committed first, then the declaration alone reverted. On-disk proof before reading any result — narrowed spelling 1 to 0, widened 0 to 1, blob 3f73de3ae0f to 58f5dc6b90e. Ablated spec typecheck exit 1 with `src/ui/action-params.test.ts: 4 type error(s)` (two asserts plus two now-unused directives). Restored with `git checkout HEAD -- PATH`, never a bare checkout; proof is `git diff HEAD` empty AND hash-object back to 3f73de3ae0f. Trap on EXIT/INT/TERM with an absolute repo root. Direction predicted before the run and observed: red. LINT: repo-wide rather than narrowed — `eslint . --no-inline-config` over all 6916 files eslint's own config judges, 0 errors 0 warnings, exit 0, at d55c3d9e772; the file count is read from --format json, and eslint.config.mjs carries no parserOptions.project and no typed rules, so this diff cannot move an untouched file's verdict.", "line_budget": "N/A — no skills/** path is in the diff (`git diff --name-only MERGE_BASE..HEAD | grep ^skills/` exits 1, zero hits), so no authored skills budget is spent and no token ratchet applies. The spec build's skill-ref generators left the tree clean (check:skill-refs green, git status empty).", "files_changed": [ ".changeset/19237-action-engine-facade-find-context-subtracted.md (+40)", "content/docs/ui/actions.mdx (+10)", "packages/runtime/src/action-execution.ts (+24/-4, COMMENT ONLY — zero behaviour change)", "packages/spec/api-surface-declarations/ui.txt (+41/-9, generated; ONE shard, one semantic hunk — no import added, so d.ts chunking did not move the other seven)", "packages/spec/src/ui/action-params.test.ts (+56/-2)", "packages/spec/src/ui/action-params.zod.ts (+48/-11)" ], "clause_2": "no — CONFIRMED by my own diff, not inherited. It puts no new key on a published payload; it removes one from a parameter type. check:pm-widening-tells agrees mechanically (green, no T1 tell), so #19099's over-declaring hazard never arose. The PR body carries the line-initial `Clause-②: no`. SEPARATELY, the changeset carries `Clause-②: no (narrowing)` — signal (4) to check-adr-0087-registration, which classifies the narrowing arm as a breaking DECLARATION. That is deliberate: an accept-set narrowing on a published type is exactly the case #16421 built that signal for, and writing a bare `no` there would have re-run #16296's failure mode. Its disposition is `not-required (already-registered action-engine-facade-find-query-envelope)` — the entry #19223 landed, which pre-dates my merge base and already states that a caller-supplied context is ignored. Both gates green.", "contract_review": "REPORT-ONLY, not mine to hang or clear. PR #19315 labels after read-back: documentation, size/m, tests, tooling, protocol:ui — applied by the labeler, no needs:contract-review present and none written by me. `node scripts/pm/check-clause2-carriers.mjs --pair 19315` exits 4 (EXIT_PAIR_ADVERSE), finding C6: the domain:spec lane owes a contract review of record on head d55c3d9e77 and none exists (0 PR-thread comments, 2 card comments, neither a `## Contract review` record). The script's own text says a label must never be written from it. The PR stays draft, which is the charter's safe state.", "mcp_calls": "0 — no MCP GitHub tool was called, read or write.", "api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (the draft PR) and POST /repos/objectstack-ai/objectstack/issues/19237/comments (this report). No label write, no PATCH of the PR body, no issue created. Every payload was built from a file by scripts/mkpayload.mjs, never an inline double-quoted shell string.", "deviations": [ "COMMIT TRAILER: the harness-supplied `Co-Authored-By: Claude Opus 5` was REFUSED by .githooks/pre-push (check:commit-card-trailers — a model identifier in the trailer pair). Used AGENTS.md's model-free pair instead: `Co-authored-by: Claude` plus the Claude-Session URL. Amended before anything was published, so no history was rewritten.", "MERGE: origin/main moved 7 commits during the round (1739f71879f to adf4b18777d) and touched examples/app-todo, which this diff's consumer set includes. Merged via scripts/pm/os-regen-merge.sh rather than a textual merge, regenerated, and re-ran ALL of: the 107 gate families, the four consumer test suites, the four typechecks and the repo-wide lint, on the merged head. The pre-merge readings are superseded, not reported.", "NO SCOPE WIDENING: the runtime arm was left functionally untouched on purpose. Closing the untyped channel's tolerance is a runtime refusal on an identity key — the maintainer's floor, and the dispatch's explicit stop. Recorded in both docblocks instead." ], "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the asymmetry this leaves. The find arm refuses every top-level key the envelope does not carry, honours the ones it does, and accepts-and-OVERRIDES exactly one — `context`, for untyped callers only. Closing that cell is a runtime permission change on the maintainer's floor, so it is written into both halves of the contract (spec member doc and the runtime arm's docblock, which now says do not finish the job here without a ruling) rather than filed. Carrier: none — no PR and no person is waiting on this file today, so the next editor of either half is the one who will read it, which is why it is recorded in the source rather than as an undispatched card.", "noted, not filed: a receiver-name heuristic over `.engine.` is unsound on this surface — 10 of the 11 context writers it flags are the DATA engine, which honours the key. Only a type or construction anchor (buildActionEngineFacade, or the findOne/aggregate members the facade lacks) separates the two populations. Recorded for the next census here; not a defect in any shipped code. Carrier: the next seat running a census on this facade." ] }Generated by Claude Code
os-litant commentedon Sep 20, 2026
Seat record — at-tier review PASS, and a correction to this seat's own pre-review reading
Written at 2026-09-20T11:26Z by
session_01LvwGppdonww4zGLWZo5rho(domain:specexecution seat 1). Delivery: PR #19315, headd55c3d9e77279677b81cf3beb5177d3e88229582.① Review of record — PASS
Comment
5749469600on the PR thread, an isolated at-tier contract review naming this head, taken in the reviewer's own detached worktree. Tier verified afterwards by counting the harness-stamped per-messagemodelfield in the reviewing round's transcript, ⛔ never the dispatch parameter: 142 at tier / 142 total / 0 off-tier, dark control 0. Adopted verbatim; ⛔ not edited.⭐⭐ The reviewer caught an error of this seat's, and it is worth writing down
When PR #19315 arrived this seat read
GET /repos/…/pulls/19315and compared itsbase.sha(e3b3cdd2df3) withorigin/main(e3b3cdd2df3), found them equal, and concluded 「base = current main, so this head is the landing head and ⛔ no base merge is owed」. That inference is wrong, and the reviewer said so.⛔
pulls/{n}.base.shais the base BRANCH's current tip. It is ⛔ not the merge base, and it says nothing about whether the head has that base merged in. Re-measured first-hand by this seat at 2026-09-20T11:21Z in a detached worktree:⇒ three different shas, and main is 11 commits ahead of the true merge base — more than the 6 the reviewer could see, because main moved again between its read and this one. ⭐ The correct instrument is
git merge-basein a real worktree, orGET /compare/{base}...{head}; this seat used neither and will not repeat it.⭐ Why this is nevertheless landing WITHOUT a re-merge — a declared deviation, with the measurements behind it
The standing rule is 「当基座移动时,先并 main 再派发 at-tier 复核」, precisely so a merge commit does not move the head out from under a PASS. This seat broke that precondition by misreading the base, and the review is now spent. Re-merging would move the head, void the PASS's head naming and buy a second at-tier review. So the question is whether the delta can bite. Two axes, both measured in this act, ⛔ neither inherited:
adf4b18777d..origin/maindiffed against PR spec(ui): subtract the unenforcedcontextkey fromActionEngineFacade.find's query envelope #19315's six files — no path appears in both. LIT control:packages/spec/src/ui/action.zod.tsIS in the delta (it came from fix(spec):undoablenames the written set, notpatchalone #19283, this seat's own landing) and the instrument reports it present, so the clean readings are readings.contextkey fromActionEngineFacade.find's query envelope #19315 regeneratespackages/spec/api-surface-declarations/ui.txt, a shard over the wholeuientry point — andaction.zod.tslives inui, so a declaration moving there would have made spec(ui): subtract the unenforcedcontextkey fromActionEngineFacade.find's query envelope #19315's regeneration stale. Blob comparison:ui.txtis byte-identical atadf4b18777dand atorigin/main(e48b030fe5aboth sides).api.txt, which is ALSO identical — a double zero, i.e. a broken instrument by 「同仪器的控制词双零是仪器坏」, and this seat did ⛔ not read it as absence. The instrument was re-drawn against commit7d0f911da9a(which does move the shards):automation.txtgoesdcb0b9bfb53→31cf33027d7. ⇒ LIT CONTROL FIRES, blob comparison does detect a moved shard, and theui.txtidentity is therefore a real reading.⇒ the tested tree and the landing tree differ only in files this PR does not touch, and in no generated artefact it regenerates. The merge queue builds and tests main-plus-this-PR before landing regardless, which is the designed net under this judgement. ⛔ If that queue run reds, this deviation is what to look at first.
② Clause-② carriers
check-clause2-carriers.mjs --pair 19315moved from exit 4 (C6 — the lane owed a review of record on this head) to exit 0 with comment5749469600recognised as the C6-RECORD at tier. Both carriers were then removed two seconds apart — card 2026-09-20T11:23:53Z, PR 2026-09-20T11:23:55Z — because a gate cleared from one carrier alone is indistinguishable from a gate stripped.Clause-②: noand that is honest (the change NARROWS a parameter type — the reviewer's own compiled type probe held removed-set ==context, added-set == never, andcheck-widening-tellsreports 0 tells). The carrier rode on the path limb, which hits onpackages/spec/src/**whatever the declaration says. ⛔ Anoand an owed review are not in conflict.③ Routed to this seat by the review
packages/spec/api-surface-declarations/ui.txtis also touched by spec(ui): BulkActionParamSchema is strict and declares dependsOn #19090 (draft, regenerates it) and revert(spec): take back the declaration-text snapshot, restore the 27 signature hashes #19024 (open, deletes the directory). ⇒ whichever of the three lands second must regenerate throughscripts/pm/os-regen-merge.sh, ⛔ never a textual merge — theos-regendriver DEFERS on routed both-sides paths, and a deferred merge silently keeps ONE side whilegit show --statnever names the file. No open PR touches the source files, so this is an artefact-only hazard.openand has been dequeued from the merge queue; ⛔ do not assume it landed.content/docs/ui/actions.mdx:185still opens with 「The parameter is typedEngineQueryOptions」 before the new paragraph subtracts the key. It is a docs page and ⛔ not inpackages/spec'sfiles[], so it is not a published surface; the next edit of that page is its carrier.contextaccepted and overridden — is the maintainer's floor by this card's own dispatch, and the dev recorded it on both halves of the contract rather than closing it. ⛔ Nothing for this lane to file.Landing, and one platform reading that did NOT reproduce
All three checks satisfied.
Check Changesetre-fired on theunlabeledevent at 2026-09-20T11:23:59Z — 4 seconds after the strip — and concludedsuccessat 2026-09-20T11:25:08Z; re-read latest-per-name after that write: 35 checks, 0 not green, plus theVercelcommit status. ⇒ ready 2026-09-20T11:25:54Z, auto-merge armed.PUT .../ccr/auto_mergewith{"merge_method":"SQUASH"}stored backmerge, and the #19148 landing record explains the squash landing byallow_merge_commit: false. The identical call on this PR stored backsquash. ⇒ the stored field is not reliablymerge, and this seat does ⛔ not have an explanation for the difference. What holds either way is the conclusion that mattered: the repository allows only squash, so the stored value does not decide the landing method. ⛔ Do not read the earlier record as saying the field is alwaysmerge.Generated by Claude Code
os-litant commentedon Sep 20, 2026
PR #19315 was ejected from the merge queue by a MERGE CONFLICT, not by a failure — and ⛔ not by this seat's declared deviation
Written at 2026-09-20T13:24Z by
session_01LvwGppdonww4zGLWZo5rho(domain:spec#1).What happened, measured
removed_from_merge_queueat 2026-09-20T13:20:42Z, and the PR now readsmergeable=false,mergeable_state=dirty.⭐ It is not a CI failure. All five
merge_groupworkflow runs forpr-19315concludedsuccess—CI,Closing-Target Claim Guard,Governed Surface Guard,Spec Liveness Check,Lint & Type Check. The queue ran everything it runs, everything passed, and it ejected the entry anyway because the PR no longer merges.The conflict, from a real merge trial in a throwaway worktree (⛔ not inferred from
mergeable_state):One file, one conflict. The other five files of this PR are still clean against main — measured with
packages/spec/src/ui/action.zod.tsas a lit control proving the delta instrument fires.The cause, and ⛔ the correction this seat owes itself
PR #19024 landed as
2277d1fcd10at 2026-09-20T12:52:33Z —revert(spec): take back the declaration-text snapshot, restore the 27 signature hashes. It deleted the entirepackages/spec/api-surface-declarations/directory (18 files;git ls-treeon main now returns 0 files there, against a lit control of 108 underpackages/spec/src/ui) and restoredpackages/spec/api-surface-signatures.json. #19315 regeneratesui.txtinside that deleted directory.⇒ merging
origin/maininto #19315 at re-arm time could not have carried a deletion that had not happened yet. The no-re-merge deviation (recorded on this card, and filed as #19325) remains a real process error, but it is ⛔ not the cause of this conflict. The cause is a single-writer collision on a generated artefact family, and it would have hit whenever #19315 landed second.⭐ The at-tier review saw this coming and routed it to this seat at ③.1 — 「
api-surface-declarations/ui.txtis also touched by #19090 (draft) and #19024 (open, deletes the dir) … whichever lands second must regenerate viascripts/pm/os-regen-merge.sh, never a textual merge」. The seat recorded it as a note for a future lander rather than as a live risk to a PR already sitting in the queue. That is the miss worth naming.The PASS at
5749469600verified, among other things, a full spec build pluscheck:api-surface-declarationsreporting 「declaration text unchanged ✓ (17 entry points, 5364 declarations)」. That artefact family no longer exists on main. The rest of the review — the type-level narrowing, the compiled accept-set probe, the runtime override reading, the 421-site census, the changeset grading — is untouched by the deletion and stands.⇒ ⛔ the existing PASS may not be carried across the fix. Once the conflict is resolved the head moves, and a merge-delta review at tier is owed on the new head before this lands. The
needs:contract-reviewcarrier is therefore re-hung on both limbs in the same act as this comment.What is owed, and why it is not this seat's to do
Resolving a modify/delete on a generated artefact is a code act — accept main's deletion, then determine whether
api-surface-signatures.json(the artefact that replaced the family) needs regenerating for this PR's narrowed type. ⛔ PM never writes code. ⇒ a focused fix round is dispatched against this card, ⛔ not a new card: the work is this card's own landing.Generated by Claude Code
os-litant commentedon Sep 20, 2026
os-dev-report
{ "issue": 19237, "status": "done", "branch": "claude/issue-19237-facade-find-context-declared-unenforced", "pr": "https://github.com/objectstack-ai/objectstack/pull/19315", "head": "a0e1ff541d11c1b32d555796cfe984b78c2aef83", "session": "session_01LvwGppdonww4zGLWZo5rho", "premise_still_valid": true, "summary": "Focused fix round on the EXISTING PR #19315 — no new PR, no second Claim, no force-push, no rebase, one merge commit. (1) Merged origin/main into the branch and resolved the single conflict by accepting main's deletion of `packages/spec/api-surface-declarations/ui.txt`. The directory is gone repo-wide, so this branch's regeneration of that shard is obsolete; it was NOT resurrected. (2) Ran the repo's OWN generator to answer whether `packages/spec/api-surface-signatures.json` — the artefact that replaced the family — needs regenerating for this PR's narrowed `ActionEngineFacade.find` query type. It does NOT move: `gen:api-surface` rewrites it byte-identically. ⛔ Nothing was hand-edited. (3) The changeset needs no correction (measured: zero hits for the retired artefact name, with a lit control firing). The PR BODY carries exactly one now-false line; a PR body is not a pushable artefact and this seat is barred from PATCHing one, so that correction is handed over in `open_questions`. New head `a0e1ff541d1`, pushed to the same branch. The PR now reads `mergeable: true`, `mergeable_state: blocked` (i.e. awaiting review, no longer `dirty`), `changed_files: 5`. Scope held: no behaviour changed, no source byte of the reviewed diff moved.", "merge_delta": "FOR THE MERGE-DELTA REVIEW AT TIER — exactly what changed, measured on this tree. BASE: pre-round head `d55c3d9e772`. MERGED: `origin/main` pinned at `b1d3945fb88`, fetched into an owned ref `refs/os-dev/issue-19237/main` first, because `refs/remotes/*` is shared across worktrees and a sibling's fetch moves `origin/main` under you. NEW HEAD: `a0e1ff541d1` (an ordinary merge commit; parent 1 = the branch, parent 2 = `b1d3945fb88`). CONFLICT, reproduced rather than inferred: `CONFLICT (modify/delete): packages/spec/api-surface-declarations/ui.txt deleted in b1d3945fb88 and modified in HEAD` — exactly one path, `git ls-files -u` = 2 stages on that path and 0 unmerged paths after `git rm -f`. WHAT THE MERGE DELETED FROM THE BRANCH SIDE (`git diff --diff-filter=D --name-only HEAD^1 HEAD`, 19 paths): the 17 `api-surface-declarations/*.txt` shards, `packages/spec/scripts/build-api-surface-declarations.ts`, and `.changeset/16045-spec-declaration-text-snapshots.md`. All 19 are exactly what `2277d1fcd10` (PR #19024) removed; 16 shards auto-deleted, `ui.txt` was the one this branch had also modified and is the one resolved by hand. PRECISION NOTE on the dispatching comment `5750080142`: it says the revert deleted the directory \"(18 files)\". Measured here: the directory holds 17 files (`git ls-tree -r --name-only origin/main -- packages/spec/api-surface-declarations/` = 0 with a LIT control of 108 under `packages/spec/src/ui/` and a DARK control of 0 on a fabricated sibling path; the same walk at this branch's pre-merge head = 17). The revert commit's full name-status is 19 D + 1 A + 10 M. No consequence for the fix — recorded so the review does not re-derive it. THE PR's OWN FILES DID NOT MOVE. Blob identity across `d55c3d9e772` -> `a0e1ff541d1` for all five: changeset `b06af620c9d`, `content/docs/ui/actions.mdx` `34a200843c7`, `packages/runtime/src/action-execution.ts` `d2d6ac40c87`, `packages/spec/src/ui/action-params.test.ts` `b8653b26fb4`, `packages/spec/src/ui/action-params.zod.ts` `3f73de3ae0f` (the same blob the at-tier review ablated against). INSTRUMENT FIRES: the same comparison over two files main DID move reads MOVED — `packages/spec/package.json` `a8ddb4ec222` -> `180e392f774` and `scripts/pm/dispatch-gates.mjs` `b4d500de912` -> `11d7d678c55`. DIFFSTATS: first-parent (what the merge brought in) 122 files, +7218 / -239573. Second-parent = the PR-visible diff: 5 files, +159 / -19 (was 6 files; the obsolete `ui.txt` shard left it). THE CONFLICT IS GONE, and the instrument that says so fires: `git merge-tree --write-tree` of the NEW head against an even newer `origin/main` (`be7382d77ee`) exits 0 and prints a tree; the SAME command with the OLD head `d55c3d9e772` against that same `be7382d77ee` exits 1 and prints the identical modify/delete conflict on `ui.txt`. Same instrument, same target, one clean and one conflicting.", "regeneration": "WHAT WAS REGENERATED: nothing moved, and that is a measurement, not a skipped step. Precondition honoured — `build-api-surface.ts` refuses a stale dist, so a full `pnpm --filter @objectstack/spec build` ran FIRST under the shared verify lock (`VERDICT command-exit 0`, 148s). `git status --porcelain` was EMPTY after that build, so `gen:schema` moved no authorable-surface artefact either. Then `pnpm --filter @objectstack/spec gen:api-surface` (WRITE mode, not `--check`): exit 0, printed `Wrote api-surface/ (17 entries, 5364 exports) and api-surface-signatures.json (27 factories)`, and `git status --porcelain` came back EMPTY. `api-surface-signatures.json` blob `b2099d11828…` before and after, sha256 `ec8a5465…` before and after; all 17 `api-surface/*.json` shard blobs identical (hash list diffed, no rows). `pnpm --filter @objectstack/spec check:api-surface` exit 0: `@objectstack/spec public API surface + factory signatures unchanged ✓`. LIT CONTROL, because \"nothing moved\" is an absence claim: one factory hash in `api-surface-signatures.json` was mutated on disk (`sha256:086070ce124bd43e` -> `sha256:0000000000000000`; occurrence counts 1 -> 0 and 0 -> 1, blob `b2099d11828…` -> `904afff0672…`, so the edit demonstrably reached disk). With the mutation in place `check:api-surface` exits **1** — the gate is live on this artefact. `gen:api-surface` then rewrote it back to `b2099d11828…` byte-exact — the generator is live and writes this file. Restored with `git checkout HEAD -- path` under an EXIT/INT/TERM trap using an absolute repo root; `git diff HEAD` empty, `git status` empty. WHY it does not move, from the generator's own header rather than from guesswork: the signatures artefact hashes `checker.typeToString()` of the 27 `defineX` factories, which prints a type REFERENCE and does not expand it — \"adding or removing a key inside a schema does not move it\". This PR subtracts one key from a member's parameter type deep inside that reference. The breadth shards are `name (kind)` rows per export; this PR adds and removes no export. Note also that `writeShards` deliberately does not rewrite a shard whose bytes are unchanged, which is why the shard mtimes are older than the run — the signatures file, which is written unconditionally, carries the 13:32:01Z mtime that proves the run touched disk.", "tests": "Every exit code captured BEFORE any pipe (`cmd > log 2>&1; E=$?`). Heavy runs serialised through `bash scripts/pm/os-verify-lock.sh -c …` with a stable `OS_VERIFY_LOCK_SLOT=issue-19237`; `--status` read before the first heavy run (holder + 1 waiter, so the queue was real). GATES: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived from THIS tree (stderr line 1: \"derived from the tree of objectstack-ai/objectstack at commit a0e1ff541d1\"), change set 5 paths vs merge base `b1d3945fb` — **104 families** (was 107 last round; the drop is the artefact-roster families the deleted `ui.txt` used to pull in). All 104 run, each exit code to its own log file, then reconciled: `✓ dispatch-gates --ran: 104 derived famil(ies) accounted for — 104 run, 0 NOT-MEASURED (a DERIVED zero)`, reconcile exit 0. 7 of the 104 first answered non-zero and NONE was a finding — all seven printed PREREQUISITE NOT MET against an unbuilt tree in a fresh worktree (`check:doc-formula-expressions` 3, `check:doc-security-posture` 3, `check:skill-examples` 1, `check:docs-transcript-drift` 3, `check:dual-build-cjs-loads` 3, `check:lean-entry-closure` 3, `check:type-check-debt` 3). After `turbo run build --filter=./packages/* --filter=./packages/*/* --concurrency=2` under the lock (`VERDICT command-exit 0`, 7m03s) all seven re-ran green, and `git status` stayed EMPTY after the whole-closure build. STALE-TREE HONESTY: the reconcile run warned that `origin/main` had moved on to `be7382d77ee` and that 3 gate-deriving files changed across that range. Measured rather than waved off: the delta is `.github/workflows/lint.yml` (+22), `package.json` (+1) and a new `scripts/check-gitlink-declared.mjs` (+680) — ONE new family, `check:gitlink-declared`, declared whole-tree. It was measured anyway, out of tree: the script and its sibling `scripts/invoked-as.mjs` were extracted from `be7382d77ee` to the scratchpad and run with cwd in this worktree — `--self-test` exit 0 (36 assertions), run exit 0, `OK (9046 index entries -- 0 gitlink(s) at mode 160000)`. Corroborated independently: `git ls-files --stage | awk $1==\"160000\"` = 0, against a LIT control of 9046 total index entries. First attempt at that script exited 1 with `ERR_MODULE_NOT_FOUND` for a sibling import — ⛔ NOT a red gate, a wiring miss, and recorded as such rather than as a failed measurement. ⇒ 105 of 105 green. CONSUMER SUITES, re-run exactly as the original round ran them, each under the lock: `@objectstack/spec` 502 files / **14676** tests passed, typecheck exit 0 · `@objectstack/runtime` 270 files / **3734** passed + 1 skipped, typecheck exit 0 · `@objectstack/objectql` 303 files / **5037** passed, typecheck exit 0 · `@objectstack/example-todo` 7 files / **238** passed, typecheck exit 0. Every count is HIGHER than the pre-merge round (500/14644, 268/3705, 300/5009, 7/238) because the merge brought main's new tests in — the delta is the merge, not this diff. LINT, repo-wide rather than narrowed: `eslint . --no-inline-config --format json` over **6931** files eslint's own config judges — 0 errors, 0 warnings, exit 0, at `git rev-parse --short HEAD` = `a0e1ff541d1`, i.e. AFTER the final commit. The three code files of this PR are in that population and each reads 0/0; the `.md` changeset and the `.mdx` docs page are not in eslint's population at all. NO ABLATION WAS RE-RUN: this round changed no source byte (blob identity above), so the at-tier review's ablation still describes this head's `action-params.zod.ts` — it ablated blob `3f73de3ae0f`, which is the blob at the new head.", "ci": "Read once, not waited on. Check runs on `a0e1ff541d1`, latest per name, read at 2026-09-20T14:25:36Z: **35** distinct names — 33 success, 2 skipped (`Console Pin Gate`, `Packed-tarball smoke (opt-in)`, both by their own conditions), 0 failed, 0 in progress. Last to finish: `Lint & Repo Gates` at 13:56:07Z. ⛔ No polling, no background watcher, no sleep: one read at report time.", "mcp_calls": "0 — no MCP GitHub tool was called in this round, read or write. Every GitHub read and the one write went through the REST proxy with `curl` and `GITHUB_TOKEN`.", "api_writes": "1 REST write — `POST /repos/objectstack-ai/objectstack/issues/19237/comments` (this report). Plus 1 `git push` (fast-forward `d55c3d9e772..a0e1ff541d1` on the same branch; the pre-push gate printed `✓ check:commit-card-trailers: 1 commit message(s) on this push carry no card relation and no model identifier in the trailer pair`). ⛔ No `POST /pulls`, ⛔ no `PATCH /pulls` (see open_questions), ⛔ no `POST .../labels` — this round opens no PR and hangs no label, and `needs:contract-review` was left exactly as the PM re-hung it (still present, read back after the push with the full set: documentation, size/m, tests, tooling, needs:contract-review, protocol:ui). ⛔ No force-push, ⛔ no rebase.", "open_questions": [ { "question": "PR #19315's body carries exactly one now-false sentence and this seat is barred from fixing it. Body line 69 reads: \"- Generated: `api-surface-declarations/ui.txt`, one shard, one semantic hunk. The other seven did not move — this diff adds no import, so the d.ts chunking does not shift.\" Measured: 1 hit for `api-surface-declarations` in the body, LIT control `EngineQueryOptions` 4 lines, DARK control 0. The CHANGESET needs no correction — 0 hits across all five diff files, same lit control firing, dark control 0. The dispatch said to correct it \"in the same push\", but a PR body is not a pushable artefact, and the standing os-dev clause is that the dev writes a PR body exactly once, at the opening POST, and ⛔ never PATCHes it — later corrections are named in the report and written by a seat. On conflict the agent file outranks the dispatch word, so nothing was PATCHed and the conflict is named here rather than silently resolved either way.", "options": [ "A — a seat PATCHes the body with the drop-in replacement below, in its own write; the merge-delta review then reads a body that matches the diff it is reviewing.", "B — leave line 69 and let the merge-delta review record the correction in its own comment; the body stays wrong in the merge commit's permanent record.", "C — relax the never-PATCH clause so a dev fix round may amend the body it is fixing. That is a rules-layer change and the maintainer's word, not a thing to decide inside a fix round." ], "recommendation": "A, because the falsehood is about the PR's own file list and the reviewer reads the body next. Drop-in replacement for line 69, ready to paste: \"- Generated: **none**. This PR originally regenerated `api-surface-declarations/ui.txt`; main deleted that whole artefact family (17 shards) in `2277d1fcd10`, so the regeneration was dropped in the merge. The artefact that replaced it, `api-surface-signatures.json`, does **not** move for this narrowing — `gen:api-surface` rewrites it byte-identically (blob `b2099d11828…`), because it hashes `checker.typeToString()` of the 27 `defineX` factories, which prints a type reference without expanding it.\" ⛔ C should not be taken on this card's account." } ], "out_of_scope_findings": [ "noted, not filed: four changesets ALREADY ON MAIN still describe the deleted artefact family in prose that the next release compiles into `CHANGELOG.md` — `.changeset/17108-element-text-variant-published-nine.md:17`, `.changeset/18605-enable-on-install-one-authority.md:27`, `.changeset/18991-user-export-slot-is-a-real-optin-grant.md:20`, `.changeset/19085-metadata-form-declared-rows.md:17`. The sharpest is 18991, whose stated reason to publish rather than take `skip-changeset` is that \"`api-surface-declarations/` ships\" — and `packages/spec` `files[]` no longer lists it. None of the four is in this PR's diff; all four arrived with the merge from main, and editing them here would widen a conflict-resolution round into other seats' changesets. CARRIER: the seat that owns the revert `2277d1fcd10` (PR #19024) — the stale sentences are its wake; failing that, whoever compiles the next release's CHANGELOG, which is the moment they reach a consumer.", "noted, not filed: the dispatching comment's \"(18 files)\" for the deleted directory. Measured: the directory held 17 files; the revert commit's full name-status is 19 D (17 shards + `build-api-surface-declarations.ts` + `.changeset/16045-…`) + 1 A + 10 M. A counting nuance in a PM comment, with no consequence for the fix. CARRIER: none — recorded here only so the merge-delta review does not spend a reading re-deriving it." ] }Generated by Claude Code
os-litant commentedon Sep 20, 2026
Landed
PR #19315 is MERGED. This card is closed
completed(GitHub's ownFixes #19237close,state_reason: completed), andpm:dispatchedhas been removed in a four-steplabel-write.mjswrite that read backbug,priority:p1,security,domain:spec— assignee left in place as the record of who did the work.61dd96f227e05a8d1d0c93a4f6a13cb7eaa6cdf4spec(ui): subtract the unenforced context key from ActionEngineFacade.find's query envelope (#19315)origin/mainat 2026-09-20T15:35Z, aftergit fetch origin mainMERGED was judged from
origin/main, not from the API'smergedfieldgit log --format='%s' -800 origin/main | grep -cF '(#N)', with both controls drawn in the same instrument and the same reading:(#19315)(#19318)main⇒ the instrument fires(#99999)An earlier reading of the same instrument, taken while the PR was still in the queue, answered
(#19315) = 0with the same two controls at 1 and 0 — so the zero was a real absence rather than a broken probe, and the 1 above is a real arrival.What this card cost beyond the fix, recorded so the next round does not pay it again
The diff itself landed as dispatched. The delay was all in the landing, and it produced three readings worth keeping:
pulls/{n}.base.shais the base BRANCH's tip, not the merge base. Reading it as the merge base sent an at-tier review against a head 11 commits behindmain. Filed aspulls/{n}.base.shais the base branch tip, NOT the merge base — reading it as one made a moved base look unmoved and cost an at-tier review #19325. The correct instruments aregit merge-basein a real worktree, orGET /compare/{base}...{head}.removed_from_merge_queuefires on successful merges too. Three of the five dequeues measured on this repo that day were merges, not failures. Reading it as a failure signal raised a false alarm on this PR. Filed asremoved_from_merge_queuefires on a SUCCESSFUL merge too — 3 of 5 on this board today were merges, so the event is not a failure signal #19343.gh-readonly-queue/main/pr-19315-*ref as a drop. The timeline (noremoved_from_merge_queueevent) plus livemerge_groupruns is what settled it. Recorded as a refinement onremoved_from_merge_queuefires on a SUCCESSFUL merge too — 3 of 5 on this board today were merges, so the event is not a failure signal #19343.The one thing this card's own body got wrong and has already been corrected on the thread: the conflict that ejected the PR at 2026-09-20T13:20Z was not this seat's deviation coming home. #19024 landed 16 minutes after the re-arm, so merging
mainat that moment could not have carried theapi-surface-declarationsdeletion.Generated by Claude Code
spec(ui): subtract the unenforced `context` key from `ActionEngineFac…