Found while correcting that rule's hint text in #13832. Not folded into that PR — #13832's triage fenced it to message text only (「⛔ rule id / severity / match set 一律不动」), and this is a match set question.
The defect
packages/lint/src/validate-readonly-flow-writes.ts returns early for any elevated flow:
// `runAs` defaults to 'user' (schema default). Only an explicit 'system'
if (flow.runAs === 'system') return;
That skip is correct for the static readonly branch: the engine's stripReadonlyFields runs under if (!opCtx.context?.isSystem), so a runAs:'system' flow legitimately maintains readonly columns — the intended channel, rightly unflagged.
It is not correct for the readonlyWhen branch. stripReadonlyWhenFields is called on the update path with no isSystem guard at all. The engine says so at the call site, in the #9107 note in packages/objectql/src/engine.ts:
isSystem is still NOT an exemption here, unlike the static strip below.
Pinned from both sides:
engine-readonly-when-derived-writes.test.ts — "LOCK 2 — isSystem does NOT exempt a caller-supplied value (Option B stays rejected)".
engine-readonly-strict-writes.test.ts — "covers readonlyWhen too — the arm a trusted (isSystem) caller can still hit".
⇒ A runAs:'system' flow whose update_record node writes a readonlyWhen field is still stripped on records whose predicate is TRUE, and the rule says nothing about it. The one class of flow the author was (until #13832) explicitly told to reach for is the class the rule stops inspecting.
Why the early return is wider than it should be
The skip is a single return at flow level, so it removes the flow from both branches at once. The static branch needs it; the conditional branch does not. Whether the fix is to narrow the early return to the static branch, or to keep the skip and accept the blind spot, is a real scope decision — a runAs:'system' flow writing a readonlyWhen field is a legitimate authoring shape whose write silently vanishes per record state, which is the same silent-no-op class the rule family exists to surface.
Worth weighing against false-positive cost: the finding would be a warning (the conditional branch's existing severity), not a gate, so it does not block a build.
Scope note
Message text for all three readonlyWhen carriers is being corrected in #13832 — that PR leaves this skip exactly as it is and records the gap in a comment beside it, so nothing here is a regression from it. This card is only about the match set.
Generated by Claude Code
Found while correcting that rule's hint text in #13832. Not folded into that PR — #13832's triage fenced it to message text only (「⛔ rule id / severity / match set 一律不动」), and this is a match set question.
The defect
packages/lint/src/validate-readonly-flow-writes.tsreturns early for any elevated flow:That skip is correct for the static
readonlybranch: the engine'sstripReadonlyFieldsruns underif (!opCtx.context?.isSystem), so arunAs:'system'flow legitimately maintainsreadonlycolumns — the intended channel, rightly unflagged.It is not correct for the
readonlyWhenbranch.stripReadonlyWhenFieldsis called on the update path with noisSystemguard at all. The engine says so at the call site, in the #9107 note inpackages/objectql/src/engine.ts:Pinned from both sides:
engine-readonly-when-derived-writes.test.ts— "LOCK 2 — isSystem does NOT exempt a caller-supplied value (Option B stays rejected)".engine-readonly-strict-writes.test.ts— "covers readonlyWhen too — the arm a trusted (isSystem) caller can still hit".⇒ A
runAs:'system'flow whoseupdate_recordnode writes areadonlyWhenfield is still stripped on records whose predicate is TRUE, and the rule says nothing about it. The one class of flow the author was (until #13832) explicitly told to reach for is the class the rule stops inspecting.Why the early return is wider than it should be
The skip is a single
returnat flow level, so it removes the flow from both branches at once. The static branch needs it; the conditional branch does not. Whether the fix is to narrow the early return to the static branch, or to keep the skip and accept the blind spot, is a real scope decision — arunAs:'system'flow writing areadonlyWhenfield is a legitimate authoring shape whose write silently vanishes per record state, which is the same silent-no-op class the rule family exists to surface.Worth weighing against false-positive cost: the finding would be a warning (the conditional branch's existing severity), not a gate, so it does not block a build.
Scope note
Message text for all three
readonlyWhencarriers is being corrected in #13832 — that PR leaves this skip exactly as it is and records the gap in a comment beside it, so nothing here is a regression from it. This card is only about the match set.Generated by Claude Code