Repository navigation
docs(example-todo): completeTask states the real completed_date hazard — the handler write is not stripped, it lands - #17817
Conversation
…ask — the write is NOT stripped, it lands The `completeTask` docblock described a read-only strip that does not run on this path. A handler's `ctx.engine` is bound through `buildActionExecutionContext`, which forces `isSystem: true` (#3914), and the engine's static read-only strip sits inside `if (!opCtx.context?.isSystem)` (`packages/objectql/src/engine.ts`), so on a handler's write it never runs. Consequences the old text had backwards: nothing is stripped, so the write lands; `completed_date_required` never refuses it (it sees a value); and the hook's stamp goes through because the strip never ran, not because the strip made an exception for it. On a write that is not a transition into `completed`, `task.hook.ts` does not stamp at all, so a handler-supplied `completed_date` silently replaces the true completion timestamp. Net: a server-owned field is MORE dangerous in a handler than in a form PUT, not less. The replacement says that, in the example's own voice, matching the corrected `content/docs/ui/actions.mdx` that PR #17608 landed. `status`-only remains the prescription; only the reason changes. Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude <noreply@anthropic.com>
|
ACCEPT — Gate state — and a trap in reading it51 checks: 37
⇒ ⭐ when one check has several generations on a head, the NEWEST generation is the verdict, and a The three load-bearing claims — re-measured here, ⛔ not accepted from the report
⇒ the mechanism the new docblock states is the one the tree implements. ⭐ And the line number this seat relayed was wrong — again. The dispatch order carried The acceptance grep — done the way triage demandedRetracted-claim phrases on this head: ⭐ The dev went further than the ask and the extra step is the valuable one: it also grepped a deliberate superset of bare mechanism tokens ( The replacement itselfIt is written for the person who will copy it, which is the whole reason this card was p2 and not a note:
The changeset skip — measured, and the negative is the interesting part
⭐ And
No ablation — declared as an absence, correctlyThe diff is comment text; no gate, pin or test in this repo asserts the content of an example docblock, so there is no assertion to mutate. ⛔ The dev did not manufacture one, and says why: a pin for example prose would be scope expansion on a p2 documentation card. ⭐ Reporting "not applicable, and here is why" is the right answer; silently omitting the section would not have been. ScopeOne file. ⛔ ⛔ The dev also declined what was not its job — "I did not re-run it, did not flip the PR out of draft and did not arm anything". Correct: those are this seat's. Proceeding to ready + merge queue. Generated by Claude Code |
Fixes #17629
Clause-②: no
What this changes
One docblock, on
completeTaskinexamples/app-todo/src/actions/task.handlers.ts. Nothing else in the tree moves.The old text described a read-only strip that does not run on this path, and it did so in the direction that leaves a copier less safe. PR #17608 removed the identical prose from
content/docs/ui/actions.mdx; this is the other half — the example the docs page had copied it from, and the demonstrated origin of the falsehood that cost #17608 three review rounds.The mechanism, re-measured in this worktree
Re-derived here rather than inherited from the card, at
objectstack-ai/objectstackc9eb773ba(the branch base):packages/runtime/src/action-execution.ts:1420buildActionExecutionContext(ec)returns{ ...base, isSystem: true }— unconditional, no branchpackages/runtime/src/action-execution.ts:1458-1459buildActionEngineFacadecalls it once and threads the result ascontextinto everyql.insert/update/delete/findthe handler'sctx.enginemakespackages/objectql/src/engine.ts:12227stripReadonlyFields+ itsreportDroppedFields) is insideif (!opCtx.context?.isSystem)— single-row update pathpackages/objectql/src/engine.ts:12410packages/objectql/src/engine.ts:11480examples/app-todo/src/objects/task.hook.ts:78data.status === 'completed' && previous?.status !== 'completed'— the transition only⇒ On a handler's write the strip never runs, so the write lands; and a completion write that is not a transition is never stamped, so a handler-supplied
completed_datesilently replaces the true one. A server-owned field is more dangerous in a handler than in a form PUT, not less. This agrees with the mechanism in the dispatch and with the correctedcontent/docs/ui/actions.mdx(read directly, lines 157-171).Line-number drift, reported as asked: the card cites
engine.ts:12213and:11466; atc9eb773bathose gates sit at:12227and:11480(+14).action-execution.ts:1420is unmoved. Everything above was located from the symbol, not the line number.Acceptance 1 + 2 — whole-file grep, with a lit control
Not only the three lines the card quotes. File read by state at
c9eb773ba(pre) and at this PR's head (post).Pass A — the retracted claim's own phrases.
stripped from the payloadbefore the record is validatedrefuse itselfthe strip lets throughlets? throughcompleted_date_requiredPass B — bare mechanism tokens (
strip|read-?only|server-owned|refuse|elevated|isSystem), a deliberate superset, because the true sentence necessarily reuses the same vocabulary: 7 matching lines post-change, all of them lines 48-63 — inside the new docblock, and each one a true statement. Reported rather than driven to zero: a zero here would only mean the replacement had stopped naming the mechanism.Lit controls, same pass, same file:
completed_datecloneTask's destructure) — so the pass really covers all 127/144 lines#7036massCompleteTaskszzz_absent(negative)⇒ The card's quoted span was the whole survival in this file — but that is now a measurement, not the assumption the card warned against.
Also checked and deliberately left alone (both accurate as written, neither states the retracted mechanism):
examples/app-todo/src/objects/task.object.ts:74-80— scopes its claim explicitly to "a non-system caller's write", where the strip really does run. True.examples/app-todo/src/objects/task.hook.ts:24-48— same, and its "a value a hook wrote is a platform value and survives" is thehookWrittenKeys/suppliedValuesbehaviour on the non-system path. True.task.handlers.ts:95(massCompleteTasks) — cross-referencescompleteTask'sstatus-only rule. The prescription survives the correction unchanged; only its reason changes, and the reason now lives one{@link}away. Left as-is, and the new docblock names bulk re-completion as the concrete non-transition case so the cross-reference lands somewhere useful.Acceptance 3 + 4 — the replacement
Written for someone who will copy it: the hazard first, the concrete wrong line (
completed_date: new Date().toISOString()) second, and the silence of the failure stated outright — no refusal, no dropped-field report, the write simply lands. It names symbols (buildActionExecutionContext,isSystem) rather than file line numbers, so it cannot rot the way the citations in this PR body can.Acceptance 5 — changeset: skipped, with a reading
Not an assumption:
files[]reading.examples/app-todo/package.jsondeclares"private": trueand nofiles[]at all. Across the workspace: 70 publishable manifests, all 70 declaringfiles[], and zero of theirfiles[]entries mentionexample; no publishable package lives underexamples/.git grepfor its unique symbols outsideexamples/app-todo:exportTasksToCSV0 files,massCompleteTasks0 files,deleteCompletedTasks2 files — both of which only name it in prose (packages/spec/src/ui/action-params.test.ts:501,packages/runtime/src/action-execution-destructive.test.ts:241), neither a copy of the content.⇒ Nothing published moves. A false statement about a mechanism becomes a true one, inside a private package.
Acceptance 5, measured half — built-output grep with a positive control
Run against the fully built tree at
9d7d2497e(pnpm turbo run build --filter='!@objectstack/docs'— 73/73 tasks, exit 0). The scan surface is every path the 70 publishable manifests declare infiles[]: 186 roots, 2967 shipped files, 247.8 MiB.exportTasksToCSV(symbol unique to the changed file)massCompleteTasks(ditto)what a handler names, lands(the NEW sentence)stripped from the payload(the RETRACTED sentence)packages/spec/dist/analytics.zod-*.d.{ts,mts}(theonFieldsDroppeddocblock frompackages/spec/src/contracts/data-engine.ts, which correctly scopes its claim to caller-supplied fields) andpackages/plugins/plugin-auth/CHANGELOG.md(a whitelist strip, a different mechanism)buildActionExecutionContext— positive controlActionHandlerContext— positive controlzzz_absent_token— negative control⇒ Symbols zero, controls lit. Nothing this diff touches reaches a consumer.
skip-changesetapplied additively (POST /issues/17817/labels, HTTP 200) and read back on two legs —GET /pulls?head=…andGET /issues/17817/labelsboth return['size/s', 'skip-changeset'], so nothing was stripped by a concurrent write.Verification
Exit codes captured before any pipe. Gate families derived mechanically, not guessed:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack→ 33 commands, all run.check:nul-bytes(8463 text files, no raw control bytes),check:system-context-census(110 elevation read sites / 91 symbols — the census this PR's prose talks about),check:comment-mask-adoption+-corpus,check:keyed-text-bounds,check:reference-carrier-shape(6645 files),check:examples-live-imports,check:published-files,check:dts-closure,check:lean-entry-closure,check:sourcemap-no-sources-content,check:dual-build-cjs-loads(104 require entry points / 67 packages / 620 CJS files)pnpm --filter @objectstack/example-todo typechecktsc --noEmit --listFileslistsexamples/app-todo/src/actions/task.handlers.ts(28 files fromapp-todo/src), so the green is a measurement, not an exclusionpnpm --filter @objectstack/example-todo exec vitest run --maxWorkers=2pnpm turbo run build --filter='@objectstack/example-todo^...'npx eslint . --no-inline-config --format jsongrep -naPover the C0/DEL classes)First-pass NOT MEASURED, then resolved: four gates (
check:dts-closure,check:dual-build-cjs-loads,check:lean-entry-closure,check:sourcemap-no-sources-content) exited 3 on the unbuilt tree — their own words, "PREREQUISITE NOT MET … NOT a pass and NOT a finding". They were re-run after the build and are among the 33 greens above.check:dual-build-cjs-loadsneeded the whole tree, not just this card's closure, and got it.Genuinely NOT MEASURED here, by design, and left to CI: the five path-scheduled CI jobs
dispatch-gatesnames as having no local invocation (Test Core6 shards,Temporal Conformance,Dogfood Regression Gate,Dogfood Verify CLI,Build Core), the 11 families whose declared population is too wide to place, and the 51 artifact-roster families whose silence is a fact about a list rather than about these paths. None of them is reported as green.Authored by Claude Code, session
session_01TSf4DV7ziu4V5j73e46b7c(https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c). This body was edited through a bare RESTPATCH /pulls, the one channel measured to append its own attribution block, so the footer below is the platform's and this line is the durable record.Generated by Claude Code