Repository navigation
fix(lint): one-line verdicts for the widget, dataset, security-posture and visibility rules; os explain RULE_ID carries their reasoning - #22448
Conversation
…d visibility rules Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…verdict shape and explanations Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…dicts in one line Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…oor that prints them Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…line verdict Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 10 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 12b0d2aebfa2b68035e913c45db4501a29d29cf3 && git checkout 12b0d2aebfa2b68035e913c45db4501a29d29cf3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2b61f2d9d6f8f7cc7f6b9f5f9772b8bc8fb0ea5d 6258d37185cd68bcf25bf6a0387b27de2190165a && git checkout -B drift-repro 2b61f2d9d6f8f7cc7f6b9f5f9772b8bc8fb0ea5d && git merge --no-ff 6258d37185cd68bcf25bf6a0387b27de2190165a
node scripts/docs-audit/affected-docs.mjs --json 2b61f2d9d6f8f7cc7f6b9f5f9772b8bc8fb0ea5d
|
…eta sentence; the fix line carries the house form Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
Part of #22161
Clause-②: no
Stage 2 of the card, slice 2: 26
packages/lintrule ids, four 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 197 characters or fewer (the longest of each id was 205 to 697 before). The values the author wrote still close the verdict: field paths, the candidate roster, the selection list, and the predicate excerpt, now capped at 72 characters.visibility-predicate-over-budgetno longer echoes the predicate at all; itspathlocates it.RULE_EXPLANATIONS(packages/lint/src/rule-explanations.ts), 26 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. Every fired message of every other rule id is byte-identical, measured below.unprovisionedAnchorVerdict()insystem-fields.tsis the one-clause form ofunprovisionedAnchorCause(), for a rule whose finding is one sentence (heredashboard-filter-field-unprovisioned). The eight rule files still on the long clause converge on it when their slices shorten them..changeset/22161-lint-slice-2-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.The shape, through the CLI's real printers against the rebuilt
@objectstack/lintdist (a scratch stack,runAuthoringRules('build', …)):(The
fix:line prints the rule's unchangedhint; its placeholder is spelledNAMEhere only because this page's sanitizer eats angle-bracket tokens.)Census (taken first, before any edit)
Method: a scratch preload (
NODE_OPTIONS=--import, never committed) recorded every finding-shaped object (rule+message) pushed into an array or produced bymap/flatMap, deduped by (rule, message), with its push site, in every vitest worker and every process a test spawns.packages/lintsuite at05c7c3fa3b(the base): 128 files, 5,894 tests passed; 241 rule ids fired, 147 over 200 characters.packages/clisuite, unit and integration, against the base's lint dist: 372 files, 4,961 passed / 2 skipped. It fires 75 ids. Rows fromos lint's own printer (commands/lint.ts), whosemessageis the printedwhere: messageline, are excluded. It adds one over-200 lint id the lint suite never fires that long (relationship/delete-behavior201), longer variants of five others (marked below), and nine over-200 ids owned bypackages/cli.metadata-protocol,specand example suites (the functional-completeness ids were measured over the spec suite by slice 1). The census counts what some test fires; a variant no test fires is not in it, andchart-config-missing,react-prop-deprecated,relationship/association-inline-editandrollup/missing-summaryfired in neither suite.messagealone; the printed line addswhereand:.Author-time ids over about 200 characters, after slice 1: 146 in
packages/lint(26 this slice, 26 in fenced files, 94 later), plus 9 inpackages/cliand the action-governance boot-log lines inpackages/objectql/src/action-governance.ts(named by stage 1, its own later slice).lint-startup-registry-verdict.ts's two ids (779, 731) belong to the repo gatecheck:startup-registry-verdict, not to an author, and are not counted.This slice — 26 ids, before → after (every distinct message each id fires in the
packages/lintsuite)dashboard-filter-field-unprovisionedvalidate-widget-bindings.tschart-field-unknownvalidate-widget-bindings.tsdashboard-filter-field-not-includedvalidate-widget-bindings.tswidget-filter-field-unknownvalidate-widget-bindings.tsdashboard-filter-field-unknownvalidate-widget-bindings.tschart-dimensions-missingvalidate-widget-bindings.tswidget-filter-field-not-includedvalidate-widget-bindings.tswidget-measures-missingvalidate-widget-bindings.tswidget-sortby-unselectedvalidate-widget-bindings.tschart-measures-missingvalidate-widget-bindings.tswidget-legacy-analytics-unrenderablevalidate-widget-bindings.tssecurity-controlled-by-parent-ambiguous-relationvalidate-security-posture.tssecurity-fls-unknown-fieldvalidate-security-posture.tssecurity-controlled-by-parent-no-relationvalidate-security-posture.tssecurity-master-detail-ungrantedvalidate-security-posture.tssecurity-owd-aliasvalidate-security-posture.tssecurity-delegation-missing-reasonvalidate-security-posture.tsvisibility-predicate-unknown-functionvalidate-visibility-predicates.tsvisibility-predicate-over-budgetvalidate-visibility-predicates.tsvisibility-bare-identifiervalidate-visibility-predicates.tsvisibility-predicate-syntaxvalidate-visibility-predicates.tsvisibility-root-mislayeredvalidate-visibility-predicates.tsdataset-field-not-includedvalidate-dataset-references.tsdataset-field-unknownvalidate-dataset-references.tsdataset-filter-field-unknownvalidate-dataset-references.tsdataset-include-unknownvalidate-dataset-references.tsThe message counts fall for three ids because the old messages repeated the object's name (the two
controlled_by_parentids) or echoed the predicate (visibility-predicate-over-budget), which the finding'swhereandpathalready carry.Fenced off by the claim — 26 ids (open PRs and the serial flow-template queue)
lint-flow-patterns.ts(15):flow-multi-write-unfiltered656,flow-decision-mode-invalid528,flow-loop-body-uncontained522,flow-try-catch-without-catch520,flow-approval-revise-target-not-service-owned366,flow-decision-unconditional-branch342,flow-error-label-not-fault315,flow-inert-node-condition286,flow-runas-unscoped284,flow-branch-label-unmatched272,flow-decision-inclusive-overlap250,flow-default-edge-with-condition239,flow-multiple-default-edges211,flow-time-relative-antipattern208,flow-date-equality-filter208validate-flow-template-paths.ts(3):flow-template-field-unprovisioned440,flow-template-lookup-traversal349,flow-template-unknown-field258validate-component-props.ts(2):component-props-invalid994,component-props-unknown-key844validate-page-field-bindings.ts(2):page-field-unprovisioned484,page-section-group-unknown214validate-expressions.ts(1):expression-invalid2077validate-filter-tokens.ts(1):filter-token-unknown386validate-form-layout.ts(1):form-section-group-unknown214validate-print-page-blocks.ts(1):print-page-block-unprintable368Remaining for later slices — 94 ids in
packages/lint, by file (longest fired message; "cli" marks a length only the cli suite reached)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-action-body-writes.ts(4):action-body-write-unprovisioned-anchor778,action-body-write-unknown-field433,action-record-write-discarded293,action-body-source-unparseable212validate-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-hook-body-writes.ts(3):hook-body-write-unprovisioned-anchor792,hook-body-write-unknown-field527 cli,hook-body-source-unparseable212validate-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-flow-node-writes.ts(2):flow-node-write-unprovisioned-anchor739,flow-node-write-unknown-field421validate-list-view-field-refs.ts(2):list-view-field-dotted445,list-view-field-unknown355validate-readonly-flow-writes.ts(2):flow-update-readonly-field410,flow-update-readonly-when-field317validate-readonly-hook-writes.ts(2):hook-api-update-readonly-field464,hook-api-update-readonly-when-field323 clivalidate-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-readonly-action-writes.ts(1):action-api-update-readonly-when-field396validate-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-shape290Outside
packages/lint— 9 ids owned bypackages/cli(fired by the cli suite)packages/cli/src/lint/hook-body-lowering.ts(4):hook-body/not-lowerable661,hook-body/unparseable469,hook-body/bundled-fallback334,hook-body/extraction-failed331packages/cli/src/utils/collect-docs.ts(4):docs/uncollected-directory558,docs/duplicate-name330,docs/nav-target299,docs/frontmatter-tags285packages/cli/src/utils/picklist-references.ts(1):picklist-reference-unverified245Picking the slice
Whole files, fenced files excluded, most over-200 ids first, at most 30:
validate-widget-bindings.tsvalidate-security-posture.tsdata-model-rules.tsrelationship/delete-behaviorat 201)relationship/master-detail-requiredandrollup/non-numeric-aggregandhave no exported rule id constant, and aRULE_EXPLANATIONSkey must be one (rule-explanations.test.ts). Exporting two new constants adds public surface, which is notClause-②: no.validate-visibility-predicates.tsvalidate-flow-trigger-readiness.tsflow-time-relative-descriptor-invalid(796) ends with the time-relative trigger schema's own refusal prose, which lives inpackages/spec, so it needs a spec editvalidate-react-page-props.tsreact-chart-drilldown-invalid(784) andreact-chart-aggregate-invalid(507) are the chart schemas' own refusal prose (packages/spec/src/ui/chart.zod.ts)validate-rls-predicate-enforceability.tsvalidate-dataset-references.tsvalidate-action-body-writes.ts,validate-sharing-rule-enforceability.tsvalidate-hook-body-writes.tsandvalidate-flow-node-writes.ts, a nine-id family to shorten together. The second shares the RLS consequence prose.So the slice is 11 + 6 + 5 + 4 = 26 ids.
Doors that print the new text
Read from the registry entries (
authoring-rules.ts,reference-integrity-suite.ts) and the runtime gate (metadata-protocol/src/runtime-authoring-gate.ts,lint/src/runtime-gate.ts):os validate,os build(andos compile, whichos devruns per compile),os lint,os verify,os initscaffold checkcommands: ALL)rule:line gains theos explainpointer;os validate --jsonwarningsandos build --jsonauthor-timeissuescarry the newmessageos doctordashboard widget checkwhere: message, norule:line (see Acceptance notes)dashboardwrites (Studio, REST/meta, MCP)messageand theOS_ALLOW_UNLINTED_METADATA_WRITESrefusal log line. Warnings (dashboard-filter-field-unprovisioned,chart-field-unknown,chart-measures-missing,widget-measures-missing,chart-dimensions-missing): the 2xxadvisoriesmessageand the deduped[Protocol] authoring advisorylog linedatasetwritesmessage, the hatch log lineviewwritesvisibility-root-mislayeredis a warning (the 2xxadvisories, the advisory log)object/permission/seedwritessecurity-controlled-by-parent-*(object),security-fls-unknown-field(permission),security-master-detail-ungranted(object or permission, a warning),security-delegation-missing-reason(seed)security-owd-aliashint; every other rule idEvery row is named in the changeset.
Tests (round 0, head
7564f71b04)explainRule(id)exists and still names what the verdict stopped saying. Existing message pins now read the verdict's substance, the path, names and outcome, never the full prose; every refusal assertion is unchanged.pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2→ Test Files 128 passed (128), Tests 5,928 passed; lock VERDICT command-exit 0.pnpm --filter @objectstack/lint run typecheck→ VERDICT command-exit 0.check:test-typecheckOK: 2 files, 6 errors and 2 pinned signatures held.pnpm --filter @objectstack/lint build:pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 test/explain-rule-id.test.ts test/commands.test.ts→ 2 files, 62 passed.explain-rule-id.test.tsiterates everyRULE_EXPLANATIONSkey, so all 26 new ids resolve throughos explainand throughexplainPointer. The two sets stay disjoint from the schema names. The rebuilt dist carries the new entries (marker grep: 1 hit each indist/rule-explanations.jsand.cjs).metadata-protocol, three runtime-gate files (runtime-authoring-gate.dataset-writes,protocol-publish-drafts-closure,protocol.dashboard-dataset-publish-gate) → 3 files, 21 passed;examples/app-showcase, three files (nav-and-detail-grants,dashboard-filter-vocabulary,my-work-visibility) → 3 files, 14 passed, after building the showcase's dependency closure.scripts/ablation-replace.mjs(wrap mode, trap-armed). The tests import the rule source directly, so no dist leg applies.chart-dimensions-missing's verdict lengthened to 240 characters (blobc07dd85c→449306c6) →validate-widget-bindings.test.ts1 failed | 161 passed, the one-line case.BY NAMEremoved from thechart-field-unknownexplanation (blobda3574bc→7288a22f) → 1 failed | 161 passed, that id's explanation case.git diff HEADempty,git status --porcelainempty.npx eslint --no-inline-config --format jsonover the 11 changed.tsfiles → 11 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.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 62 commands from the 11 changed paths (merge base05c7c3fa3). All 62 ran sequentially, each with its own log and an exit code captured before any pipe; 61 exited 0 on the first run.pnpm check:dual-build-cjs-loadsexited 3, PREREQUISITE NOT MET, because eight packages outside this tree's builds had nodist/; after building them it exited 0.--ran→✓ dispatch-gates --ran: 62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED. A control-character scan of every changed file: no match.Patch round 1 (head
6258d37185)7564f71b04went red in@objectstack/spec'srepoproject:retired-key-migrate-sentence.test.tswalkspackages/lint/srcand judged the newchart-field-unknownexplanation's sentence "os migrate meta --from 17lists the mechanical edits for existing sources" neither house-form nor MIXED (REWORK6078055875on [maintainer] validate: thefield-no-consumerswarning is one 856-character line, printed by validate, build and dev alike — one-line verdict +rule:id + a pointer to the full reasoning (os explain, which today takes only schema names) #22161).77a246b85ddrops that sentence from the explanation; the house sentence still prints on the rule's xAxis fix line invalidate-widget-bindings.ts. The rule's MOVED-fact pin now names "refused on a dataset-bound widget".repoproject 54 files / 915 passed; with the old sentence restored, exactly the threeretired-key-migrate-sentence.test.tscases go red (3 failed / 912 passed). Lint suite 129 / 5,938.origin/mainmerged twice (2e5eb5aed3,6258d37185); the net diff is unchanged in scope (12 files, +903 / −197).6079174263on [maintainer] validate: thefield-no-consumerswarning is one 856-character line, printed by validate, build and dev alike — one-line verdict +rule:id + a pointer to the full reasoning (os explain, which today takes only schema names) #22161; seat ACCEPT6079204385.Acceptance notes
os doctorprints the widget ids without arule:line. Its dashboard check printswhere: message, with thehintonly under--verbose. It never named the rule id, and theos explainpointer stage 1 put on the other printers never reached it, so a doctor reader now sees the short verdict and no pointer.os validateon the same project prints therule:line with the pointer. That is adomain:cliprinter change, outside this slice's files, and is recorded rather than built.rule:line with its pointer runs up to about 190 characters for the longer ids (security-controlled-by-parent-ambiguous-relationplus itscovers). This is stage 1's printer shape, carried as before.unprovisionedAnchorCause()keeps its other callers. Onlydashboard-filter-field-unprovisionedmoves to the new one-clauseunprovisionedAnchorVerdict(); the eight other rule files converge when their slices shorten them, which the helper's docblock states.Generated by Claude Code