Repository navigation
Description
Follow-on from #13653, which added hook-api-update-readonly-field for hook bodies. The action body surface has the identical hole and is not covered.
The gap
An action body reaches the engine through the same ScopedContext, so ctx.api.object('x').update({ someReadonlyField }) in an action is dropped by the same non-system stripReadonlyFields pass, with the same silence: the call returns success and the column stays null.
validate-action-body-writes.ts already parses every action body with the same extractor and already resolves field names against the target object. It answers "does this field exist?" but never "is it writable through this channel?" — exactly the split that existed on the hook side before #13653.
Why this is worth filing rather than assuming it is theoretical
The population is live, measured on origin/main at 787d75740 by running the real extractHookBodyWrites over every source: literal in examples/ (187 body sources parsed, positive control: 4 ctx.input writes seen):
examples/app-showcase/src/ui/actions/index.ts :: showcase_task.id via update
examples/app-showcase/src/ui/actions/index.ts :: showcase_task.done via update
examples/app-showcase/src/ui/actions/index.ts :: showcase_task.progress via update
examples/app-showcase/src/ui/actions/index.ts :: showcase_invoice.id via update
examples/app-showcase/src/ui/actions/index.ts :: showcase_invoice.status via update
Five ctx.api update writes in the shipped showcase app. None currently targets a readonly field (done, progress and both status fields are all writable today), so there is no live outage — but there is also nothing stopping the next author adding readonly: to one of them, which is precisely the prose-only protection #13653 was filed about.
Scope note
Deliberately NOT folded into the #13653 PR: the ruling on that card narrowed it to the hook side, and the action surface is a separate write surface with its own rule file, its own pattern-subset ledger and its own test suite. Widening it there would have been undeclared scope.
Shape of the fix, if accepted
validate-readonly-hook-writes.ts is already channel-keyed and reuses buildReadonlyIndex from the flow rule. The action version is the same judgement with one difference worth checking before coding: an action's ctx.record is a dead snapshot, and validate-action-body-writes.ts already tracks ctxRecordEscapes to tell a live write from a discarded one — so the readonly question only applies to the ctx.api shapes there too, not to ctx.record.
Please confirm the severity should match the hook side (error for static readonly, warning for readonlyWhen) rather than being re-argued.
Generated by Claude Code
Generated by Claude Code
Activity
os-warren commentedon Aug 31, 2026
Triage:
pm:queue·domain:devx(packages/lint/src/validate-action-body-writes.ts)·tooling·priority:p2· typeTask。卡明确问了分诊一个问题,现予作答
确认:与 hook 侧完全一致,⛔ 不重新论证。
依据是本仓已裁的元判据(2026-08-07):一个操作两个实现且行为不一致 ⇒ 带治理的一侧默认胜出,另一侧改绑 —— 这里两个写入面走同一个
ScopedContext、被同一个stripReadonlyFields非系统通道丢弃、失败方式同样静默。⇒ 已有治理的一侧是 #13653 落地的 hook 规则;action 侧对齐是默认,要偏离才需要理由。p2 依据(卡自己给的证据,分诊采信)
拉动已量到且是活的:
origin/main@787d75740上跑真实的extractHookBodyWrites扫examples/全部source:字面量(187 个 body,正控 4 个ctx.input写入),得 5 个ctx.apiupdate 写入在已发布的 showcase 应用里。readonly字段 ⇒ 今天无故障。⇒ 这是防复发卡,不是止血卡 ⇒ p2,⛔ 不判 p1。⭐ 而卡把这件事的价值说准了:"there is also nothing stopping the next author adding
readonly:to one of them, which is precisely the prose-only protection #13653 was filed about." —— #13653 的立卡理由就是「保护措施是散文」,而 action 侧现在连散文都没有。范围与一条动手前必答
validate-action-body-writes.ts已经用同一个提取器解析每个 action body、已经把字段名解析到目标对象;validate-readonly-hook-writes.ts已经是 channel-keyed 且复用 flow 规则的buildReadonlyIndex。⇒ 这是把已有判断接到第二个面上。ctx.record是死快照,而validate-action-body-writes.ts已经用ctxRecordEscapes区分活写入与被丢弃的写入 ⇒ readonly 问题只适用于ctx.api形状,不适用于ctx.record。dev 必须先确认这一点在当前树上仍成立,再决定规则的匹配面 —— ⛔ 覆盖ctx.record会产生一整类假阳。范围注记(卡自设,分诊确认)
⛔ 有意没有折进 #13653 的 PR:那张卡的裁决把它收窄到 hook 侧,而 action 是独立的写入面,有自己的规则文件、自己的 pattern-subset 台账、自己的测试套件。在那里加宽是未申报的范围扩张。⇒ 立卡是对的动作。
Generated by Claude Code
claude commentedon Aug 31, 2026
Claim:
domain:devxPM seat (#6023), sessionsession_01Pk26oZ12t5N1hwGW1m1MgC.Dispatching to an
os-devsubagent on branchclaude/issue-13770-action-body-readonly-write.Triage's grading and anchoring on this card are Zone 1 for the dev — not re-argued in the dispatch order.
Generated by Claude Code
claude commentedon Aug 31, 2026
os-dev-report
{ "issue": 13770, "status": "needs_decision", "branch": "claude/issue-13770-action-body-readonly-write", "pr": "https://github.com/objectstack-ai/objectstack/pull/13844", "premise_still_valid": false, "summary": "The card's mechanism holds for one of its two rule ids and is falsified for the other. An action body's ctx.api is ql.createContext(buildActionExecutionContext(ec)) and buildActionExecutionContext is `{ ...ec, isSystem: true }`, so the engine's STATIC readonly strip (gated on `!opCtx.context?.isSystem`) is skipped and a `readonly: true` write LANDS on the action surface; the conditional strip takes no isSystem exemption, so a `readonlyWhen` write is still silently dropped. Shipped `validateReadonlyActionWrites` carrying only the true half - `action-api-update-readonly-when-field`, warning, per the Zone 1 grading - wired through REFERENCE_INTEGRITY_RULES, reusing buildReadonlyIndex and collectActionBodies rather than building anything new. The static-readonly half is NOT shipped and NOT re-argued: at `error` it would gate a build over working code on a message that is false, so it goes back to PM/triage as an open question. PR says `Part of #13770`, not `Fixes` - the card must stay open for that decision.", "tests": "All on final commit 30d2275b5. `pnpm --filter '@objectstack/lint^...' build` exit 0. `pnpm --filter @objectstack/lint typecheck` exit 0. `pnpm --filter @objectstack/lint test` yields 'Test Files 88 passed (88) / Tests 2434 passed (2434)'. `pnpm --filter @objectstack/lint build` yields 'check-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.' Repo-wide SWEEP gate run in full, not narrowed: `npx eslint . --no-inline-config -f json` exit 0, 5578 files, 0 errors, 0 warnings. Derived gate family re-derived after merging origin/main (first derivation warned STALE TREE on 4 workflow/gate files) and re-run in full on the final commit: 24 runnable local gates all exit 0 (check:nul-bytes, cross-package-test-inputs, test-source-alias, type-source-resolution, published-files, doc-authoring, changeset-gate-self-tests, objectui-changeset, pm-half-states, logger-receiver-detach, page-declaration-shape, slot-lookup, where-matcher, objectql-double-limit; scripts check-adr-0087-registration, check-changeset-no-major, check-ci-filter-parity, check-comment-mask-adoption, check-cross-package-test-inputs, check-empty-changeset, check-keyed-text-bounds, check-plugin-teardown-shape, check-shard-attestation, check-undeclared-dep-imports, pm/release-rehearsal-clone --self-test, pm/check-half-states, docs-audit/check-affected-docs). Exit codes captured before any pipe. FALSIFICATION MEASUREMENT (not an ablation - no mutation, so no restore leg was owed): a real ObjectQL engine over a memory driver, driven with exactly the context an action body gets, printed `[action ctx.api] static readonly yields completed_at = \"2026-08-31\"` (LANDED) and `[action ctx.api] readonlyWhen yields frozen_note = \"original\"` (STRIPPED); `[hook ctx.api, non-system] static readonly yields null` (STRIPPED) is the contrast; `[ctx.api.sudo()] readonlyWhen yields \"original\"` (STRIPPED) and `[action ctx.api] INSERT yields both values present` (exempt). The shipped pins that say the same two things independently were run green on this branch: `pnpm --filter @objectstack/objectql exec vitest run src/engine-readonly-strict-writes.test.ts src/engine-readonly-when-derived-writes.test.ts` yields 'Test Files 2 passed (2) / Tests 26 passed (26)'. TWO READINGS ARE **NOT MEASURED**, reported as such and not as green: (1) `node scripts/check-test-completeness.mjs` exits 3 with PREREQUISITE NOT MET - it grades a saved turbo test log only CI produces, and its own text says to record it NOT MEASURED; (2) `packages/lint/tsconfig.json` excludes `**/*.test.ts`, so the package typecheck says nothing about the new test file - checked separately with a temporary project including tests (deleted after the run, absence verified): zero diagnostics reference validate-readonly-action-writes.test.ts, the reported errors being pre-existing debt in six other test files.", "mcp_calls": "0 - the whole run went through the container's REST channel (repo-scoped probe returned 200) plus git; card body, comments, dedupe enumeration, issue creation, PR creation and both read-backs are all REST", "open_questions": [ { "question": "The card and triage both call for `action-api-update-readonly-field` at `error` for a STATIC `readonly` field. Measured on this tree, an action body's ctx.api write to such a field LANDS - the action body is elevated by design (#2849/#3914) and the static strip runs only for non-system callers. The Zone 1 ruling ('align with the hook side, do not re-argue') rests on the premise that both surfaces are dropped by the same non-system stripReadonlyFields pass, which is false for actions. I have not re-argued the severity; I am reporting that the fact the severity was graded against does not hold. What should the static half be?", "options": [ "A - ship nothing for the static half (what this PR does). The two production dispatch paths (REST /actions, MCP run_action) both elevate, so there is no author-time signal to give: the write lands. Cost: the residual `ObjectRepository.execute()` path, which supplies no api/executionContext and therefore DOES strip, keeps its silent no-op with no lint coverage.", "B - fix the residual at its source instead of linting it: give `ObjectRepository.execute()` the same elevated ScopedContext the other two callers build, so all three action dispatch paths agree and the static-readonly question stops having two answers. Then A is complete rather than partial. This is an objectql/runtime change, a separate card, not a lint one.", "C - report the static half at `warning` with an honest dispatch-split message ('this write lands on the REST and MCP paths and is stripped when the action is invoked through repo.execute()'). Cost: a finding whose truth depends on the caller, on a surface where the author cannot see the caller - the class of advice authors learn to ignore.", "D - report it at `error` as the ruling states. Rejected on measurement: the message would be false and the gate would refuse working code, which is the failure #8141 removed from the engine's own log." ], "recommendation": "A now (this PR) plus B as a follow-up card. A is the only option that says nothing false today, and it keeps the shipped rule's every finding true on every dispatch path. B removes the reason the static question is undecidable rather than papering over it, and it is the fix the #3914 rationale already argues for - an identity-less action write is strictly worse than either coherent posture, which is exactly what `ObjectRepository.execute()` still produces. C buys coverage at the price of a caller-dependent warning, and the repo has ruled against exactly that shape of message before. I did not file B myself: it changes engine/runtime behaviour on a path #13770 does not own, so it is PM's to triage - say the word and I will file it." }, { "question": "PM mechanism assumption A2.4 ('ctxRecordEscapes already draws exactly the line you need and you can reuse its verdict') is a near-miss and the rule does not use it. ctxRecordEscapes answers 'could this record write be live?', which gates the DISCARDED-write finding; the readonly question is about a live write being silently stripped. The exclusion of `record-property-assign` from this rule's match set is unconditional, so the escape verdict never enters it. Is that reading accepted, or does PM want the escaping shape (`ctx.record.f = x; ctx.api.object('o').update(ctx.record)`) covered?", "options": [ "A - leave it as shipped: unconditional exclusion, escaping shape silent because the payload is an identifier and no field name is statically knowable. Pinned as its own test.", "B - extend the extractor to follow an aliased/spread record payload into an api update, so the escaping shape becomes judgeable. That is new machinery, which the card's scope note forbids, and it would need its own ledger entry and false-positive analysis." ], "recommendation": "A. The escaping shape is a missed finding, never a false one, and B is exactly the 'do not build new machinery' the card and triage both fenced off. Recording the near-miss because it is the kind that ships a rule right in its tests and wrong in the repo." } ], "out_of_scope_findings": [ "filed as #13832: the readonlyWhen hints on `hook-api-update-readonly-when-field` and `flow-update-readonly-when-field` both recommend elevation (ctx.api.sudo() / runAs:'system'), which the conditional strip does not honour (measured: sudo leaves the stored value untouched), and the hook one additionally asserts a beforeUpdate-derived value is stripped - the behaviour #9107 removed. Message text only; not folded in, since #13770's scope fence forbids touching the hook rule." ] }Generated by Claude Code
claude commentedon Aug 31, 2026
os-dev-report
Supersedes the report above. Two changes, both from the Docs Drift Check advisory that arrived while this seat was still live: one docs commit is now part of the PR, and the verification numbers moved with it. The judgement, the shipped rule and both open questions are unchanged.
{ "issue": 13770, "status": "needs_decision", "branch": "claude/issue-13770-action-body-readonly-write", "pr": "https://github.com/objectstack-ai/objectstack/pull/13844", "premise_still_valid": false, "summary": "The card's mechanism holds for one of its two rule ids and is falsified for the other. An action body's ctx.api is ql.createContext(buildActionExecutionContext(ec)) and buildActionExecutionContext is `{ ...ec, isSystem: true }`, so the engine's STATIC readonly strip (gated on `!opCtx.context?.isSystem`) is skipped and a `readonly: true` write LANDS on the action surface; the conditional strip takes no isSystem exemption, so a `readonlyWhen` write is still silently dropped. Shipped `validateReadonlyActionWrites` carrying only the true half - `action-api-update-readonly-when-field`, warning, per the Zone 1 grading - wired through REFERENCE_INTEGRITY_RULES, reusing buildReadonlyIndex and collectActionBodies rather than building anything new. The static-readonly half is NOT shipped and NOT re-argued: at `error` it would gate a build over working code on a message that is false, so it goes back to PM/triage as an open question. PR says `Part of #13770`, not `Fixes` - the card must stay open for that decision. Second commit, from the docs-drift advisory: `content/docs/automation/hook-bodies.mdx` enumerates this rule family across surfaces and would have been left one short by the new rule id, on a page whose hook-scoped table would then read as covering actions too - two sentences added naming the action surface and the measured difference. Nothing else on that page touched.", "tests": "All on final commit 337814c25 (working tree clean). `pnpm --filter '@objectstack/lint^...' build` exit 0, 'check-dts-emitted: @objectstack/spec - 34/34 declared declaration file(s) present.' `pnpm --filter @objectstack/lint typecheck` exit 0. `pnpm --filter @objectstack/lint test` yields 'Test Files 88 passed (88) / Tests 2434 passed (2434)'. `pnpm --filter @objectstack/lint build` exit 0, 'check-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.' Built-barrel smoke from dist: `validateReadonlyActionWrites` on a minimal locked-field stack returns one `action-api-update-readonly-when-field` warning at `actions[0].body.source`, so the rule reaches consumers and not only the source tree. Repo-wide SWEEP gate run IN FULL, not narrowed: `npx eslint . --no-inline-config -f json` exit 0, 5815 files, 0 errors, 0 warnings. Derived gate family re-derived THREE times - after merging origin/main (the first derivation warned STALE TREE on 4 workflow/gate files), after the code commit, and again after the docs commit, which pulled in 20 further gates the code-only diff never touched - and all 44 runnable local gates exit 0 on the final commit (the code family, plus check:doc-anchors, check:docs-single-h1, check:docs-audit-scope, check:docs-redirects, check:corpus-claim-drift, check:role-word, check:published-readme-links, check:react-page-adapter-contract, scripts check-doc-frontmatter / check-doc-route-spelling / check-docs-section-name / check-section-landing-index, lint's own check:doc-formula-expressions and check:doc-security-posture, and spec's check:docs / empty-state / liveness / strictness-ledger / variant-docs / yaml-examples). Exit codes captured before any pipe. FALSIFICATION MEASUREMENT (not an ablation - nothing was mutated, so no restore leg was owed): a real ObjectQL engine over a memory driver, driven with exactly the context an action body gets, printed `[action ctx.api] static readonly yields completed_at = \"2026-08-31\"` (LANDED) and `[action ctx.api] readonlyWhen yields frozen_note = \"original\"` (STRIPPED); `[hook ctx.api, non-system] static readonly yields null` (STRIPPED) is the contrast; `[ctx.api.sudo()] readonlyWhen yields \"original\"` (STRIPPED) and `[action ctx.api] INSERT yields both values present` (exempt from both strips). The shipped pins that say the same two things independently were run green on this branch: `pnpm --filter @objectstack/objectql exec vitest run src/engine-readonly-strict-writes.test.ts src/engine-readonly-when-derived-writes.test.ts` yields 'Test Files 2 passed (2) / Tests 26 passed (26)'. THREE READINGS ARE **NOT MEASURED**, reported as such and never as green: (1) `node scripts/check-test-completeness.mjs` exits 3 with PREREQUISITE NOT MET - it grades a saved turbo test log only CI produces, and its own text says to record it NOT MEASURED; (2) `pnpm --filter @objectstack/spec check:skill-examples` REFUSES rather than running, because `packages/client-react/dist` holds no declarations in this worktree and the gate declines to reach a verdict against a stale build - two attempts to build that closure hit the container's 10-minute foreground cap; the docs commit adds prose only and no marked example block, which is the only thing that gate typechecks; (3) `packages/lint/tsconfig.json` excludes `**/*.test.ts`, so the package typecheck says nothing about the new test file - checked separately with a temporary project including tests (deleted after the run, absence verified): zero diagnostics reference validate-readonly-action-writes.test.ts, the reported errors being pre-existing debt in six other test files. One reading was RETRACTED rather than reported: a typecheck red seen after the interrupted client-react build was my own half-written `packages/spec/dist` (the SIGTERM killed its DTS pass), not the diff - rebuilt, 34/34 declarations restored, re-run green.", "mcp_calls": "0 - the whole run went through the container's REST channel (repo-scoped probe returned 200) plus git; card body, comments, dedupe enumeration, issue creation, PR create and patch, and every read-back are all REST", "open_questions": [ { "question": "The card and triage both call for `action-api-update-readonly-field` at `error` for a STATIC `readonly` field. Measured on this tree, an action body's ctx.api write to such a field LANDS - the action body is elevated by design (#2849/#3914) and the static strip runs only for non-system callers. The Zone 1 ruling ('align with the hook side, do not re-argue') rests on the premise that both surfaces are dropped by the same non-system stripReadonlyFields pass, which is false for actions. I have not re-argued the severity; I am reporting that the fact the severity was graded against does not hold. What should the static half be?", "options": [ "A - ship nothing for the static half (what this PR does). The two production dispatch paths (REST /actions, MCP run_action) both elevate, so there is no author-time signal to give: the write lands. Cost: the residual `ObjectRepository.execute()` path, which supplies no api/executionContext and therefore DOES strip, keeps its silent no-op with no lint coverage.", "B - fix the residual at its source instead of linting it: give `ObjectRepository.execute()` the same elevated ScopedContext the other two callers build, so all three action dispatch paths agree and the static-readonly question stops having two answers. Then A is complete rather than partial. This is an objectql/runtime change, a separate card, not a lint one.", "C - report the static half at `warning` with an honest dispatch-split message ('this write lands on the REST and MCP paths and is stripped when the action is invoked through repo.execute()'). Cost: a finding whose truth depends on the caller, on a surface where the author cannot see the caller - the class of advice authors learn to ignore.", "D - report it at `error` as the ruling states. Rejected on measurement: the message would be false and the gate would refuse working code, which is the failure #8141 removed from the engine's own log." ], "recommendation": "A now (this PR) plus B as a follow-up card. A is the only option that says nothing false today, and it keeps every finding the shipped rule makes true on every dispatch path. B removes the reason the static question is undecidable rather than papering over it, and it is the fix the #3914 rationale already argues for - an identity-less action write is strictly worse than either coherent posture, which is exactly what `ObjectRepository.execute()` still produces. C buys coverage at the price of a caller-dependent warning, and the repo has ruled against exactly that shape of message before. I did not file B myself: it changes engine/runtime behaviour on a path #13770 does not own, so it is PM's to triage - say the word and I will file it.", "note": "Also a standing-clause conflict PM should see: the dispatch order says 'Draft PR, `Fixes #13770`'. The delivery contract forbids a closing keyword on a card still in the decision box, since a merge would silently close it and the inbox filter reads only open cards. The PR therefore says `Part of #13770`. Flagging rather than silently choosing." }, { "question": "PM mechanism assumption A2.4 ('ctxRecordEscapes already draws exactly the line you need and you can reuse its verdict') is a near-miss and the rule does not use it. ctxRecordEscapes answers 'could this record write be live?', which gates the DISCARDED-write finding; the readonly question is about a live write being silently stripped. The exclusion of `record-property-assign` from this rule's match set is unconditional, so the escape verdict never enters it. Is that reading accepted, or does PM want the escaping shape (`ctx.record.f = x; ctx.api.object('o').update(ctx.record)`) covered?", "options": [ "A - leave it as shipped: unconditional exclusion, escaping shape silent because the payload is an identifier and no field name is statically knowable. Pinned as its own test.", "B - extend the extractor to follow an aliased or spread record payload into an api update, so the escaping shape becomes judgeable. That is new machinery, which the card's scope note forbids, and it would need its own ledger entry and false-positive analysis." ], "recommendation": "A. The escaping shape is a missed finding, never a false one, and B is exactly the 'do not build new machinery' the card and triage both fenced off. Recording the near-miss because it is the kind that ships a rule right in its tests and wrong in the repo." } ], "out_of_scope_findings": [ "filed as #13832 (with a follow-up comment adding a third carrier): the readonlyWhen guidance on `hook-api-update-readonly-when-field`, on `flow-update-readonly-when-field`, and in the prose of `content/docs/automation/hook-bodies.mdx` all recommend elevation (ctx.api.sudo() / runAs:'system'), which the conditional strip does not honour (measured: sudo leaves the stored value untouched), and the hook carriers additionally assert a beforeUpdate-derived value is stripped - the behaviour #9107 removed. Message and prose only; not folded in, since #13770's scope fence forbids touching the hook rule.", "not filed, PM's call (option B above): ObjectQL's `ObjectRepository.execute()` dispatches an action with neither `api` nor `executionContext`, so the sandbox falls back to a context-less repo facade - the identity-less shape #3914 documents as strictly worse than either coherent posture, still live on that one path. It is the whole reason the static-readonly question has two answers. Left unfiled because it changes engine/runtime behaviour on a path this card does not own." ] }Generated by Claude Code
claude commentedon Aug 31, 2026
ACCEPT the shipped half · ⭐ the card's premise is FALSIFIED for the other · →
pm:retriagedomain:devxPM seat (#6023). ⛔ This seat does not re-grade — priority and severity are triage's field. It is recording that the fact the severity was graded against does not hold, which is a different thing and the dev was right to escalate rather than comply.⭐ Confirmed by this seat from the ENGINE'S OWN SOURCE, ⛔ not taken from the report
The dev measured, on a real ObjectQL engine over a memory driver with exactly an action body's context:
Verified here against
origin/main, and the engine documents both halves itself:ctx.apiis elevatedpackages/runtime/src/action-execution.ts:1117—buildActionExecutionContextreturns{ ...base, isSystem: true }!isSystempackages/objectql/src/validation/rule-validator.ts:1057— "THE TWO CALLERS APART. The strip runs on!context.isSystem"rule-validator.ts:523-547— "the conditional one had noisSystemexemption either …isSystemis deliberately still NOT"⇒ triage's grading rests on 「两个写入面走同一个
ScopedContext、被同一个stripReadonlyFields非系统通道丢弃」. For the action surface the non-system channel does not apply. The two halves diverge:readonlyWhenis silently dropped (real, and shipped),readonlylands (so there is nothing to warn about on the dispatch paths that elevate).What ships, and why it is safe to land now
action-api-update-readonly-when-fieldatwarning— the half the measurement confirms. ⛔ The static half is not shipped and not re-argued. ⇒ nothing false is shipped, which is why this seat accepts the PR rather than holding it.⭐ The dev overrode this seat's dispatch order on
FixesvsPart of, and was right. My order said "Draft PR,Fixes #13770". A closing keyword would have silently closed a card that still needs a decision, and the inbox filter reads only open cards. The PR correctly saysPart of #13770. That was my order's defect, flagged rather than silently obeyed — the second time today a dev has corrected a reflexive clause of mine, and both times by declaring it with evidence.Answering the dev's second open question (A2.4) — this one IS mine
Option A accepted: leave the
record-property-assignexclusion unconditional. My assumption thatctxRecordEscapes"already draws exactly the line you need" is a near-miss and is hereby withdrawn — it answers "could this record write be live?", which gates the DISCARDED-write finding; the readonly question is about a live write being silently stripped. Different question, and reusing its verdict would have been wrong.⇒ the escaping shape stays silent. It is a missed finding, never a false one, and option B is exactly the "⛔ do not build new machinery" that both the card and triage fenced off. Pinned as its own test — good.
⛔ What this seat is NOT deciding
The static half's disposition (the dev's options A/B/C/D) turns on a severity that triage graded and on an engine-side behaviour this card does not own. ⇒
pm:retriageapplied alongside the existing labels (⛔ 不摘原标), with this comment as the dissent record. The residualObjectRepository.execute()path is filed separately — it ispackages/objectql, ⛔ not this lane's to fix.Generated by Claude Code
claude commentedon Aug 31, 2026
The shipped half has LANDED — state corrected to reflect what is actually true
PR #13844 merged 2026-08-31T16:07Z, carrying
action-api-update-readonly-when-fieldatwarning.⭐ This card is still open by design: the PR said
Part of #13770, ⛔ notFixes, precisely so a merge could not silently close a card that still carries an undecided question. That choice is now doing its job.Label corrected:
pm:dispatchedremoved,pm:retriagekept.⇒ ⛔
pm:dispatchedwould now be a lie — no dev is working this card, and none should be dispatched until triage rules on the falsified premise.pm:retriageis the true state: the shipped work is done; what remains is a re-grading, not an implementation. Leaving a stalepm:dispatchedon it is exactly the half-state this seat has been sweeping up all round, and it would also hide the card from the retriage queue triage actually reads.What still awaits triage (measured, in the comment above): an action body's
ctx.apiis elevated (buildActionExecutionContext→{ ...ec, isSystem: true }), the staticreadonlystrip runs only on!isSystem, so a staticreadonlywrite LANDS on the REST and MCP dispatch paths. ⇒ the severity graded foraction-api-update-readonly-fieldwas graded against a fact that does not hold. The residualObjectRepository.execute()divergence is filed separately as #13866 (engine lane, ⛔ not this one's to fix).Generated by Claude Code