Skip to content

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

Closed

Description

@os-bill

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.

⚠️ Attribution split. Two facts this seat verified first-hand at 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 context is 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 the find arm.
  • ⛔ 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

  1. Refuse it in the arm, with a prescription, the same way PR fix(spec,runtime): ActionEngineFacade.find takes 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.
  2. Omit the 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. ⚠️ Instrument note: the query shape 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

  1. self-assigned this
    on Sep 20, 2026
  2. os-litant commented on Sep 20, 2026

    @os-litant
    Contributor

    Claim: PM loop round 1 of the resumed shift (the shift's earlier rounds are recorded in seat post #6017)
    Session: session_01LvwGppdonww4zGLWZo5rho
    Branch: claude/issue-19237-facade-find-context-declared-unenforced
    Worktree: objectstack-issue-19237
    Domain: domain:spec
    Seat: domain:spec#1
    File 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's node 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 at origin/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.ts and packages/runtime/src/action-execution.ts are 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 touches packages/spec/src/shared/expression.zod.ts plus two ADRs only. ⚠️ Residual to re-measure yourself: #19090 (spec(ui): BulkActionParamSchema is strict and declares dependsOn) may touch packages/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.


    ⚠️ Read the card and BOTH of these before you touch anything — the card's premise has MOVED since it was filed

    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:

    The one key worth calling out is context: the envelope admits it because every engine option bag does, but this facade is trusted and context-less by design, so a caller-supplied context is overridden, not honoured. Documented on the member and pinned in the runtime test, because it is a security-shaped property of a spread ORDER.

    and

    context is still spread last, so the facade's own elevated ExecutionContext wins over a caller-supplied one.

    ⇒ 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 find path from ACCEPTING a context it will not honour, at the type/declaration layer (the card's exit 2, Omit or 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 is Clause-②: no.

    ② The premise this ruling hangs on — named, falsifiable, and YOURS to measure FIRST

    No call site anywhere writes a context on 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:

    ⚠️ p0 upgrade trigger, checkable: measure any call site that writes a context on a facade query and relies on it to narrow identity or tenant. If one exists, this is a live isolation defect rather than a latent one ⇒ p0 immediately. ⛔ 「It could happen」 is not the trigger; a named call site is.

    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 ActionHandlerContext, and a dark control on a name that cannot exist). ⚠️ Note #19223's own warning about the denominator: the facade is reached through ActionHandlerContext.engine, so an importer count of ActionEngineFacade is 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 context is 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:

    • ⛔ Whether the caller's context is 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 the find arm.
    • ⛔ 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.

    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 Clause-②: no, and that is what this claim carries. ⚠️ Card #19099 (open, p1, in the decision box) records that check-widening-tells can fire a T1 tell against exactly this shape, and that over-declaring to yes is 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 prescribed Clause-②-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 .d.ts-visible type in packages/spec/src/** regenerates packages/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 through bash scripts/pm/os-regen-merge.sh, never a textual merge, and you re-merge origin/main before opening the PR and again before it lands. ⚠️ A .describe()-only change does NOT move those shards (PR #19130 measured that); a type change does.

    Acceptance

    • The census above, with its instrument, its radius, a firing control and a dark control, and its exit codes captured before any pipe.
    • The mechanism (override vs drop) traced to a named line on today's origin/main, with the base sha stated.
    • If the premise holds: the narrowing, plus a pin that reds if the key becomes acceptable again. State whether the pin is a type-level test or a runtime one and why.
    • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived from the tree you are on, each exit code redirected to a file and read back before any pipe, then reconciled with --ran.
    • PR opened as a draft, first line Fixes #19237, with a line-initial Clause-②: line carrying whatever arm your measurement supports.

    Charter increments for this round

    1. ⛔ Do not leave a second claim comment — this one is the identity. Check that the newest claim on this card names your branch; if it does not, stop and report.
    2. ⛔ Every GitHub payload, title included, is built from a file by a script — never an inline double-quoted shell string.
    3. ⛔ Capture every exit code BEFORE any pipe. A $? read after | tail is the pipe's status.
    4. ⭐ All readings state their base sha and the clock they were read at. This dispatch's own readings were taken at 2026-09-20T08:39Z against origin/main = 1739f71879f; if the base has moved when you start, re-take every one of them.
    5. ⭐ Anything in this order you measure to be false or impossible — including the ruling's premise and the 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

  3. os-litant commented on Sep 20, 2026

    @os-litant
    Contributor

    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

  4. os-litant commented on Sep 20, 2026

    @os-litant
    Contributor

    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:spec execution seat 1). Delivery: PR #19315, head d55c3d9e77279677b81cf3beb5177d3e88229582.

    ① Review of record — PASS

    Comment 5749469600 on 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-message model field 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/19315 and compared its base.sha (e3b3cdd2df3) with origin/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.sha is 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:

    origin/main                                 596090efbe7e66ae264a691e40409d0db21805a4
    PR head                                     d55c3d9e77279677b81cf3beb5177d3e88229582
    git merge-base origin/main HEAD             adf4b18777d507236cd24b7ed59b45a7c71bd1fd
    pulls/19315.base.sha                         e3b3cdd2df3bda349ef7a41b0de39c8de4fddc87
    git merge-base --is-ancestor origin/main HEAD  ⇒ NO
    

    ⇒ 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-base in a real worktree, or GET /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:

    1. File overlap: ZERO. All 11 commits of adf4b18777d..origin/main diffed against PR spec(ui): subtract the unenforced context key from ActionEngineFacade.find's query envelope #19315's six files — no path appears in both. LIT control: packages/spec/src/ui/action.zod.ts IS in the delta (it came from fix(spec): undoable names the written set, not patch alone #19283, this seat's own landing) and the instrument reports it present, so the clean readings are readings.
    2. Generated-shard staleness: NONE. spec(ui): subtract the unenforced context key from ActionEngineFacade.find's query envelope #19315 regenerates packages/spec/api-surface-declarations/ui.txt, a shard over the whole ui entry point — and action.zod.ts lives in ui, so a declaration moving there would have made spec(ui): subtract the unenforced context key from ActionEngineFacade.find's query envelope #19315's regeneration stale. Blob comparison: ui.txt is byte-identical at adf4b18777d and at origin/main (e48b030fe5a both sides). ⚠️ The first control for this reading was 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 commit 7d0f911da9a (which does move the shards): automation.txt goes dcb0b9bfb53 → 31cf33027d7. ⇒ LIT CONTROL FIRES, blob comparison does detect a moved shard, and the ui.txt identity 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 19315 moved from exit 4 (C6 — the lane owed a review of record on this head) to exit 0 with comment 5749469600 recognised 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.

    ⚠️ For the record on why the carrier was hung at all: the card declares Clause-②: no and that is honest (the change NARROWS a parameter type — the reviewer's own compiled type probe held removed-set == context, added-set == never, and check-widening-tells reports 0 tells). The carrier rode on the path limb, which hits on packages/spec/src/** whatever the declaration says. ⛔ A no and an owed review are not in conflict.

    ③ Routed to this seat by the review

    1. Generated shard collision, for whoever lands next. packages/spec/api-surface-declarations/ui.txt is 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 through scripts/pm/os-regen-merge.sh, ⛔ never a textual merge — the os-regen driver DEFERS on routed both-sides paths, and a deferred merge silently keeps ONE side while git show --stat never names the file. No open PR touches the source files, so this is an artefact-only hazard. ⚠️ revert(spec): take back the declaration-text snapshot, restore the 27 signature hashes #19024 is currently open and has been dequeued from the merge queue; ⛔ do not assume it landed.
    2. Optional, non-blocking, no card owed. content/docs/ui/actions.mdx:185 still opens with 「The parameter is typed EngineQueryOptions」 before the new paragraph subtracts the key. It is a docs page and ⛔ not in packages/spec's files[], so it is not a published surface; the next edit of that page is its carrier.
    3. Not routed, deliberately. The typed/untyped asymmetry — an untyped caller still has its context accepted 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 Changeset re-fired on the unlabeled event at 2026-09-20T11:23:59Z — 4 seconds after the strip — and concluded success at 2026-09-20T11:25:08Z; re-read latest-per-name after that write: 35 checks, 0 not green, plus the Vercel commit status. ⇒ ready 2026-09-20T11:25:54Z, auto-merge armed.

    ⚠️ A counter-observation to what this seat recorded two hours ago, stated because it weakens that record. On #19283 and #19290 the PUT .../ccr/auto_merge with {"merge_method":"SQUASH"} stored back merge, and the #19148 landing record explains the squash landing by allow_merge_commit: false. The identical call on this PR stored back squash. ⇒ the stored field is not reliably merge, 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 always merge.


    Generated by Claude Code

  5. os-litant commented on Sep 20, 2026

    @os-litant
    Contributor

    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_queue at 2026-09-20T13:20:42Z, and the PR now reads mergeable=false, mergeable_state=dirty.

    ⭐ It is not a CI failure. All five merge_group workflow runs for pr-19315 concluded success — 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):

    CONFLICT (modify/delete): packages/spec/api-surface-declarations/ui.txt
    deleted in origin/main and modified in HEAD.
    

    One file, one conflict. The other five files of this PR are still clean against main — measured with packages/spec/src/ui/action.zod.ts as a lit control proving the delta instrument fires.

    The cause, and ⛔ the correction this seat owes itself

    PR #19024 landed as 2277d1fcd10 at 2026-09-20T12:52:33Z — revert(spec): take back the declaration-text snapshot, restore the 27 signature hashes. It deleted the entire packages/spec/api-surface-declarations/ directory (18 files; git ls-tree on main now returns 0 files there, against a lit control of 108 under packages/spec/src/ui) and restored packages/spec/api-surface-signatures.json. #19315 regenerates ui.txt inside that deleted directory.

    ⚠️ This seat's first reading of the ejection was 「my declared deviation coming home」. That is wrong, and the timeline says so:

    event time
    this seat re-armed auto-merge on #19315 2026-09-20T12:36:58Z
    #19024 landed, deleting the directory 2026-09-20T12:52:33Z

    ⇒ merging origin/main into #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.txt is also touched by #19090 (draft) and #19024 (open, deletes the dir) … whichever lands second must regenerate via scripts/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.

    ⚠️ Part of the review's factual basis has expired with the directory

    The PASS at 5749469600 verified, among other things, a full spec build plus check:api-surface-declarations reporting 「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-review carrier 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

  6. os-litant commented on Sep 20, 2026

    @os-litant
    Contributor

    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

  7. os-litant commented on Sep 20, 2026

    @os-litant
    Contributor

    Landed

    PR #19315 is MERGED. This card is closed completed (GitHub's own Fixes #19237 close, state_reason: completed), and pm:dispatched has been removed in a four-step label-write.mjs write that read back bug, priority:p1, security, domain:spec — assignee left in place as the record of who did the work.

    • squash 61dd96f227e05a8d1d0c93a4f6a13cb7eaa6cdf4
    • subject spec(ui): subtract the unenforced context key from ActionEngineFacade.find's query envelope (#19315)
    • closed_at 2026-09-20T15:33Z, read from the issue payload
    • landing read on origin/main at 2026-09-20T15:35Z, after git fetch origin main

    MERGED was judged from origin/main, not from the API's merged field

    git log --format='%s' -800 origin/main | grep -cF '(#N)', with both controls drawn in the same instrument and the same reading:

    probe count role
    (#19315) 1 the subject
    (#19318) 1 lit control — a PR known to be on main ⇒ the instrument fires
    (#99999) 0 dark control — a number that cannot exist ⇒ the instrument is not matching everything

    An earlier reading of the same instrument, taken while the PR was still in the queue, answered (#19315) = 0 with 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:

    1. pulls/{n}.base.sha is 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 behind main. Filed as pulls/{n}.base.sha is 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 are git merge-base in a real worktree, or GET /compare/{base}...{head}.
    2. removed_from_merge_queue fires 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 as removed_from_merge_queue fires on a SUCCESSFUL merge too — 3 of 5 on this board today were merges, so the event is not a failure signal #19343.
    3. The queue-branch listing alone decides nothing. A second false alarm on this PR came from reading an absent gh-readonly-queue/main/pr-19315-* ref as a drop. The timeline (no removed_from_merge_queue event) plus live merge_group runs is what settled it. Recorded as a refinement on removed_from_merge_queue fires 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 main at that moment could not have carried the api-surface-declarations deletion.


    Generated by Claude Code

  8. added a commit that references this issue on Sep 28, 2026
    61dd96f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions