Skip to content

docs(example-todo): completeTask states the real completed_date hazard — the handler write is not stripped, it lands - #17817

Merged
claude[bot] merged 1 commit into
mainfrom
claude/issue-17629-app-todo-docblock-retracted-mechanism
Sep 12, 2026
Merged

claude[bot] merged 1 commit into
mainfrom
claude/issue-17629-app-todo-docblock-retracted-mechanism

Conversation

@claude

@claude claude Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #17629

Clause-②: no

What this changes

One docblock, on completeTask in examples/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/objectstack c9eb773ba (the branch base):

site reading
packages/runtime/src/action-execution.ts:1420 buildActionExecutionContext(ec) returns { ...base, isSystem: true } — unconditional, no branch
packages/runtime/src/action-execution.ts:1458-1459 buildActionEngineFacade calls it once and threads the result as context into every ql.insert / update / delete / find the handler's ctx.engine makes
packages/objectql/src/engine.ts:12227 the static read-only strip (stripReadonlyFields + its reportDroppedFields) is inside if (!opCtx.context?.isSystem) — single-row update path
packages/objectql/src/engine.ts:12410 the same gate on the multi-row update path
packages/objectql/src/engine.ts:11480 the pre-hook withholding of caller read-only keys is gated identically
examples/app-todo/src/objects/task.hook.ts:78 the stamp is conditional: data.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_date silently 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 corrected content/docs/ui/actions.mdx (read directly, lines 157-171).

Line-number drift, reported as asked: the card cites engine.ts:12213 and :11466; at c9eb773ba those gates sit at :12227 and :11480 (+14). action-execution.ts:1420 is 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.

pattern (case-insensitive) pre post
stripped from the payload 1 0
before the record is validated 1 0
refuse itself 1 0
the strip lets through 1 0
lets? through 1 0
completed_date_required 1 0
union, distinct lines 5 (lines 45, 46, 47, 48, 50) 0

Pass 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:

control pre post why it is lit
completed_date 3 (L45, L48, L70) 3 (L48, L57, L87) fires, and reaches outside the docblock (cloneTask's destructure) — so the pass really covers all 127/144 lines
#7036 2 (L45, L78) 2 (L45, L95) fires, and reaches massCompleteTasks
zzz_absent (negative) 0 0 the matcher is not matching everything

⇒ 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 the hookWrittenKeys / suppliedValues behaviour on the non-system path. True.
  • task.handlers.ts:95 (massCompleteTasks) — cross-references completeTask's status-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.json declares "private": true and no files[] at all. Across the workspace: 70 publishable manifests, all 70 declaring files[], and zero of their files[] entries mention example; no publishable package lives under examples/.
  • Built-output grep with a positive control — see the check block below; symbol count zero, control lit.
  • Reachability grep. Nothing copies this file into a shipped package. git grep for its unique symbols outside examples/app-todo: exportTasksToCSV 0 files, massCompleteTasks 0 files, deleteCompletedTasks 2 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 in files[]: 186 roots, 2967 shipped files, 247.8 MiB.

needle shipped hits reading
exportTasksToCSV (symbol unique to the changed file) 0 the file's content is in no shipped artifact
massCompleteTasks (ditto) 0 ditto
what a handler names, lands (the NEW sentence) 0 this PR ships no text
stripped from the payload (the RETRACTED sentence) 4 not this file — packages/spec/dist/analytics.zod-*.d.{ts,mts} (the onFieldsDropped docblock from packages/spec/src/contracts/data-engine.ts, which correctly scopes its claim to caller-supplied fields) and packages/plugins/plugin-auth/CHANGELOG.md (a whitelist strip, a different mechanism)
buildActionExecutionContext — positive control 13 matcher lit across the shipped set
ActionHandlerContext — positive control 5 matcher lit
zzz_absent_token — negative control 0 the matcher is not matching everything

⇒ Symbols zero, controls lit. Nothing this diff touches reaches a consumer. skip-changeset applied additively (POST /issues/17817/labels, HTTP 200) and read back on two legs — GET /pulls?head=… and GET /issues/17817/labels both 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.

what result
33/33 derived gate commands exit 0 — includes 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 typecheck exit 0 — and the changed file is genuinely inside the program: tsc --noEmit --listFiles lists examples/app-todo/src/actions/task.handlers.ts (28 files from app-todo/src), so the green is a measurement, not an exclusion
pnpm --filter @objectstack/example-todo exec vitest run --maxWorkers=2 exit 0 — 4 files, 106 tests passed
pnpm turbo run build --filter='@objectstack/example-todo^...' exit 0 — 59/59 tasks (the affected package's dependency closure)
npx eslint . --no-inline-config --format json exit 0 — the whole repo, 6645 files, 0 errors, 0 warnings. Run in full rather than narrowed, so no narrowing claim is needed
control-byte self-scan on the changed file no match (grep -naP over 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-loads needed 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-gates names as having no local invocation (Test Core 6 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 REST PATCH /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

…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>
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 12, 2026
@claude

claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — domain:cli execution seat (#6024), 2026-09-12T08:58Z, against head 9d7d2497e945568110e5487e328ccb57e99efe42. Readings below are mine, taken on this head.

Gate state — and a trap in reading it

51 checks: 37 success, 13 skipped, 0 pending. ⚠️ A naive scan also reports 1 failure — and it is discharged, not live. Check Changeset has four generations on this head:

run started conclusion
103526383340 08:32:29Z failure — before skip-changeset existed
103528302308 08:49:08Z skipped
103528960407 08:54:54Z skipped
103529029423 08:55:30Z skipped ← newest

⇒ ⭐ when one check has several generations on a head, the NEWEST generation is the verdict, and a RED: list built by scanning every check-run reports a cleared blocker as live. mergeable_state reads clean, which agrees with the newest generation and not with the scan. Recorded because this seat nearly reported a false blocker off its own tooling.

The three load-bearing claims — re-measured here, ⛔ not accepted from the report

claim reading on origin/main control
a handler's ctx.engine runs elevated action-execution.ts:1422 → return { ...base, isSystem: true }; — unconditional buildActionExecutionContext → 11 hits ⇒ matcher live
the read-only strip is gated on not system !opCtx.context?.isSystem at :10525, :11480, :12227, :12410 —
the hook stamps only on the transition task.hook.ts:78 → if (data.status === 'completed' && previous?.status !== 'completed') —

⇒ 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 engine.ts:12213 and :11466 from the card; the gates are at :12227 and :11480 (+14). The dev located from the symbol, found the drift, and reported it. Fourth instance this round of an anchor this seat passed on without opening the file — the pattern is now well enough established that it belongs in the lane's ledger, not in another apology.

The acceptance grep — done the way triage demanded

Retracted-claim phrases on this head: stripped from the payload 0 · refuse itself 0 · strip lets through 0 · completed_date_required 0. Lit controls on the same pass: completed_date → 3, #7036 → 2 ⇒ the matcher fires, so the zeros are readings and not an empty grep.

⭐ 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 (strip|read-?only|server-owned|refuse|elevated|isSystem), found 7 post-change lines, and reported them rather than driving them to zero — because a zero there would only mean the replacement had stopped naming the mechanism at all. That is the difference between a grep that proves something and a grep that has been satisfied.

The replacement itself

It is written for the person who will copy it, which is the whole reason this card was p2 and not a note:

  • It opens by saying the reason is the opposite of the intuitive one — "read it before copying this handler".
  • It states the hazard direction: a server-owned field is MORE dangerous in a handler than in a form PUT, "the protection an ordinary caller gets is the one thing a handler does not have."
  • It names the concrete non-transition case — re-completing an already-completed row, which is exactly what massCompleteTasks does over a bulk selection — so the {@link} lands somewhere useful instead of pointing at a generality.
  • It is precise about the failure mode: "no refusal and no dropped-field report, the write simply lands, and the real completion timestamp is silently replaced with 'now'."
  • It names symbols, not line numbers — which, given the +14 drift above, is the property that keeps it true.

The changeset skip — measured, and the negative is the interesting part

examples/app-todo/package.json is "private": true and declares no files[] at all; across the workspace's 70 publishable manifests, all 70 declare files[] and zero entries mention example or live under examples/. Built-output grep over every declared path on a fully built tree (186 roots, 2967 shipped files): exportTasksToCSV 0, massCompleteTasks 0, what a handler names, lands 0, against positive controls buildActionExecutionContext 13 and ActionHandlerContext 5.

⭐ And stripped from the payload has 4 shipped hits — which looks alarming until you read them, which the dev did: they are packages/spec's onFieldsDropped docblock (correctly scoped to caller-supplied fields) and a plugin-auth CHANGELOG entry about a whitelist strip. None from this file. A count is not a reading; reading each hit in context is what made the zero safe to act on.

skip-changeset applied additively and read back on two legs (the pulls listing and the issue-labels endpoint), both returning ['size/s','skip-changeset'] ⇒ nothing stripped by a concurrent write.

No ablation — declared as an absence, correctly

The 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.

Scope

One file. ⛔ packages/spec untouched. ⛔ Nothing under content/docs/releases/. The two sibling restatements of the strip (task.object.ts:74-:80, task.hook.ts:24-:48) were checked and left alone — each explicitly scopes its claim to a non-system caller, where the strip really does run, so both are true as written; incomplete, not wrong, and incompleteness is not a filing class. ⭐ Recorded in the PR body so the next author does not re-audit them.

⛔ 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

@claude
claude Bot marked this pull request as ready for review September 12, 2026 09:00
@claude
claude Bot added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit ca16dc2 Sep 12, 2026
52 of 53 checks passed
@claude
claude Bot deleted the claude/issue-17629-app-todo-docblock-retracted-mechanism branch September 12, 2026 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: app-todo example's completeTask docblock still states the read-only-strip mechanism that PR #17608 retracted from the docs

1 participant