Repository navigation
fix(lint): one-line verdicts for the hook, action and flow record-write rules; os explain RULE_ID carries their reasoning - #22548
Conversation
…RULE_ID` carries their reasoning Stage 2 of the card, slice 3: the 14 rule ids of the hook-body, action-body and flow-node write rules and the three readonly write rules each print one verdict sentence. The reasoning moves into 14 RULE_EXPLANATIONS entries; the prose the three write surfaces shared is written once there (and the verdict clauses once in the rule files) so the families cannot drift apart. Rule ids, severities, paths, hints and accept/refuse behaviour are unchanged. Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…rint the new text Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…no dot Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 4 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 94e4033eb0d28b79e38bb96bb4f5004193311085 && git checkout 94e4033eb0d28b79e38bb96bb4f5004193311085
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin faf6348508519197c6047b46fb30b6ae8910f6b2 77821c2e1d2094a8f11dd25fb22dd6f4880228e2 && git checkout -B drift-repro faf6348508519197c6047b46fb30b6ae8910f6b2 && git merge --no-ff 77821c2e1d2094a8f11dd25fb22dd6f4880228e2
node scripts/docs-audit/affected-docs.mjs --json faf6348508519197c6047b46fb30b6ae8910f6b2
|
Part of #22161
Clause-②: no
Stage 2 of the card, slice 3: the 14 rule ids of the six record-write rules in
packages/lint, six whole source files. The card stays open for the later slices listed under "Remaining" below.What changes
messageof one verdict sentence. Every finding the rules' own suites fire is now 196 characters or fewer, and the CLI suite's handler-hook variants 195 or fewer (the longest of each id was 212 to 792 before). The author's values still close each verdict: the field, the object, thectx.apicall and the run identity. A hook lowered from an inlinehandlerkeeps its location suffix " (judged on the metadata body lowered from the inline handler)", and the bound holds with it.RULE_EXPLANATIONS(packages/lint/src/rule-explanations.ts), 14 new entries, soos explain RULE_IDprints it and the CLI'srule:line ends with the pointer for these ids. No CLI file changes:explainPointer()andos explainresolve any key the table holds (packages/cli/test/explain-rule-id.test.tsiterates every key; run below).path,hint(thefix:line) and what each rule accepts or refuses are unchanged. No condition, branch, dedupe key or skip moved; every hunk in the six rule files is amessageexpression, a comment, an import, or a shared verdict helper..changeset/22161-lint-slice-3-one-line.md:@objectstack/lintpatch, naming every door that prints the new text (below).Clause-②: nofollows the contract review on slice 1 (its section ②):RULE_EXPLANATIONSentries are data in an existing export.Shared prose: written once (the dispatch's H2)
H2 holds. Three families of sentences were shared across the action, hook and flow surfaces. Each was typed out separately in each rule, or concatenated from one long helper. Each now lives in one place for the verdict and one place for the explanation, and every id that uses it references it:
rule-explanations.ts)unprovisionedAnchorCause()plus the 5-sentenceunprovisionedAnchorWriteConsequence())*-write-unprovisioned-anchoridsunprovisionedAnchorWriteVerdict()invalidate-hook-body-writes.ts, over slice 2's one-clauseunprovisionedAnchorVerdict()(system-fields.ts, unchanged)UNPROVISIONED_ANCHOR_WRITE(3 paragraphs)*-write-unknown-fieldidsUNDECLARED_FIELD_WRITE_REFUSALandundeclaredApiWriteVerdict()invalidate-hook-body-writes.tsDECLARED_FIELD_DOORhook-body-source-unparseable,action-body-source-unparseablebodyParseFailureVerdict()invalidate-hook-body-writes.tsBODY_PARSE_FAILURE(2 paragraphs)readonlystrip and the conditionalreadonlyWhenstripREADONLY_WHEN_STRIP_SCOPE,READONLY_INSERT_STRIP_OUTCOMEinvalidate-readonly-flow-writes.ts(already the module the other two importbuildReadonlyIndexfrom)READONLY_STATIC_STRIP,READONLY_WHEN_STRIP(2 paragraphs), plusFLOW_FIELDS_CALLER_PAYLOADandHOOK_API_CALLER_PAYLOADfor the two sibling pairsunprovisionedAnchorWriteConsequence()had exactly these three callers and is not exported from the package barrel, so it is replaced, not left dead.unprovisionedAnchorCause()keeps its other callers in the files later slices will shorten.The shape, as
os lint's printer wrote it inpackages/cli/test/lint-hook-rules-reach-handler-hooks.test.tsagainst the rebuilt@objectstack/lintdist (where: message, recorded by the census preload below):The other verdict forms, one each, as the lint suite fired them:
Census (taken first, before any edit, at the base
faf6348508)Method: a scratch preload (
NODE_OPTIONS=--import, never committed) patchedArray.prototype.pushto record every finding-shaped object (rule+message) of the 14 ids, deduped by (rule, message), with its push site, in every vitest worker and every process a test spawns. Lengths aremessagealone; the printed line addswhereand:. Rows fromos lint's own printer (commands/lint.ts), whosemessageis the printedwhere: messageline, are excluded, as in slice 2.packages/lintsuite at the base: 131 files, 6,010 tests passed. All 14 ids fired, every one over 200.packages/clisuite, unit and integration, against the base's lint dist: 376 files, 373 passed, 4,961 tests passed and 35 skipped. Three files failed before any test:published-subpath-console.pin,published-subpath-hook-body.pinanddev-standalone-self-heal.integration. Each refused becausepackages/cliitself had nodist/(the census built the cli's dependency closure, not the cli). None of the three calls a lint rule (grep: nolintConfig,runAuthoringRules,ctx.apior rule id), so the census is complete for these ids. The cli suite fires three of the 14, all fromtest/lint-hook-rules-reach-handler-hooks.test.tswith the lowered-handler suffix:hook-body-write-unknown-field527 andhook-api-update-readonly-when-field323 (both longer than the lint suite's), andhook-api-update-readonly-field419 (the lint suite's 464 is longer).*-source-unparseableids at 212, carried by the parse diagnostic plus "; 2 syntax errors in total".system-fields.ts(unchanged) andchecked-parse.ts(unchanged). No id quotespackages/specrefusal prose, so none was dropped for that reason.hook-body-write-unprovisioned-anchorvalidate-hook-body-writes.tsaction-body-write-unprovisioned-anchorvalidate-action-body-writes.tsflow-node-write-unprovisioned-anchorvalidate-flow-node-writes.tshook-body-write-unknown-fieldvalidate-hook-body-writes.tshook-api-update-readonly-fieldvalidate-readonly-hook-writes.tsaction-body-write-unknown-fieldvalidate-action-body-writes.tsflow-node-write-unknown-fieldvalidate-flow-node-writes.tsflow-update-readonly-fieldvalidate-readonly-flow-writes.tsaction-api-update-readonly-when-fieldvalidate-readonly-action-writes.tshook-api-update-readonly-when-fieldvalidate-readonly-hook-writes.tsflow-update-readonly-when-fieldvalidate-readonly-flow-writes.tsaction-record-write-discardedvalidate-action-body-writes.tshook-body-source-unparseablevalidate-hook-body-writes.tsaction-body-source-unparseablevalidate-action-body-writes.tsThe per-id message counts are unchanged, so no two variants of an id collapsed into one sentence. The cli "after" column is the four cli files that call these rules (run below), not the whole suite again.
Doors that print the new text
Read from the registry (
authoring-rules.ts: the reference-integrity suite entry iscommands: ALL,runtimeTypes: ['flow', 'view', 'object', 'dataset', 'report']), the suite's per-memberruntimeTypes(reference-integrity-suite.ts: the six members carry the frozen['flow']default) and the runtime gate (lint/src/runtime-gate.ts,metadata-protocol/src/runtime-authoring-gate.ts):os validate,os build(andos compile, whichos devruns per compile),os lint,os verify,os initscaffold checkrule:line gains theos explainpointer;os validate --jsonwarningsandos build --jsonauthor-timeissuescarry the newmessageflowwrites (Studio, REST/meta, MCP)flow-node-write-unknown-field,flow-update-readonly-field): the 422 issuemessageand theOS_ALLOW_UNLINTED_METADATA_WRITESrefusal log line. Warnings (flow-node-write-unprovisioned-anchor,flow-update-readonly-when-field): the 2xxadvisoriesmessageand the deduped[Protocol] authoring advisorylog linehookoractionwrite does not dispatch the suite (the body members parse JavaScript, whichruntime-lazy-deps.test.tspins off the publish path), and aflowsnapshot carries no hook or action bodyhint; every other rule idEvery row is named in the changeset.
Tests (head
77821c2e1d)explainRule(id)exists and still names what the verdict stopped saying. The measured-refusal pins (INVALID_FIELD / 400, "identically on every driver", "before any statement is built", "ordinary CALLER write", what fails) moved from the message to the explanation. The retired driver-split negatives now hold over both the message and the explanation. Every refusal assertion, rule id, severity,path,whereand hint pin is unchanged.pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2gave Test Files 131 passed (131), Tests 6,036 passed (6,036); lock VERDICT command-exit 0. The first run, at4c78055635, was red in exactly one case:rule-explanations.test.tsrefuses a dot incovers, andaction-record-write-discarded's phrase read "ctx.record".77821c2e1drewords it.pnpm --filter @objectstack/lint run typecheckgave VERDICT command-exit 0.check:test-typecheckOK: 2 files, 6 errors and 2 pinned signatures held.@objectstack/lintdist (marker grep: 1 hit each indist/rule-explanations.jsand.cjs):pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/explain-rule-id.test.ts test/commands.test.ts test/lint-hook-rules-reach-handler-hooks.test.ts test/validate-build-gate-parity.test.ts src/flow-node-undeclared-field-write.integration.test.ts test/lint-hook-rules-reach-handler-hooks.e2e.test.tsgave Test Files 5 passed (5), Tests 107 passed (107). Of the six named paths,vitest listselects the first five, fourunitand oneintegration. The flow-node file pins the runtime refusal the verdict names, not the message. The sixth, the.e2efile, selected nothing: it is nightly-tier.explain-rule-id.test.tsiterates everyRULE_EXPLANATIONSkey, so all 14 new ids resolve throughos explainandexplainPointer. The nightly-tier file then ran on its own afterpnpm turbo run build --filter=@objectstack/cli:OS_TEST_TIERS=nightly … vitest run test/lint-hook-rules-reach-handler-hooks.e2e.test.tsgave 1 file, 10 passed. It spawnsos build/os lint, and everyhook-api-update-readonly-fieldverdict it fired is at most 187 characters.repoproject (the dispatch's H3):pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2gave Test Files 54 passed (54), Tests 915 passed (915); lock VERDICT command-exit 0. It walkspackages/lint/srcas a text corpus. The new explanations carry noos migrate metasentence (grep: 0), nosecurity-*rule id constant, and no retired key.unprovisionedAnchorWriteConsequence(), the one removed function, had exactly the three in-slice callers (grep at the base) and is not on the package barrel.system-fields.tsandchecked-parse.tsare untouched, so no other rule's message can move. The per-id message counts in the census table are equal before and after.77821c2e1d, throughscripts/ablation-replace.mjswrap mode, trap-armed). The tests import the rule source, so there is no dist leg.action-record-write-discarded's pre-slice message was restored verbatim through the anchor of its new two-line message: anchor x1 to x0, blob134c72868e97to1a67d89f7b73.src/validate-action-body-writes.test.tsthen gave Test Files 1 failed (1), Tests 1 failed | 44 passed (45). The one red case was the bound pin, "every verdict the cases above fired for those ids is one line of at most 200 characters" (the restoredctx.record.stageverdict, 291 characters). Restored: blob after restore134c72868e97== blob at HEAD,git diff HEADempty,git status --porcelainempty.npx eslint --no-inline-config --format jsonover the 13 changed.tsfiles (git diff --name-only faf6348508..HEAD) gave 13 files in the report, 0 errors, 0 warnings, none ignored.eslint.config.mjsnever enables type-aware linting (its comment at:327), so no untouched file's verdict can move. A control-character scan of every changed file found no match, andcheck:nul-bytesexits 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon the actual diff (14 paths vs merge basefaf634850) derived 62 commands, identical to the dispatch's 62. I also ran the 4 artifact-roster gates it flags as rostered in a directory this diff touches:check-changeset-fixed,check:authz-resolver,check:error-code-casingandcheck:filter-alias-parity. All 66 ran sequentially, each with its own log and an exit code captured before any pipe, at77821c2e1d; 64 exited 0. Two exited 3 with PREREQUISITE NOT MET, which is not a measurement.check:dual-build-cjs-loadslacked eight packages'dist/and exited 0 after building them.check:type-check-debt's re-measure build had a message-less tsup DTS-worker exit inplugin-approvalson the shared box, and its re-run exited 0 ("1 ledger entr(ies) re-measured … none above its recorded number").--rangave✓ dispatch-gates --ran: 62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED.Remaining for later slices — 80 ids in
packages/lint, by fileSlice 2's list (PR #22448, census at
05c7c3fa3b; longest fired message, "cli" marks a length only the cli suite reached) minus this slice's six files and 14 ids. The lengths are slice 2's, not re-measured here.data-model-rules.ts(6):unique/legacy-organization-composite480 cli,relationship/master-detail-required462,unique/unscoped-declared-index427,unique/double-declaration402 cli,rollup/non-numeric-aggregand364,relationship/delete-behavior201 clivalidate-flow-trigger-readiness.ts(5):flow-time-relative-descriptor-invalid796,flow-time-relative-descriptor-unroutable532,flow-trigger-unroutable518,flow-api-trigger-secret-missing336,flow-trigger-unknown-event222validate-react-page-props.ts(5):react-chart-drilldown-invalid784,react-chart-aggregate-invalid507,react-chart-field-unprovisioned429,react-block-needs-record-context267,react-page-source-unparseable211validate-rls-predicate-enforceability.ts(5):rls-predicate-unparseable2056,rls-predicate-unenforceable1679,rls-predicate-unknown-user-variable1544,rls-predicate-unknown-field1399,rls-predicate-over-budget1259validate-sharing-rule-enforceability.ts(4):sharing-rule-unlowerable-condition1093,sharing-rule-object-not-shareable774,sharing-rule-object-controlled-by-parent660,sharing-rule-runtime-variable-condition492validate-ai-agent-authoring.ts(3):default-agent-legacy-alias466,default-agent-outside-roster388,agent-authoring-withdrawn352validate-predicate-path-refs.ts(3):predicate-rhs-path-shaped648,predicate-path-unrooted434,predicate-path-unresolved371validate-searchable-fields.ts(3):searchable-field-unprovisioned512,searchable-field-unsearchable449,searchable-field-unknown295validate-sortable-fields.ts(3):sort-field-unprovisioned844,sort-field-unsortable369,sort-field-unknown287lint-view-refs.ts(2):view-ref-nav-view-missing437,view-key-collision278 clivalidate-approval-approvers.ts(2):approval-approvers-may-resolve-empty451,approval-approver-not-membership-tier272validate-chart-bindings.ts(2):chart-measure-unknown442,chart-axis-not-selected327validate-dashboard-action-refs.ts(2):dashboard-action-route-unresolved242,dashboard-action-target-undefined239validate-dataset-measure-aggregates.ts(2):measure-aggregate-field-type-refused809,dimension-json-stored-field-refused531validate-empty-combinators.ts(2):filter-empty-combinator352,filter-empty-node226validate-list-view-field-refs.ts(2):list-view-field-dotted445,list-view-field-unknown355validate-rule-compilability.ts(2):validation-rule-json-schema-uncompilable517,validation-rule-regex-uncompilable482validate-translation-references.ts(2):translation-target-unknown345,translation-option-key-unknown230lint-flow-credential-literals.ts(1):flow-credential-literal390validate-action-dispatch-contract.ts(1):action-dispatch-contract-mismatch927validate-action-name-refs.ts(1):action-name-undefined409validate-ai-surface-affinity.ts(1):ai-skill-surface-mismatch281validate-ai-tool-references.ts(1):ai-skill-tool-unresolved441validate-capability-references.ts(1):capability-reference-unknown219validate-component-types.ts(1):component-type-unknown807validate-flow-filter-tokens.ts(1):flow-filter-token-unknown278validate-managed-api-methods.ts(1):object/managed-api-method-unaffordable412validate-mapping-target-fields.ts(1):mapping-target-field-unknown466validate-nav-access.ts(1):nav-object-ungranted353validate-nav-object-servability.ts(1):nav-object-unservable528validate-nav-target-refs.ts(1):nav-target-unresolved426validate-object-field-refs.ts(1):object-field-ref-unknown320validate-object-references.ts(1):object-reference-unregistered-platform324validate-org-axis-red-lines.ts(1):org-axis-cross-org-bu-grant365validate-page-visualization-bindings.ts(1):page/visualization-without-binding629validate-preset-comparands.ts(1):filter-preset-comparand669validate-retired-permission-residue.ts(1):permission-retired-lifecycle-residue235validate-rule-schema-formats.ts(1):validation-rule-json-schema-unknown-format1049validate-seed-replay-safety.ts(1):seed-insert-mode-duplicates-on-replay222validate-seed-state-machine.ts(1):seed-value-outside-state-machine320validate-semantic-roles.ts(1):semantic-role-field-unprovisioned291validate-translatable-sections.ts(1):translation-section-name-missing553validate-view-containers.ts(1):view-container-shape290The 26 ids slice 2 fenced, the 9 ids owned by
packages/cli, and theaction-governance.tsboot-log lines stay as PR #22448's body lists them. This slice touched none of them.Acceptance notes
fix:lines are unchanged and several stay long. The dispatch holds hints as they are. The readonly hook hints run to several hundred characters (therunAs: 'system'/ own-hook-stamp /sudo()remedy), and so does the flowreadonlyWhenhint. Whether hints get the same one-line budget is the card's call, as slice 2's report already noted. Some explanation paragraphs restate reasoning those long hints also carry (elevation does not waivereadonlyWhen,sudo()is not marshalled into the sandbox). Slice 2's entries overlap their hints the same way.pathishooks[i].handler, andpackages/clipins its wording, so it stays verbatim. Every combination the suites fire is at most 196. A combination no suite fires can pass 200: a hook lowered from ahandlerthat INSERTs a long-named readonly field (239 with the fixtures' names). The changeset says a verdict over a long name grows with it.unprovisionedAnchorCause()keeps five callers (validate-flow-template-paths.ts,validate-page-field-bindings.ts,validate-react-page-props.ts,validate-searchable-fields.ts,validate-sortable-fields.ts). They converge onunprovisionedAnchorVerdict()when their slices shorten them, as slice 2's docblock states.os lintprintswhere: message. For a lowered hook its printed line reaches 215 characters with thewhereprefix. That is the printer's shape, stage 1's, carried as before.Generated by Claude Code