fix(spec): ActionEngineFacade.delete declares the id array the runtime already accepts, and says which convention is the contract - #17608
Conversation
…h convention is the contract `ActionEngineFacade.delete` declared `id: string` while the runtime facade has always accepted `string | string[]`, iterating one `ql.delete` per id. The declaration is now `string | string[]`, and the member's doc comment states what the runtime comment used to record as an accident of two handler suites: both spellings are contract, the array form is a convenience over the same per-row path, and it is neither bulk nor atomic. Two consumers follow from the declaration: - `packages/runtime`'s "tolerant of both conventions" comment is retired (a comment correction only; the arm's behaviour is untouched). - `examples/app-todo/src/actions/task.handlers.ts` drops the hand-rolled `ActionContext` copy of the facade — which existed because the published type could not express its array call, and which had already drifted on `find` — for the published `ActionHandlerContext`. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…#15117) The level is measured, not assumed: nothing is removed and nothing narrows, no runtime behaviour changes, and the sibling re-declaration of the neighbouring member (`find`, #14175) shipped as a patch from the same interface. What ships is a published type that finally describes behaviour that was already served. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…tion-engine-delete-id-array
…#15117) `check:spec-docblock-symbol-anchors` refuses a line number as an anchor form, and the citation added with the widening (`:1471`) was one — a NEW finding, not one of the seven day-one residuals. It is now the symbol anchor `packages/runtime/src/action-execution.ts#buildActionEngineFacade`, which is also the more honest citation: this card exists partly because the line numbers the issue quoted had already drifted by three hundred lines. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 140 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 00965726fa29eec4293ba9959a661d0cd29e80b3 && git checkout 00965726fa29eec4293ba9959a661d0cd29e80b3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0918c441188cae8c3990ac8cb38dbb6dd6e7b6e4 8c90c0b0f3cfcc1c321e5cd022c6c4b565c88e9b && git checkout -B drift-repro 0918c441188cae8c3990ac8cb38dbb6dd6e7b6e4 && git merge --no-ff 8c90c0b0f3cfcc1c321e5cd022c6c4b565c88e9b
node scripts/docs-audit/affected-docs.mjs --json 0918c441188cae8c3990ac8cb38dbb6dd6e7b6e4
|
|
Seat adoption record — adopted VERBATIM. Tier verified from the transcript, ⛔ not self-report: 105 harness-stamped Independently re-measured by this seat before adopting — ⛔ the verdict's central claim is not taken on its word: ⭐ The discipline finding of this review is the purest instance of tonight's recurring lesson, and it deserves to be read past this PR. The round reported
⇒ ⭐ A control that lights, a scan that clears, a gate that exits 0 — none of them is a reading unless the instrument could have come back the other way. ⛔ And it is this seat's miss too, not only the round's. I had not read ④ on this PR; I took the round's gate summary and moved on to commissioning the review. The review is what caught it — which is the gate working, and also the reason ④ is a seat pre-check and not a thing to inherit from a report. On the level itself, the verdict does not merely cite a gate — it reads the written rule: an additive widening of a published surface takes at least ⭐ What the verdict confirms, so the rework does not re-litigate it: the contract sentence's five claims are each verified against the arm ( Carrier handling — the seat's protocol, ⛔ not the verdict's sentence about itThe verdict says the carriers "stay until the must-fixes land". ⛔ That is carrier bookkeeping, which is the seat's protocol and outside a reviewer's remit, and the standing rule is explicit: a FAIL clears both carriers exactly as a PASS does — the label means a review is pending, and one has happened. The owed work rides the handover comment on card #15117, ⛔ not a label. ⇒ Disposition of the three cards the verdict names
Contract review (
|
…ged docs page (#15117) Contract review returned FAIL with two must-fixes. 1. The changeset is `minor`, not `patch`. The written rule (`.github/workflows/pr-automation.yml`, maintainer ruling 2026-09-04 batch #35) is that a purely additive widening of a published package's public surface takes at least `minor`, and a commit type may raise a bump but never lower it below what the act requires. The PR's own `Clause-②: yes` line says this widens the accept set, in those words. The `find` precedent it leaned on does not reach: that was a NARROWING, it landed the day the rule was ruled, and the rule disclaims pre-rule `patch` precedents. How the wrong level survived local verification is the more useful half: `check-changeset-no-major` reads the clause-② declaration from the event payload and nothing else, so a local run without `--event` cannot exercise the level axis at all. Its exit 0 was recorded as a reading when the instrument could not have come back the other way. 2. `content/docs/ui/actions.mdx` — the page the repo's own Docs Drift Check flagged on this PR — is re-verified against the rewritten example. Its handler snippet still annotated `ctx: ActionContext`, a type that file no longer declares; it now imports and annotates the published `ActionHandlerContext`. The same snippet also wrote `completed_date`, which is `readonly` on `todo_task` and stamped by the object's `beforeUpdate` hook: copying it made the action refuse itself against `completed_date_required`. Both facts are verified against `task.object.ts` and `task.hook.ts`. The page now states the `delete` convention beside where it already states `find`'s. Folded in: the example's comment attributed to the contract a request the contract does not make. The contract asks for `ActionHandler`; it says so, and says why a file of function declarations annotates `ActionHandlerContext` instead. Comment-only — 11 changed lines, all comments or blank. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
|
Seat adoption record — adopted VERBATIM. Tier verified from the transcript, ⛔ not self-report: 70 harness-stamped ⛔ First: this seat amplified the false claim, and says so before anything elseIn the re-hang record above I wrote that the round's Re-measured by this seat before adopting — ⛔ not taken on the reviewer's word either: ⇒ A handler's ⭐ The round read the #7036 history and wrote it in the present tense. The removal is right; the stated reason is a false statement about the platform, on a published page, introduced by this push — and the true reason is close to its inverse: the write is not stripped, it lands, and on a re-complete with no transition it would overwrite the real completion timestamp.
⭐ The reviewer also tested this seat's own instruction instead of obeying itThe commission asked it to judge whether the round's refusal to re-run the gate was right, and warned it off reflexive praise. It found the round's premise — "the level question here has exactly one instrument: CI with the payload" — is false by the gate's own header: ⛔ This seat had praised that refusal too — as "harder than re-running" and "the correct generalisation". The generalisation is correct; the application was not, because a second instrument existed. Both halves now stand on the record together. Landing state⇒ What this re-read confirms and the rework must not re-litigate: the changeset is Contract review — narrow re-read (
|
…d field (#15117) The previous push removed `completed_date` from the handler snippet, which was right, and then explained it with a mechanism that is false. Re-measured: - A handler's `ctx.engine` runs ELEVATED — `buildActionExecutionContext` returns `{ ...base, isSystem: true }` (`packages/runtime/src/action-execution.ts`) — and the read-only strip is gated `if (!opCtx.context?.isSystem)` (`packages/objectql/src/engine.ts`). Nothing is stripped on that path, so "stripped from the payload before the record is validated" never happens. - The refusal does not exist either, for any caller. The hook's stamp is unconditional on the transition and its own docblock says that is precisely so a caller-supplied value is overwritten and survives the strip; a live test asserts it — "a caller that still sends `completed_date` is not punished for it — the hook value wins". The REJECTED row whose mechanism the page described is labelled "Measured before the fix". The true reason is close to the inverse, and is now what the page says: the write is not stripped, it LANDS, and on a write that is not a completion transition the hook does not stamp, so the handler's "now" silently replaces the real completion timestamp. I read the #7036 history at the three sites I cited and wrote it in the present tense, without reading the four sites the conclusion depended on. A citation that exists is not a citation that entails. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
|
Contract review verdict adopted verbatim — PASS. Seat readings before adopting (2026-09-11T05:01Z): tier from the review transcript Transport de-escaping applied by the seat before posting, and declared: Contract review — final re-read (
|
| page says | site | holds? |
|---|---|---|
a handler's ctx.engine runs elevated (isSystem) |
action-execution.ts:1420-1422 { ...base, isSystem: true } — unconditional, even for an absent ec; the facade binds it once at :1459 and passes it on every ql.update (:1467); both ctx.engine producers use it (:2091, domains/actions.ts:728) |
yes |
| the read-only strip is gated on not a system write, so it does not run on a handler's write | engine.ts:12213 if (!opCtx.context?.isSystem) around stripReadonlyFields; opCtx.context is options.context (:11199); and the pre-hook withholding of caller readonly keys is gated the same way (:11466), so nothing is hidden-then-restored over a hook stamp on a system write either. The one strip that does run on a system write (stripReadonlyWhenFields, :12195) skips any field without readonlyWhen (rule-validator.ts:679), which completed_date has none of |
yes |
completed_date is readonly on todo_task, stamped by the beforeUpdate hook on the completion transition |
task.object.ts:81-84 readonly: true; task.hook.ts:78-80 stamps only when data.status === 'completed' && previous?.status !== 'completed'; previous is bound from the pre-update read before the hook runs (engine.ts:11693, :11729, dispatch :11735), with no isSystem skip on the dispatch |
yes |
| on the transition the hook overwrites a handler-supplied value | :79 is a plain assignment, not ??= — the docblock's "UNCONDITIONAL" (:36-40) is about not deferring to a caller's key, which is exactly this |
yes |
| re-completing an already-completed task is not stamped | with data.status === 'completed' and previous.status === 'completed', :78 is false and the else if at :85-89 is false (data.status !== 'completed' fails) — neither branch touches completed_date |
yes |
| so nothing overwrites and nothing strips, and the handler's "now" replaces the real timestamp | no stamp (above), no strip (above), completed_date_required passes on a payload carrying a value, and driver.update (:12256) writes hookContext.input.data as-is |
yes |
| "more dangerous here than in a form PUT" | a form PUT is non-system: strip runs, and the live test at task-completion-trigger.test.ts:327-343 shows a forged value outside a transition is stripped for that caller |
yes, as a comparison |
The earlier verdict's "the stamp is unconditional" and the page's "stamps on the transition" are not in tension: the stamp is unconditional given a transition (:79) and gated on a transition (:78). The page states both halves correctly. The correction is not wrong in a new direction.
2. The PR body
The Docs section's second bullet now states the mechanism the sites above entail: buildActionExecutionContext = envelope + isSystem: true; strip gated on !opCtx.context?.isSystem; the hook's stamp unconditional on the transition so a caller value is overwritten; the :287 test named by its title (verbatim match); the REJECTED row's "Measured before the fix" label (:228); and "on a write that is not a completion transition the hook does not stamp, so the handler's 'now' silently replaces the real completion timestamp." All true.
The "exactly one instrument" sentence is retracted in the body's own words and replaced with the header's --base <sha> --event event.json (check-changeset-no-major.mjs:26-31), which is what the header says.
3. The instrument
The pair is real and I reproduced it independently, with the level read out of committed state. One payload (built from the live PR: labels documentation, size/m, tests, tooling, needs:contract-review, protocol:ui, body 13,740 bytes carrying - **Clause-(2): yes**), one base 3ef96b4712, two detached trees:
tree 2653b827af committed '@objectstack/spec': patch -> exit 1 "This PR declares clause-(2) YES, and it grades NO package ... - @objectstack/spec: patch"
tree 8c90c0b0f3 committed '@objectstack/spec': minor -> exit 0 "LEVEL AXIS: this PR declares clause-(2) `yes`, and no package ... is graded `patch`."
The red leg is on the very head CI refused (2653b827af, job 103136662669) and its text is line-for-line the job log's, down to the changeset path and the - @objectstack/spec: patch row. So this is a lit pair that reproduces a known red, not one assembled to match one: the instrument and its input are the same, and the answer flips with the committed level.
"The gate reads from git" is the real cause of the inert working-tree legs. scan() (:546-556) diffs merge-base..head with git diff --name-status ... -- '.changeset/*.md' and reads content with showOrNull(head, file) = git show <head>:<path> (:484-490); head defaults to HEAD (:1742). Nothing in that path opens the working tree (the readdirSync/readFileSync reader at :1678-1688 serves --list). Demonstrated without editing anything: from the tree whose disk says minor, --head 2653b827af -> exit 1; from the tree whose disk says patch, --head 8c90c0b0f3 -> exit 0. The disk copy is not consulted, so a working-tree flip cannot light the gate. Control: the first round's probe shape (committed patch, no --event) -> exit 0 with "LEVEL AXIS: NOT APPLICABLE — no pull_request to read a declaration from", which is the payload-less zero the first FAIL was about.
One wording note, not a finding: the body's "the red leg reproduces CI's refusal on the head CI refused" reads as if the round's red leg was 2653b827af; the round's own description (one detached tree, the committed level the only variable) means it was a throwaway commit of the flip. Both are valid legs of the same instrument, and the literal one is reproduced above.
4. Anything outside scope
Nothing. The round diff is the one file; the PR-wide set against 3ef96b4712 is the same six files as before. Check Changeset is success on both runs at 8c90c0b0f3 (state, not exit code); every other check is success or skipped except Lint & Repo Gates, in_progress at read time.
Must-fix before landing: none.
Cards, not blockers:
examples/app-todo/src/actions/task.handlers.ts:45-50— the handler's own[#7036]docblock still tells the pre-[17.0.0-rc.0] Action body ctx.api is bound to a context-less engine facade — every owner-scoped write dies FORBIDDEN while the audit line claims TRUSTED #3914 story ("stripped from the payload ... made this action refuse itself"). It is byte-identical at the merge base (:37-42there), untouched by this PR, and outside its diff; it is the sentence the page originally copied. Not this PR's introduction and not a blocker; a one-line follow-up under the example, or fold it in only if the round is already touching the file.- The two cards from the first review (behavioural pin for the five
deleteclaims; theid != nullskip ataction-execution.ts:1474) stand as filed; nothing here changes them.
Verdict: PASS
Generated by Claude Code
|
Seat status — PR #17608 is CLEARED TO LAND and is held only by a platform channel outage. 2026-09-11T05:11Z. Recorded here rather than in session memory: a wait is a state, and a state that lives only in a session does not exist. All four landing pre-checks are satisfied, on head
|
| # | check | reading |
|---|---|---|
| 1 | at-tier verdict for the current head | PASS at 8c90c0b0f3 — the PR's head.sha re-read at 05:06Z still names that commit. Tier verified from the transcript before adopting: "model" = 73 of 73 claude-fable-5-1, lit control "type":"assistant" = 61. Adopted verbatim above. |
| 2 | check-clause2-carriers --pair 17608 |
exit 0 — "the clause-② declaration is readable in the fixed spelling and both carriers agree" |
| 3 | check-governed-merges --test over the final 6-path diff |
exit 0, 0 of 6 on the register. Lit control: the same predicate with AGENTS.md appended → exit 3, GOVERNED. The instrument could have come back the other way. |
| 4 | every check green, newest-run-per-check-name | 34 distinct names: 30 success, 4 skipped, zero red, zero pending (05:08Z). Lint & Repo Gates had been in_progress at 04:59Z and completed success. Independently corroborated: mergeable_state moved unstable → clean. Check Changeset red was missed on this very PR. |
needs:contract-review was cleared on both carriers (card #15117 and this PR), one stroke each seconds apart, each read back: gate absent, nothing else added or lost.
What is blocking
Nothing about this PR. Draft→ready cannot be reached right now on any channel:
- MCP
update_pull_request—API rate limit already exceeded for user ID 324100929, on three attempts across five minutes. - REST
PATCH /pulls/17608 {"draft":false}— HTTP 200, response's owndraftfieldtrue, read-backdraft = True. Inert, exactly as filed in [finding] Platform reading: RESTPATCH /pulls/{n}with{"draft": false}answers HTTP 200 and silently does nothing — the channel table records the field as unsupported, but on documentation, and a 200 reads as success #17582 and re-measured today rather than recalled. POST /pulls/17608/ccr/ready_for_review— a route this seat learned today from the GraphQL 403's own text (see [finding] Platform reading: RESTPATCH /pulls/{n}with{"draft": false}answers HTTP 200 and silently does nothing — the channel table records the field as unsupported, but on documentation, and a 200 reads as success #17582) — HTTP 503"GitHub is temporarily unavailable. Retry shortly."on seven attempts across 05:09:01Z-05:11:22Z with backoff.
⛔ The seat will not merge by another route, will not bypass the queue, and will not take a green PR's readiness as licence to improvise. It retries.
One finding from the review that is NOT a blocker
examples/app-todo/src/actions/task.handlers.ts:44-50 still carries the pre-#3914 story this PR removed from content/docs/ui/actions.mdx — the same false "stripped from the payload … made this action refuse itself" sentence the page copied from it. Filed as #17629 with the true mechanism and its sites.
The adopted verdict describes that site as "outside its diff". Correcting the record: the lines are untouched, but the file is one of this PR's six (+32/-24). So the review's conditional disposition — "fold it in only if the round is already touching the file" — was in fact available. The seat chose the other branch it offered, deliberately: the verdict is PASS with no must-fix, ⛔ a seat does not manufacture a fourth must-fix on a PR reviewed three times, and this PR declares Clause-②: yes, so any head move re-hangs the gate and buys a fifth review for six lines of prose. #17629 lands separately and serialised after this PR — same file, hard same-file serialisation.
Next action on this PR is mechanical and unchanged: retry the ready flip, then enable_pr_auto_merge, then confirm the queue ref pr-17608-<sha>.
Generated by Claude Code
Fixes #15117
needs:contract-reviewhangs on both carriers from the moment this PR exists, and nothing lands until an at-tier review returns.What the two ends said
packages/spec/src/ui/action-params.zod.ts:242delete(object: string, id: string)— one idpackages/runtime/src/action-execution.ts:1471async delete(object: string, idOrIds: string | string[]), iteratingql.deleteonce per id:1469examples/app-todo/src/actions/task.handlers.ts:18ActionContextcopy of the facade, reaching the array form at:87Measured on this branch's merge base
86c50528, not taken from the card — the card cites the runtime arm at:1174-1181, which had drifted by about three hundred lines. Lit control:ActionEngineFacadeoccurs 5 times inaction-params.zod.ts, so those readings are readings.What this PR does
delete(object: string, idOrIds: string | string[]), and the member's doc comment now states the contract rather than leaving a reader to infer it from a runtime comment two packages away: both spellings are contract; the single id is one row, the array is a set; the array form is a convenience over the same per-row path, never a bulk or atomic delete (no transaction around the set, a part-way failure leaves the earlier ids deleted); an empty array deletes nothing and resolves. The interface-level docblock, which saidfindwas the one member whose argument shape the signature never settled, now names both.ActionContextis gone, replaced byActionHandlerContextfrom@objectstack/spec/uion all 8 handlers. That also removes the already-driftedfindcopy at:27(the pre-ActionEngineFacade.find(object, query)takes a bare filter whileinsert/update/deletetake explicit shapes — the type says neither, and reading it wrong returns empty with no error #14175query: Record...spelling).ActionHandlerContextis used rather thanActionHandlerfor the annotation becauseActionHandlerreturnsunknown, which would erase the precise return types ofcloneTaskandexportTasksToCSV; the two are named together in the file's own comment.action-params.test.tsgains a#15117block modelled on its#14175neighbour: a type-levelAssertthat the slot is exactlystring | string[](read off the interface, never retyped), positive controls for both conventions and the empty set, and four@ts-expect-errorrefusals.Proven by behaviour, both directions
Every reading below is a real run in this worktree; exit codes were captured before any pipe.
The defect, before the widening — the example annotated with the published type, against the unwidened
dist:After —
pnpm --filter @objectstack/example-todo typecheckexits 0 with zeroerror TSlines. The single-id convention still type-checks (pinned as a positive control in the spec suite).Lit control — the same call site with an obviously-wrong argument, on the widened type, is still refused:
That control matters twice: it proves the green above is not a program that stopped checking the file. Restore was proven by state —
git hash-objectequal to the HEAD blob, whole-treegit status --porcelainempty.Ablation — the widening reverted in
src,packages/specrebuilt, andablation-dist-preflightused to prove the mutation reached the artifact the consumers resolve through (marker absent from all 216 built files) before any colour was believed:Line 485 is the
Assertpin, so the pin is capable of failing. The restore leg rebuilt and re-proved the marker present in 2 built files; the tree was verified clean by whole-treegit status --porcelainand by blob hash, and both channels went back to green.Verification
pnpm --filter @objectstack/spec typecheck— exit 0 (src, scripts and the test layer;check:test-typecheckOK).pnpm --filter @objectstack/spec exec vitest run --project local src/ui/action-params.test.ts— 32 passed.pnpm --filter @objectstack/runtime typecheck— exit 0; the fouraction-execution*suites — 119 passed.pnpm --filter @objectstack/example-todo typecheck— exit 0.pnpm --filter @objectstack/spec check:generated— all 15 generated artifacts up to date, after a real build (nothing to regenerate).scripts/pm/dispatch-gates.mjsderives for this diff were run, all green — 84 before the review, and 106 after it re-derived withcontent/docs/**in the change set;--ranreconciles 106 derived / 106 run / 0 unrun oncc5b7925cb. Across both rounds nine arrived first as prerequisite refusals — an unbuiltdist, orcheck:type-check-debtOOM-ing under a tighterNODE_OPTIONSthan its own CI-shaped 6144 MB ceiling — each of which says in its own words that nothing was measured. Every one was satisfied and re-run; ⛔ not one was reported as a pass, and ⛔ not one as a finding. The single genuine local finding was mine:check-spec-docblock-symbol-anchorsrefused the:1471line anchor I had written, repaired to a symbol anchor.pnpm lint— the repo-wideeslint . --no-inline-configpass, not a narrowed subset — exit 0, re-run at each head; the current head is8c90c0b0f3.origin/mainwas merged in withscripts/pm/os-regen-merge.sh; the spec generated-artifact gates were re-run green on the merged tree.Changeset
minoron@objectstack/spec. Corrected under contract review — this PR first shippedpatch, and that was wrong. The written rule (.github/workflows/pr-automation.yml, maintainer ruling 2026-09-04 batch #35) is that a purely additive widening of a published package's public surface takes at leastminor, and a commit type may raise a bump but never lower it below what the act requires. This PR's ownClause-②: yesline says the change widens the accept set, in the same words. Thefindprecedent the first version leaned on does not reach: #14175 was a narrowing, it landed the day the rule was ruled, and the rule explicitly disclaims pre-rulepatchprecedents. "Nothing is removed and nothing narrows" remains true in the changeset body — it argues the change is not breaking, which nobody disputed.⭐ How the wrong level survived this PR's own verification, recorded because the shape matters more than the fix. The first report cited
check-changeset-no-major --base origin/main :: exit 0as evidence the level was fine. That gate reads the clause-② declaration from the event payload and nothing else, so a local run with no--eventcannot exercise the LEVEL axis at all: the exit 0 said the script ran, not that the level was graded. CI, which had the payload, had already refused 86 seconds earlier. A gate that exits 0 is not a reading unless the instrument could have come back the other way.⛔ And the first correction of that paragraph was itself wrong, so it is corrected here rather than quietly dropped. It said "the level question here has exactly one instrument — CI with the payload". That is false by the gate's own header:
scripts/check-changeset-no-major.mjsdocuments--base SHA --event event.jsonas the level axis driven offline — labels and body only, no token, no network — and I authored this body and knew the labels, so writing that payload was always within reach. Declining to re-run the instrument that could not answer was right; concluding that no instrument could was not. The response to "your probe could not have come back the other way" is to pick up one that can, not to stop measuring.So it is picked up. One detached tree, one payload, one base, the committed level the only variable — the gate reads the changeset out of git, not the working tree, which is why a working-tree flip measures nothing (my first two attempts at this pair were exactly that mistake, and both came back green):
The red leg reproduces CI's refusal on the head CI refused. The shipped head reads exit 0 on the same axis. That is a reading: the instrument came back the other way when the level was wrong.
examples/app-todois private andpackages/runtime's change is a comment, so neither is in the changeset.Docs — the flagged page, disposed of
The repo's Docs Drift Check named
content/docs/ui/actions.mdxon this PR (via ActionEngineFacade). It is owed, and it is fixed here rather than dismissed:src/actions/task.handlers.ts, still annotatedctx: ActionContext— a type that file no longer declares after this PR. It now imports and annotates the publishedActionHandlerContext, with a sentence on why a copy of the context drifts silently, and why a file of function declarations annotates the context type rather thanActionHandler.completed_date. Removing it is right; the reason this PR first gave for removing it was false, and is corrected. The first version said the write is stripped and made the action refuse itself againstcompleted_date_required. It is not, and it does not. A handler'sctx.engineruns elevated —buildActionExecutionContextreturns the caller's envelope plusisSystem: true— and the read-only strip is gated on!opCtx.context?.isSystem(packages/objectql/src/engine.ts), so nothing is stripped on that path at all. The refusal does not exist for an ordinary caller either: the hook's stamp is unconditional on the transition precisely so a caller-supplied value is overwritten and survives the strip, and a live test asserts it — "a caller that still sendscompleted_dateis not punished for it — the hook value wins". TheREJECTEDrow whose mechanism I described is labelled "Measured before the fix". The true reason is close to the inverse, and is what the page now says: the write is not stripped, it lands, and on a write that is not a completion transition the hook does not stamp, so the handler's "now" silently replaces the real completion timestamp. I had cited three sites that exist; the conclusion depended on four others I had not opened. A citation that exists is not a citation that entails.find's convention in a Callout at the old:149-162; thedeleteconvention now sits directly beside it — both spellings contract, one delete per id in order, no transaction, an empty array resolving.content/blog/protocol-first-development.mdxalso shows the old single-id signature. It is a dated post, so it is not owed and is deliberately untouched.验收备注 (acceptance notes)
Observed, deliberately not filed and not fixed here:
check:spec-docblock-symbol-anchorscarries 7 day-one residual line-number anchors in spec docblocks, one of them thefindtwin of the citation this PR adds (:1183). Each already has a dated residual row and a prescribed repair in the gate's own output, so the gate is the carrier — no card is owed. This PR's own citation is a symbol anchor, not a line anchor.examples/app-todo/src/actions/register-handlers.ts:78hand-rolls the ObjectQL registration seam (registerAction, whose handler slot is(ctx: any)returningunknown) — a different surface fromActionEngineFacade, in a file this card does not name, and the(ctx: any)is exactly the untyped-bag shapeActionHandlerexists to replace. Not a defect, not a contract violation, not an authoring trap, so not filed; the natural home for it is a card on typing the registration seam, which does not exist today.examples/app-todo'stest/files cannot resolve their workspace deps until those packages are built. That is a prerequisite, not a defect, and it is why the first typecheck reading in this worktree carried a pile of TS2307 alongside the real TS2345.⛔ Draft on purpose: do not flip ready, enqueue, or arm auto-merge — the contract review returns first.
Generated by Claude Code