Uh oh!
There was an error while loading. Please reload this page.
fix(lint): inspect runAs:'system' flows on the readonlyWhen branch (#14201) - #14370
Conversation
The runAs:'system' exemption in validate-readonly-flow-writes was a single flow-level early return, removing an elevated flow from BOTH branches of the rule. Only the static branch warrants it: the engine skips stripReadonlyFields under `if (!opCtx.context?.isSystem)`, while stripReadonlyWhenFields runs on the update path with no isSystem guard at all. The exemption now gates the static branch only, so a system flow whose update_record node writes a readonlyWhen field reports the conditional branch's existing warning. Rule ids and severities are untouched.
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e8cc6a53793d2fb2a4debec4a1275eb13d0c59e2 && git checkout e8cc6a53793d2fb2a4debec4a1275eb13d0c59e2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d62f990a985eb15152579ad34c99f43e76e3657a 8b99b5c2dda048511ebde049bb0311052bb6b81b && git checkout -B drift-repro d62f990a985eb15152579ad34c99f43e76e3657a && git merge --no-ff 8b99b5c2dda048511ebde049bb0311052bb6b81b
node scripts/docs-audit/affected-docs.mjs --json d62f990a985eb15152579ad34c99f43e76e3657a |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14201
What changed
packages/lint/src/validate-readonly-flow-writes.tscarried therunAs:'system'exemption as a single flow-level early return, so an elevated flow left the walk
before either branch of the rule could judge it. That skip now gates the static
readonlybranch only; thereadonlyWhenbranch inspects arunAs:'system'flowlike any other, at its existing
warningseverity. Rule ids and severities areuntouched.
A field declaring both flags therefore falls through to the conditional branch
under
runAs:'system'— which is the truth about that write: the static strip isskipped, the conditional one is not.
Engine authority (verified on
origin/main, not recalled)packages/objectql/src/engine.ts, the by-id update path — the conditional strip runswith no guard, and the note at the call site says so:
The static strip, twenty lines below, is the one an elevated caller really does bypass:
The bulk path (
stripReadonlyWhenFieldsMulti/stripReadonlyFields) is split thesame way, so the asymmetry is not a by-id accident. Pinned from both sides:
engine-readonly-when-derived-writes.test.tsline 195, "LOCK 2 - isSystem does NOTexempt a caller-supplied value (Option B stays rejected)", and
engine-readonly-strict-writes.test.ts.The rule's own header already recorded this gap ("the skip above is therefore WIDER
than the conditional lock warrants ... Left as-is deliberately") — that paragraph is
now rewritten to describe the split it documents. The action sibling
validate-readonly-action-writes.tswas born with exactly this shape: an action bodyis system-elevated by design, so it carries the conditional half and only that half.
Tests
packages/lint/src/validate-readonly-flow-writes.test.ts— 5 new cases, existing oneskept:
runAs:'system'flow whoseupdate_recordwrites areadonlyWhenfield ⇒ one
warningonflow-update-readonly-when-field, message naming the runidentity it was judged under;
runAs:'system'flow writing a staticreadonly:truefield ⇒ still silent;half only, no error;
readonlyandreadonlyWhen⇒ warning under system, errorunder user;
create_recordexemption, both underrunAs:'system'.Commands run (all at head
8b99b5c2d, exit codes captured before any pipe)pnpm --filter '@objectstack/lint^...' buildpnpm --filter @objectstack/lint testpnpm --filter @objectstack/lint typechecknode scripts/pm/dispatch-gates.mjs --commandsunion (33 families)node scripts/pm/dispatch-gates.mjs --ranThe three exit-3 rows are prerequisite-not-met, i.e. NOT MEASURED locally and not
findings, each saying so in its own verdict text:
check-test-completeness.mjs(gradesa saved
turbo run testlog CI tees),check:dual-build-cjs-loadsandcheck:type-check-debt(both need the whole workspace built).Reverse verification: with the flow-level
returnre-injected and the rest of thechange left in place, the rule test file goes 4 failed / 20 passed — exactly the
four new
runAs:systemcases — and back to 24 passed restored. The mutation wasconfirmed on disk (unique anchor asserted, injected-marker
grep -c= 1, mutatedgit hash-objectdiffers from theHEADblob) and the restore proved by hash equalitywith the
HEADblob plus an emptygit diff HEAD; no build was involved because thetest imports the rule module by relative source path, not through
dist/.ESLint was run narrowed rather than repo-wide, and the narrowing is measured: this
repo runs one
eslint.config.mjsthat never enables type-aware linting for any file(no
parserOptions.project, no typed@typescript-eslintrules — stated and measuredin that config's own header), so no file's verdict depends on a file outside itself and
a two-file diff cannot move an untouched file's result.
eslint --no-inline-config --format jsonover the diff reports 3 result entries: 0 errors / 0 warnings on both.tsfiles, and the changeset.mdreported as ignored (no matching configuration),so 2 lintable files, both clean. The repo-wide sweep is CI's
Lint & Repo Gatesrun.Not in scope
The
readonlyWhenhint text is left exactly as #14202 landed it. It reads correctlyunder an elevated run too — "the conditional lock is NOT waived by a system context" is
precisely what a
runAs:'system'author needs told — and message text is that card'sfence, not this one's.
Generated by Claude Code