Skip to content

lint: flow-update-readonly-when-field skips runAs:'system' flows entirely, but the conditional strip has no isSystem exemption #14201

Description

@baozhoutao

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

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions