Repository navigation
fix(lint): one-line verdicts for the flow-trigger, react-page, dataset-measure and validation-rule compile rules; os explain RULE_ID carries their reasoning - #22700
Conversation
…RULE_EXPLANATIONS Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. 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 fb50a6b74cc03c9c8fd195dac68930d48e6764d2 && git checkout fb50a6b74cc03c9c8fd195dac68930d48e6764d2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d8830c28056b9f466adfe9d59cd179b1d0fa6784 858cc2140028faeabf9f07b986d0d3a17845b06c && git checkout -B drift-repro d8830c28056b9f466adfe9d59cd179b1d0fa6784 && git merge --no-ff 858cc2140028faeabf9f07b986d0d3a17845b06c
node scripts/docs-audit/affected-docs.mjs --json d8830c28056b9f466adfe9d59cd179b1d0fa6784
|
Landing pre-checks at
|
Part of #22161
Clause-②: no
Stage 2 of the card, slice 5: the 15 rule ids of five whole
packages/lintsource files — the flow-trigger readiness rules, the react-page prop rules, the dataset measure / dimension rule and the two validation-rule compile 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 194 characters or fewer, and the runtime publish gate's suites (the whole@objectstack/metadata-protocolsuite) 166 or fewer; the longest of each id was 211 to 1,049 before.whereandpathname. The validation rule and its object (where:object 'account' · validation 'support_shape'), the JSON Pointer of an unregisteredformat(path:…schema#/properties/email/format), the measure or dimension name (where:dataset "x" › measure "y"), and the source of an uncompilableregex(V8 echoes it as/SOURCE/:; the unchangedfix:line still quotes it). This follows slice 4's accepted design call.TimeRelativeTriggerSchema's refusal of a descriptor: ONE issue, an unrecognized key first (its rename usually explains the others — a misspelledfieldis also the missingdateField), then(and N more); the trigger's bind-time warn still prints the whole list.schema is invalid: …): its first violation, which names the offending keyword, then(and N more).@objectstack/specunknown-key refusal (Unrecognized key(s) on SURFACE: KEY.): the key and the schema's rename (Did you mean …?), without the wrong-layer prescription bullet and the history sentence the producer appends after them. The producer orders its refusal "which key → how to fix it → why it used to be silent" so a one-line reader can stop early; the cut isschemaRefusalHead(), exported fromvalidate-flow-trigger-readiness.tsand imported byvalidate-react-page-props.ts(not on the barrel).ObjectChartaggregate.groupByunion that matches no form: the forms whose complaint is about the value's CONTENT, dropping one whose only complaint is that the whole value is another type (the bare-field-name form's "expected string, received object", for an object), unless every form says only that (unionRefusal(); each arm's issue is still rendered bydescribeIssue).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. No CLI source changes:explainPointer()andos explainresolve any key the table holds (packages/cli/test/explain-rule-id.test.tsiterates every key; run below). The 60 entries already in the table are byte-equal at the head (measured: each entry's JSON, base vs head).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 the removal of a value that only fed the old message. The helper and plumbing hunks, each message-only:declarerOf(verdict, baseObject, owner)→declarerOf(verdict, baseObject): string → string; a joined leaf readsjoined object "X"(it readobject "X" (reached through this cube's join chain)).jsonStoredDimensionFindinglosesselector;refusedMeasureFindinglosesaccepted;MemberSitelosesname(thewherestill carries it). TheACCEPTED_TYPES_BY_AGGREGATE.get(aggregate)lookup stays at both call sites and still gates skip 5 (if (!accepted) continue/return), unchanged.validateRuleSchemaFormats' pending entries andvalidateRuleCompilability's walk destructuring droplabel/objectName, which only fed the message.problemsjoin is replaced bydescriptorRefusal(issues); thefallbackternary that picks the consequence is unchanged (its schedule arm's words drop "plain")..changeset/22161-lint-slice-5-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.packages/cli/testorpackages/metadata-protocolfile asserts these ids' message text (grep: the consumers pin rule ids andpath, never the message), so neither is touched.The verdict forms, one each, as the suites fired them (census below):
(
TAGstands for the block's JSX tag,RecordRelatedListin angle brackets, which this page's sanitizer would eat.)Shared prose: written once (the dispatch's mechanism assumption 2)
rule-explanations.ts)flow-trigger-unknown-event(both arms),flow-trigger-unroutable(both arms),flow-time-relative-descriptor-unroutable(no-fallback arm)NEVER_FIRESflow-time-relative-descriptor-unroutable,flow-trigger-unroutableFLOW_TRIGGER_ROUTINGflow-trigger-unknown-event,flow-trigger-unroutableRECORD_TRIGGER_GRAMMARflow-time-relative-descriptor-*TIME_RELATIVE_PARTITIONvalidateFlowTriggerReadinessidsFLOW_GATE_REACHflow-time-relative-descriptor-invalid,react-chart-drilldown-invalid,react-chart-aggregate-invalidschemaRefusalHead()CHART_PROP_REFUSAL(the two chart ids; the descriptor entry states its own)validation-rule-regex-uncompilable,validation-rule-json-schema-uncompilableSKIPPED_ON_EVERY_WRITEVALIDATION_RULE_FAIL_OPENvalidation-rule-json-schema-uncompilable,validation-rule-json-schema-unknown-formatRUNTIME_AJV_ENVIRONMENTmeasure-aggregate-field-type-refused,dimension-json-stored-field-refuseddeclaredAs()/declarerOf()(existing)JSON_STORED_COLUMNANALYTICS_MEMBER_WALKreact-chart-field-unprovisionedand slice 2'sdashboard-filter-field-unprovisionedunprovisionedAnchorVerdict()(system-fields.ts, slice 2's short form)UNPROVISIONED_ANCHOR_CAUSE, extracted verbatim from the dashboard entry (its text is byte-equal, measured)The explanation module imports nothing, so the lists it writes out are held to their sources by the rules' tests: the aggregate × field-type rows to
AGGREGATE_FIELD_TYPE_COMPATIBILITY, the JSON-stored types toSTRUCTURED_JSON_TYPES/MULTI_OPTION_TYPES/MULTI_CAPABLE_TYPES, and each wrong-layer key an explanation names (schedule,runAs;drilldown,mode,report,view,sort; five aggregate and twogroupBykeys) to the schema still answering it with a prescription.Census (taken first, before any edit, at the base
cb3bb9333f)Method: slice 4's scratch preload (
NODE_OPTIONS=--import, never committed), which patchesArray.prototype.pushto record every finding-shaped object (rule+message) of the files' ids, deduped by (rule, message), with its push site, in every vitest worker. Lengths aremessagealone; the printed line addswhereand:. Rows fromos lint's own printer (commands/lint.ts) are excluded.packages/lintsuite at the base: 135 files, 6,231 passed (the CLI closure build put thedist/its five dist-reading tests wait for in place). All 15 ids fired, every one over 200, from 19 push sites. The files' other ids were measured too and are all 197 or under (react-chart-axis-unknown197,flow-trigger-unknown-object156,flow-draft-status-ambiguous161,react-chart-field-unknown142, …);react-prop-deprecatedfired in no suite, as slice 2 recorded.packages/cliunit tier, the whole project (279 files) against the base's lint dist: 277 files and 4,097 tests passed; the two that did not load (published-subpath-console.pin,published-subpath-hook-body.pin) needpackages/cli/dist, which was not built then. It fires none of the 15 ids. Positive control, same preload:test/validate-per-package-authoring-seam.test.tswithfield-no-consumersadded to the recorded set records it. The integration tier is declared to CI (no file in it names these ids, and a grep for their fixtures' shapes finds none).os validate(the source CLI) onapp-crm,app-todo,app-showcaseandapp-multi-package: all exit 0 and none of the 15 ids fires; control:flow-draft-status-ambiguous, an id ofvalidate-flow-trigger-readiness.ts, is recorded.@objectstack/metadata-protocolsuite (224 files, 221 run, 28,047 passed), which reads@objectstack/lintfrom its builtdist/. The before reading was taken on a lintdist/built from the base text of the six slice sources (proved indist/: the old marker in all four entry bundles, the new one in none), then the sources were restored blob-equal to HEAD and rebuilt (preflight: old marker absent, new present). It fires three ids.flow-time-relative-descriptor-invalidflow-time-relative-descriptor-unroutableflow-trigger-unroutableflow-api-trigger-secret-missingflow-trigger-unknown-eventreact-chart-drilldown-invalidreact-chart-aggregate-invalidreact-chart-field-unprovisionedreact-block-needs-record-contextreact-page-source-unparseablemeasure-aggregate-field-type-refuseddimension-json-stored-field-refusedvalidation-rule-json-schema-uncompilablevalidation-rule-regex-uncompilablevalidation-rule-json-schema-unknown-formatAll 19 push sites fire before and after. The message counts fall where the old message echoed what
whereorpathcarries: the validation rule's label and object, the format's pointer, the measure's name;react-chart-aggregate-invalidloses one because two refusals differed only in the echoed(received …)value after the dropped history sentence.Doors that print the new text
Read from the registry (
authoring-rules.ts: every entry iscommands: ALL;validateFlowTriggerReadinessandvalidateFlowApiTriggerSecretareCLI_AND_RUNTIMEwithruntimeTypes: ['flow'],validateDatasetMeasureAggregateswith['dataset'],validateRuleCompilabilityandvalidateRuleSchemaFormatswith['object'];validateReactPagePropsis a reference-integrity suite member with the default['flow']) and the CLI's callers (commands/validate.ts,compile.ts,lint.ts,verify.ts,utils/scaffold-validate.ts, whichinit.tsandgenerate.tsrun):os validate,os build(andos compile, whichos devruns per compile),os lint,os verify, theos init/os generatescaffold checkrule:line gains theos explainpointer;os validate --jsonerrorsandos build --jsonauthor-timeissuescarry the newmessage/meta, MCP),flowwritesflow-*ids, all errorsmessageand theOS_ALLOW_UNLINTED_METADATA_WRITESrefusal log linedatasetwritesanalyticsCubes, so the cube leg speaks on the CLI only)objectwritesvalidation-rule-*idsreact-*idshint; every other rule idEvery row is named in the changeset.
Tests
The rule suites import the rule source; the CLI and runtime-gate suites read the rebuilt
@objectstack/lintdist (marker:never binds, so the flow never runs1 hit each indist/index.js,index.cjs,runtime.js,runtime.cjs;reached through this0;flow-time-relative-descriptor-invalid2 hits each indist/rule-explanations.jsand.cjs).explainRule(id)exists and still names what the verdict stopped saying; the explanation lists are held to their spec sources (above).schemaRefusalHeadhas its own cases (rename kept, bullet and history dropped, one sentence quoted whole). The receiver meta-test invalidate-rule-compilability.test.tsexcuses the four new verdict-helper strings as plumbing. Every refusal assertion, rule id, severity,path,whereand hint pin is unchanged; the measured-consequence pins (the audit and the banner count, zero diagnostics at any layer, the HMAC secret, the accepted-type rows, the analytics door, ADR-0015) moved from the message to the explanation.pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2at1a40fc23e9(lint source byte-identical to the head): Test Files 135 passed (135), Tests 6,264 passed (6,264); lock VERDICT command-exit 0 (33 new cases).pnpm --filter @objectstack/lint build && 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/cli exec vitest run --maxWorkers=2 test/explain-rule-id.test.ts test/commands.test.ts test/validate-build-gate-parity.test.ts test/migrate-meta-engine-guidance.test.ts: Test Files 4 passed (4), Tests 91 passed (91).explain-rule-id.test.tsiterates everyRULE_EXPLANATIONSkey, so all 15 new ids resolve throughos explainandexplainPointer;node bin/run-dev.js explain flow-trigger-unroutableprints the entry.migrate-meta-engine-guidance.test.tsis the tracker-id refusal (mechanism assumption 4).@objectstack/metadata-protocolsuite, before and after (census above): Test Files 221 passed | 3 skipped (224), Tests 28,047 passed | 19 skipped, both runs; lock VERDICT command-exit 0.repoproject (mechanism assumption 3):pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2gave Test Files 55 passed (55), Tests 971 passed (971); lock VERDICT command-exit 0, at858cc21400(the head). Two of its tests walkpackages/lint/srcas a corpus:src/shared/retired-key-migrate-sentence.test.ts(theos migrate metasentence scanner; the new text carries no such sentence, grep 0) andsrc/identity/position-delegatable-enforcer.pin.test.ts(exportedsecurity-*rule id constants; this slice declares none). The otherlint/srcmentions underpackages/spec/srcnamevalidate-component-props.test.tsor are comments.#plus digits in any printed message or explanation (rule-explanations.test.tsrefuses one in every paragraph); the citations stay in//comments.858cc21400, throughscripts/ablation-replace.mjswrap mode under the verify lock, plus a shelltraprestoringHEAD). The rule suites import the rule source, so there is no dist leg.react-block-needs-record-context's pre-slice message was restored verbatim (the replacement text checked equal to the base file's) through the anchor of its new message: anchor x1 to x0, replacement x0 to x1, blob9f1728087ba8to218b907d4de9. Predicted before the run: exactly the bound pin goes red, since the old text still carries every fragment the other cases pin (renders empty,record:activity,record:highlights). Observed:src/validate-react-page-props.test.tsgave Test Files 1 failed (1), Tests 1 failed | 170 passed (171); the one red case was "every verdict the cases above fired is one line of at most 200 characters" (the restoredRecordDetailsverdict, 258 characters). Restored: blob after restore9f1728087ba8== blob at HEAD,git diff HEADempty,git status --porcelainempty.npx eslint --no-inline-config --format jsonover the 11 changed.tsfiles gave 11 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 head858cc21400(merge basecb3bb9333f), 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. The 62nd,pnpm check:dual-build-cjs-loads, is NOT MEASURED, reason: the dispatch forbids the whole-workspace build it needs.--rangaveRun reconciliation — 62 derived, 61 run, 0 NOT-MEASURED, 1 UNRUN(the one above). I also ran the 52-family "Artifact rosters" block the same run prints, 49 of them at858cc21400: all exited 0 (14 are checker-health--self-testrows, which grade a checker and not this diff; four keep their roster in a directory this diff touches —check-changeset-fixed,check:authz-resolver,check:error-code-casing,check:filter-alias-parity— all green). The three PR-context guards (check-closing-target-claim,check-partof-closing-keyword,check-single-claim-paths) need this PR's number and body, so they run against this PR after it opens; their readings are in the report on the card.check:type-check-debtran without--re-measure. The long ones on the shared box:check:type-check-debt431s,check:query-options-erasure275s.Remaining for later slices — 56 ids in
packages/lint, by filePR #22670's list minus this slice's five files and 15 ids. The lengths are slice 2's (PR #22448, census at
05c7c3fa3b; "cli" marks a length only the cli suite reached), 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-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-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-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-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.react-prop-deprecated(validate-react-page-props.ts, about 225 characters by its template) fires in no suite and was outside the claim's 15 ids, so it is unchanged here; it belongs to whichever slice next takes that file's leftovers.Acceptance notes
fix:lines are unchanged, and two of them now point at text the verdict no longer carries in full.react-chart-drilldown-invalid's andreact-chart-aggregate-invalid's hint ends "the rejection above carries the fix", andflow-time-relative-descriptor-invalid's says "satisfies each message above … an unrecognized key names the declared key it was probably meant to be". The rename survives in the verdict; a wrong-layer prescription (schedule/runAson a descriptor,drilldown/mode/report/view/sorton a drill block, five aggregate and twogroupBykeys) and the descriptor's other issues do not — the explanations name those keys, and the trigger's bind-time warn still prints the whole list. Whether hints get the one-line budget, and whether a forwarded prescription moves into thefix:line, is the card's own later call (slices 2–4's notes; slice 4'ssharing-rule-runtime-variable-conditionhint is 2,280 characters).formatname, a long field path or a long engine reason lengthens the verdict.flow-time-relative-descriptor-invalid's fixture sits at 194 because the spec's surface name ("this flow start node'sconfig.timeRelativedescriptor") is 55 characters of it.schemaRefusalHead()reads the producer's sentence shape (strictUnknownKeyErrorin@objectstack/spec: the key sentence, then an optionalDid you mean …?, then bullets and history). It cuts and never rewrites — the test holds that the head is a prefix of the schema's own sentence — and a refusal that does not have that shape is quoted to its first sentence or whole.validate-component-props.ts(component-props-invalid, 994, fenced in slice 2) forwards the same producer throughdescribeIssue; its slice can liftschemaRefusalHead/unionRefusalintozod-issue-format.tsbeside it, which this slice's file surface did not include.packages/cliunit-tier census 9m42s; the showcase closure build (for the example corpus) 6m18s; each@objectstack/metadata-protocolcensus about 5m40s (the before leg 6m06s with its two lint builds); the specrepoproject 15m02s.Generated by Claude Code