Repository navigation
fix(lint): one-line verdicts for the approval-approver, data-model, agent-authoring, view-reference and chart-binding rules; os explain RULE_ID carries their reasoning - #22742
Conversation
…agent-authoring, view-ref and chart-binding rules Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
…200 bound Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check40 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. 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 b247ec43b4e22212d5b383aaf0f9d780c6ac5b73 && git checkout b247ec43b4e22212d5b383aaf0f9d780c6ac5b73
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f59a73c3950550c7e1085d06682cc9c0fdb54c5d 4bd706494beae8a54e0f0874aafffba2b9a12680 && git checkout -B drift-repro f59a73c3950550c7e1085d06682cc9c0fdb54c5d && git merge --no-ff 4bd706494beae8a54e0f0874aafffba2b9a12680
node scripts/docs-audit/affected-docs.mjs --json f59a73c3950550c7e1085d06682cc9c0fdb54c5d |
…e id constants join the barrel Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Written 2026-10-10T23:38Z. Read: card #22161 (body and all 42 comments — the maintainer's dispatch quoted in the body, stage 1's FAIL ① Derived judgments
② Semver level
One body inconsistency, escalated (no code): two bullets of the PR body predate the seat's correction and still say the opposite of line 2 and the changeset — under "What changes", the bullet " ③ Boundary flags
Implemented-by: VERDICT: PASS |
Part of #22161
Clause-②: yes (widening: three rule id constants exported from the barrel —
RELATIONSHIP_MASTER_DETAIL_REQUIRED,RELATIONSHIP_DELETE_BEHAVIOR,ROLLUP_NON_NUMERIC_AGGREGAND; corrected by the seat on #22161, the changeset moves tominor)Stage 2 of the card, slice 7: the 15 rule ids of five whole
packages/lintsource files — the approval-approver rules, the data-model rules (the three ADR-0120 uniqueness ids and three ofos lint's data-model sweep), the AI agent-authoring rules, the view-reference rules and the chart-binding rules. The card stays open for the later slices listed under "Remaining for later slices" below.What changes
messageof one verdict sentence. Every finding the rules' own suites fire is now 197 characters or fewer, the wholepackages/cliunit tier's 188 or fewer, andos validateon the four example apps' 191 or fewer; the longest of each id was 201 to 480 before (199 to 478 in the rules' own suites)."object"/"object.field"subject, becauseos lintprints alintDataModelissue'smessagewith only a positionalat objects[N]…beside it; the uniqueness ids keep the index name or its column list for the same reason.view-ref-nav-view-missingkeeps the object's list views (they are the fix), quoted to at most three and then(and N more);chart-axis-not-selectedquotes at most three names of the chart's selection, thenand N more.managerarm ofapproval-approvers-may-resolve-emptystays conditional. Its verdict says what happens WHEREsys_user.manager_idis unset and asserts nothing about the column, which a static check cannot read; the sentence that said so explicitly is now the explanation's, and the pin that held it (does not claim a runtime fact it did not read) holds the conditional verdict and the explanation together.relationship/master-detail-required's constant was module-private, androllup/non-numeric-aggregandandrelationship/delete-behaviorwere string literals).RELATIONSHIP_MASTER_DETAIL_REQUIRED,RELATIONSHIP_DELETE_BEHAVIORandROLLUP_NON_NUMERIC_AGGREGANDare exported fromdata-model-rules.tsbeside theUNIQUE_*constants and re-exported from the root barrel (packages/lint/src/index.ts), so eachRULE_EXPLANATIONSkey is held to an exported constant (rule-explanations.test.ts) andrule-id-barrel-exports.test.tsstays green. The id strings are unchanged. Level:minor, as the seat's correction on [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 (6103196579) orders: three new named exports widen the published surface, and the contract review6103394547(②) confirmed it.RULE_EXPLANATIONS(packages/lint/src/rule-explanations.ts), 15 new entries, soos explain RULE_IDprints it and the CLI'srule:line ends with the pointer for these ids (for the three data-model sweep ids, onos lint's own printer too). No CLI source changes:explainPointer()andos explainresolve any key the table holds (packages/cli/test/explain-rule-id.test.tsiterates every key; it ran in the unit tier below);node bin/run-dev.js explain unique/double-declarationprints the entry. The 87 entries already in the table are byte-equal (the diff ofrule-explanations.tsis additions only).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 five rule files is amessageexpression, a comment, an import, a verdict helper or constant, or the removal of a value that only fed the old message. The plumbing hunks, each message-only:validate-approval-approvers.ts:STAYS_LOCKED, the lock clause both empty-slate arms end with under the defaultlockRecord: true; two comments that described the old wording are corrected (the manager arm is conditional, and the explanation says why).MANAGER_ONLY_REMEDYandMANAGER_ONLY_ROUTESfeed only the hint and are untouched (the dispatch's mechanism assumption 3: message and hint share no constant).data-model-rules.ts:CBP_ORPHAN_CONSEQUENCE, the consequence the threecontrolled_by_parentshapes of R2 end with;cbpMasterReferenceFindinglosesderivationandflagWords(both fed only the message);lintUniqueDeclarationslosesglobalSide/orgSideandlintLegacyOrganizationCompositeslosesspelling(each fed only the message); the module-privateMASTER_DETAIL_REQUIREDbecomes the exportedRELATIONSHIP_MASTER_DETAIL_REQUIREDand two literals become constants (above).lint-view-refs.ts:viewRoster()replaceslistNames()in the nav verdict (thelistNamesimport goes).validate-chart-bindings.ts:UNKNOWN_CONSEQUENCEandUNSELECTED_CONSEQUENCE(records of strings, message-only) are cut to one clause each;selectionRoster()replaceslist()in the not-selected verdict (list()still feeds the hints)..changeset/22161-lint-slice-7-one-line.md:@objectstack/lintminor, naming every door that prints the new text (below).Clause-②: yes (widening: three rule id constants exported from the barrel), as line 2 says. Slice 1'sno/patchreading coveredRULE_EXPLANATIONSentries as data in an existing export; the three new constants are new named exports, so they are a widening (seat order6103196579, contract review6103394547②).packages/cli/test/data-model-rules.test.tsasserts fragments of the uniqueness and delete-behavior messages, and every fragment it pins (ADR-0120, the index names,CONTRADICT,silently dead,declared twice,NULL-distinct, the NULL-row clause,(cascade/restrict, the leading"object.field") is in the new verdicts, so it passes unedited (the whole unit tier ran, below). Nopackages/metadata-protocolfile and nocontent/docspage quotes these messages (grep).The verdict forms, the longest of each arm as the suites fired them (census below):
Shared prose: written once (the dispatch's route)
rule-explanations.ts)approval-approvers-may-resolve-emptySTAYS_LOCKEDcontrolled_by_parentdetail without its master is unreadablerelationship/master-detail-requiredCBP_ORPHAN_CONSEQUENCEunique/unscoped-declared-index,unique/double-declarationUNIQUE_SCOPE_WORDSlintDataModel's sweep reportsrelationship/master-detail-required,relationship/delete-behavior,rollup/non-numeric-aggregandDATA_MODEL_SWEEP_REACHPLATFORM_AGENT_ROSTERchart-measure-unknown,chart-axis-not-selectedCHART_BINDING_SURFACESThe explanation module imports nothing, so a fact it writes out is held to its source by the rule's test: the agent aliases (
data_chat→ask,metadata_assistant→build) by running the rule on each, the membership tiers by the verdict readingBUILTIN_MEMBERSHIP_ROLES, and each shared paragraph by asserting it is one text under every id it serves.Census (taken first, before any edit, at the base
e5899a67d6)Method: slice 4's scratch preload (
NODE_OPTIONS=--import, never committed), which patchesArray.prototype.pushto record every finding-shaped object (rule+message) of the five files' ids, deduped by (rule, message), with its push site, in every process. Lengths aremessagealone; the printed line addswhereand:. Rows fromos lint's own printer (commands/lint.ts) are excluded. Positive control in every run:field-no-consumers(or, in the runtime-gate suite, the approval file's other ids) added to the recorded set and recorded.packages/lintsuite, base 135 files / 6,304 tests (after the CLI and example closure build put thedist/its dist-reading tests wait for in place): all 15 ids fire, from 17 push sites (source lines), every one over 200 exceptrelationship/delete-behavior(199 here).packages/cliunit tier, the whole project (280 files, 4,162 tests, all loaded —packages/cli/distwas built): fires six of the 15 —agent-authoring-withdrawn,relationship/delete-behavior(201, the one reading over 200 only this tier reached),relationship/master-detail-required(warning arm only), and the threeunique/*ids (packages/cli/test/data-model-rules.test.ts). Controlfield-no-consumersrecorded.view-key-collisiondoes not fire here (slice 2's list carried a cli-only 278 for it). The integration tier is declared to CI.@objectstack/metadata-protocolsuite (226 files, 223 run, 28,101 passed), which reads@objectstack/lintfrom its builtdist/, before on the base-built dist and after on the rebuilt one. It fires none of the 15 ids; control: the approval file'sapproval-expression-invalidandapproval-expression-no-empty-policyrecorded in both runs.os validate(the built CLI) onapp-crm,app-todo,app-showcaseandapp-multi-package, all exit 0:app-showcasefiresapproval-approvers-may-resolve-empty(both arms, with and without the lock clause); controlfield-no-consumersrecorded.so every reference to1 hit each indist/index.js,index.cjs,runtime.js,runtime.cjs, oldclashes with another view0): the same runs at0ed57745b0(lint source byte-identical to the head4077426704, which changes two numbers in the changeset).os validateexamples: before → afterapproval-approvers-may-resolve-emptyapproval-approver-not-membership-tierunique/unscoped-declared-indexunique/double-declarationunique/legacy-organization-compositerelationship/master-detail-requiredrelationship/delete-behaviorrollup/non-numeric-aggregandagent-authoring-withdrawndefault-agent-outside-rosterdefault-agent-legacy-aliasview-ref-nav-view-missingview-key-collisionchart-measure-unknownchart-axis-not-selectedAll 17 push sites fire before and after. A higher message count after is a case this slice added (the unique-verdict file's quadrants, the agent roster probe, the selection-roster case, the three-list-view case);
relationship/master-detail-required's warning arm (115–122) is unchanged text. The runtime-gate suite is not a column: it fires none of the 15 before or after.Doors that print the new text
Read from the registry (
authoring-rules.ts:validateApprovalApproversiscommands: ALL,CLI_AND_RUNTIME,runtimeTypes: ['flow'];lintViewRefsandlintUnscopedDeclaredIndexesarecommands: ALL, CLI only;lintUniqueDeclarationsandlintLegacyOrganizationCompositesare['validate', 'build'], CLI only, and reachos lintthroughlintDataModel; the reference-integrity suite,CLI_AND_RUNTIME, dispatchesvalidateChartBindingson['flow', 'report']andvalidateAiAgentAuthoringon the default['flow']), the runtime gate's split (runtime-authoring-gate.ts: errors → the 422, everything else → 2xxadvisoriesplus the deduped[Protocol] authoring advisorylog line), and the CLI's callers (commands/validate.ts,compile.ts,lint.ts;os verifyruns thevalidateset and theos init/os generatescaffold check thebuildset):os validate,os build(andos compile, whichos devruns per compile),os lint,os verify, the scaffold checkunique/*idsrule:line gains theos explainpointer;os validate --jsonerrorsandos build --jsonauthor-timeissuescarry the newmessageos lintonly (and the metadata-generation rubric's lint half)relationship/master-detail-required,relationship/delete-behavior,rollup/non-numeric-aggregandflowwriteapproval-approvers-may-resolve-empty(info),approval-approver-not-membership-tier(warning)advisoriesentry'smessageand the advisory log linereportwritechart-measure-unknownat a report'svaluesor chartyAxis(error); at a report chart'sseries[](warning);chart-axis-not-selected(warning)messageand theOS_ALLOW_UNLINTED_METADATA_WRITESrefusal log line for the error; theadvisoriesentry and the advisory log line for the warningsflowwrite's snapshot carries no agents or apps), the view-reference andunique/*ids (CLI only), the sweep ids (os lintonly), the chart ids at a list-view or page chart (areportwrite carries neither surface)hint; every other rule idEvery row is named in the changeset.
Tests
The rule suites import the rule source; the CLI, runtime-gate and example runs read the rebuilt
@objectstack/lintdist. Every heavy run went throughscripts/pm/os-verify-lock.sh; its VERDICT lines are quoted.validate-approval-approvers.test.ts,validate-ai-agent-authoring.test.ts,lint-view-refs.test.ts,validate-chart-bindings.test.ts,data-model-rules.master-detail-required.test.tsanddata-model-rules.summary-rollup.test.tswrap their rule import and record every finding their cases fire; a final block in each holds every recorded verdict of its ids to one line of at most 200 characters, behind a coverage control (each id fired, and each arm: both empty-slate arms with and without the lock clause, both declaration arms, both nav arms, the query and three presentation positions, R2 at both severities). The newdata-model-rules.unique-verdicts.test.tsdoes the same for the three uniqueness ids over every arm. Exact pins hold one verdict per arm; explanation pins hold, per id, thatexplainRule(id)exists and names what the verdict stopped saying, that each shared paragraph is one text under every id it serves, and that the aliases the explanation spells are the ones the rule resolves. Every refusal assertion, rule id, severity,path,whereand hint pin is unchanged; the one prose pin that read the old text (does not claim a runtime fact it did not read) now holds the conditional verdict and the explanation.pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2at0ed57745b0: Test Files 136 passed (136), Tests 6,334 passed (6,334); VERDICT command-exit 0 (base 135 / 6,304; one new file, 30 new cases).pnpm --filter @objectstack/lint build && pnpm --filter @objectstack/lint run typecheck: VERDICT command-exit 0;check-dts-emitted6/6;check:test-typecheckOK, 2 files / 6 errors / 2 pinned signatures held (the test layer compiles undertsconfig.test.json, which reaches the new test file).pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: Test Files 280 passed (280), Tests 4,162 passed (4,162), before and after; VERDICT command-exit 0 both. It includestest/explain-rule-id.test.ts(iterates everyRULE_EXPLANATIONSkey, so the 15 new ids resolve throughos explain,explainPointerand the listing) andtest/data-model-rules.test.ts(unedited).node bin/run-dev.js explain unique/double-declarationprints the entry.@objectstack/metadata-protocolsuite, before and after: Test Files 223 passed | 3 skipped (226), Tests 28,101 passed | 19 skipped, both runs; VERDICT command-exit 0.pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/shared/retired-key-migrate-sentence.test.ts src/identity/position-delegatable-enforcer.pin.test.ts(the two spec tests that readpackages/lint/src: theos migrate metasentence scanner, whose one hit in these files is the unchangedunique/unscoped-declared-indexfix, and the exportedsecurity-*constants, none added): Test Files 2 passed (2), Tests 19 passed (19).#plus digits in any printed message or explanation (rule-explanations.test.tsrefuses one in every paragraph); the citations stay in//comments and test names.0ed57745b0, throughscripts/ablation-replace.mjswrap mode under the verify lock, plus a shelltraprestoringHEADby absolute path). The rule suites import the rule source, so there is no dist leg.view-key-collision's pre-slice message was restored verbatim (the replacement block read from the base file) through the anchor of its new message: anchor x1 → x0, blob483ffe09850e→057794818e4b. Predicted before the run: exactly two cases red — the 200-character bound pin and that id's exact-text pin; every other assertion on the id pins fragments the old text also carries ('task.default','task.default_2','task.mine'). Observed:src/lint-view-refs.test.tsTest Files 1 failed (1), Tests 2 failed | 36 passed (38); the two red cases are exactly those two (the bound pin read 257 characters). Restored: blob483ffe09850e== blob at HEAD,git diff HEADempty,git status --porcelainempty.npx eslint --no-inline-config --format jsonover the 14 changed.tsfiles gave 14 files in the report, 0 errors, 0 warnings.eslint.config.mjsnever enables type-aware linting (noparserOptions.project, no typed rules), so no untouched file's verdict can move. A control-character scan of every changed file found no match.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwith no paths, on the head4077426704(merge basea360cee92e; the derived set is identical to the one atd9f3e019c8), derived 62 commands. I ran 61 of them sequentially, each with its own log and its exit code captured before any pipe: all 61 exited 0. One is NOT MEASURED, as the dispatch directs:pnpm check:type-check-debt, which ischeck-type-check-coverage.mjs --re-measureinpackage.json(it builds every package); its non-re-measuring half ischeck:type-check-coverage, which ran and exited 0.--rangaveRun reconciliation — 62 derived, 61 run, 0 NOT-MEASURED, 1 UNRUN(that one). I also ran 34 families of the "Artifact rosters" block the same derivation prints (the 38 non-self-test rows, lesscheck:engine-double-contract, which is also derived, and the three PR-context guards, which run against this PR once it exists): all exited 0, the four rostered under a directory this diff touches (check-changeset-fixed,check:authz-resolver,check:error-code-casing,check:filter-alias-parity) included. On a first full pass at0ed57745b0, three refused rather than measured:check:dual-build-cjs-loadsandcheck:published-readme-exportswithPREREQUISITE NOT MET(six packages'dist/absent in this worktree; I built exactly those six —client-react,embedder-openai,knowledge-ragflow,organizations,studio,service-cluster-redis— lock VERDICT 0, and both then exited 0), andcheck-plugin-teardown-shapeonce, reading a temp config the concurrently running CLI suite had just deleted (re-run, exit 0); the whole union then reran green at4077426704, first time. The long ones on the shared box:check:query-options-erasure291s,check:slot-lookup152s,check-comment-mask-corpus115s.Remaining for later slices — 29 ids in
packages/lint, by filePR #22717's list minus this slice's five files and 15 ids. The lengths are slice 2's (PR #22448, census at
05c7c3fa3b), not re-measured here.validate-dashboard-action-refs.ts(2):dashboard-action-route-unresolved242,dashboard-action-target-undefined239validate-empty-combinators.ts(2):filter-empty-combinator352,filter-empty-node226validate-list-view-field-refs.ts(2):list-view-field-dotted445,list-view-field-unknown355validate-translation-references.ts(2):translation-target-unknown345,translation-option-key-unknown230lint-flow-credential-literals.ts(1):flow-credential-literal390validate-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-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-retired-permission-residue.ts(1):permission-retired-lifecycle-residue235validate-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, andreact-prop-deprecatedas PR #22700's notes it. This slice touched none of them.Acceptance notes
managerarm ofapproval-approvers-may-resolve-emptyprints a 2,159-characterfix:line (MANAGER_ONLY_REMEDY+MANAGER_ONLY_ROUTES+ the escape), the same class assharing-rule-runtime-variable-condition's 2,280. It points at nothing the verdict cut — it is self-contained — and is unchanged here, as every slice leaves hints. No other hint of the 15 ids refers to text the verdict cut.field-no-consumers(stage 1's id) still prints a 341-character verdict onos validateofexamples/app-showcase: its "a consumer of the same name on …" clause lists every other object that consumes the field name, unbounded. Measured by this slice's census control, not touched here; it is this card's own subject, for a later slice.UNSELECTED_CONSEQUENCE.queryis unreachable since the reportchart.yAxisstopped carrying a not-selected check (chart-axis-not-selectednow fires only at presentation positions; every query position IS the selection). The record's type keys everyMeasurePosition, so the entry stays, shortened like the rest; removing it is a type change outside a message-only slice. Thechart-axis-not-selectedexplanation says a query position is never reported.view-key-collisiondid not fire in thepackages/cliunit tier at the base, though slice 2's list marked its 278 as a cli-only reading; the lint suite fires it at 257 → 145. Recorded as measured.packages/lint/src/index.ts, three barrel lines for the three new constants. The dispatch's mechanism assumption 2 prescribes "an exported constant beside the others", andrule-id-barrel-exports.test.tsfails a rule id constant no published barrel re-exports, so the constant cannot land without the barrel line. No open PR touches the file.packages/lint/src/data-model-rules.unique-verdicts.test.tspins the three uniqueness ids' verdicts over every arm (each spelling, named and unnamed index, a declared tenant column, all four scope quadrants of the double declaration). Their behaviour tests live inpackages/cli/test/data-model-rules.test.ts, outside the rule's own package, so the shape pin has no existing lint file to join; the other three data-model ids joindata-model-rules.master-detail-required.test.tsanddata-model-rules.summary-rollup.test.ts.Generated by Claude Code