Skip to content

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

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22161-s2-lint-slice-5
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22161-s2-lint-slice-5

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #22161
Clause-②: no

Stage 2 of the card, slice 5: the 15 rule ids of five whole packages/lint source 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

  • One verdict line per finding. Each finding of the 15 ids prints a message of 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-protocol suite) 166 or fewer; the longest of each id was 211 to 1,049 before.
  • The verdict no longer repeats what where and path name. The validation rule and its object (where: object 'account' · validation 'support_shape'), the JSON Pointer of an unregistered format (path: …schema#/properties/email/format), the measure or dimension name (where: dataset "x" › measure "y"), and the source of an uncompilable regex (V8 echoes it as /SOURCE/: ; the unchanged fix: line still quotes it). This follows slice 4's accepted design call.
  • A quoted refusal is quoted to its verdict, with the rest counted:
    • TimeRelativeTriggerSchema's refusal of a descriptor: ONE issue, an unrecognized key first (its rename usually explains the others — a misspelled field is also the missing dateField), then (and N more); the trigger's bind-time warn still prints the whole list.
    • ajv's metaschema refusal (schema is invalid: …): its first violation, which names the offending keyword, then (and N more).
    • a @objectstack/spec unknown-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 is schemaRefusalHead(), exported from validate-flow-trigger-readiness.ts and imported by validate-react-page-props.ts (not on the barrel).
    • an ObjectChart aggregate.groupBy union 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 by describeIssue).
  • The reasoning moves to RULE_EXPLANATIONS (packages/lint/src/rule-explanations.ts), 15 new entries, so os explain RULE_ID prints it and the CLI's rule: line ends with the pointer for these ids. No CLI source changes: explainPointer() and os explain resolve any key the table holds (packages/cli/test/explain-rule-id.test.ts iterates 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).
  • Nothing else moves. Rule ids, severities, path, hint (the fix: 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 a message expression, 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 reads joined object "X" (it read object "X" (reached through this cube's join chain)).
    • jsonStoredDimensionFinding loses selector; refusedMeasureFinding loses accepted; MemberSite loses name (the where still carries it). The ACCEPTED_TYPES_BY_AGGREGATE.get(aggregate) lookup stays at both call sites and still gates skip 5 (if (!accepted) continue / return), unchanged.
    • validateRuleSchemaFormats' pending entries and validateRuleCompilability's walk destructuring drop label / objectName, which only fed the message.
    • The time-relative problems join is replaced by descriptorRefusal(issues); the fallback ternary that picks the consequence is unchanged (its schedule arm's words drop "plain").
    • No helper changed shape (no string → boolean); no truth table is involved.
  • .changeset/22161-lint-slice-5-one-line.md: @objectstack/lint patch, naming every door that prints the new text (below). Clause-②: no follows the contract review on slice 1 (its section ②): RULE_EXPLANATIONS entries are data in an existing export.
  • No rider: no packages/cli/test or packages/metadata-protocol file asserts these ids' message text (grep: the consumers pin rule ids and path, never the message), so neither is touched.

The verdict forms, one each, as the suites fired them (census below):

config.timeRelative never binds, so the flow never runs: Unrecognized key(s) on this flow start node's `config.timeRelative` descriptor: `field`. Did you mean `field` → `dateField`? (and 2 more)
has config.timeRelative = "daily" (a string), not a descriptor object, so the sweep is never installed and the flow fires through its `config.schedule` cadence, dropping the descriptor
has config.timeRelative = "daily" (a string), not a descriptor object, so the sweep is never installed and with no other trigger declared the flow binds to nothing and never fires
declares type: 'record_change' but its start node's triggerType is 'onCreate', which the engine routes to no trigger, so the flow is demoted to a manual one and never fires
binds the inbound api trigger (type: 'api' and start-node triggerType: 'api') but its start node declares no config.secret, so the engine refuses to register the flow
triggerType is an array (["record-after-create","record-after-delete"]), but a start node takes one trigger event, so the flow binds to nothing and never fires
drillDown: Unrecognized key(s) on this chart drill-down block: `maxrows`. Did you mean `maxrows` → `maxRows`?
aggregate.groupBy matches no accepted form: Unrecognized key(s) on this chart groupBy: `dateGranularty`. Did you mean `dateGranularty` → `dateGranularity`?
aggregate.groupBy "owner_id": 'owner_id' is an injected column with no storage on external object 'ext_customer', so the chart groups everything into one empty bucket
TAG renders "record:related_list", which reads its record from a record page's context; a kind:'react' page mounts none, so the block renders empty
kind:'react' source did not parse (line 1, column 11: Octal literals are not allowed. Use the syntax '0o755'.), so the component-contract checks may have missed problems in it
aggregate "count_distinct" over field "account.region" (`json` on joined object "fx_branch") is refused by the aggregate × field-type table: the number would depend on the SQL dialect
aggregate "count_distinct" over field "measured" (`select` with `multiple: true` on object "crm_opportunity") is refused: a list stored as JSON, which no two backends compare alike
groups by field "grouped" (`composite` with `multiple: true` on object "crm_opportunity"), a structured-JSON value, so every query grouping by it is refused (400 INVALID_FIELD)
ajv cannot compile `schema` (schema is invalid: data/type must be equal to one of the allowed values (and 2 more)), so the write path skips the rule and it enforces nothing on any record
`regex` does not compile (Unterminated character class), so the write path skips the rule and it enforces nothing on any record
`format: 'emial'` is not a registered format, so ajv drops the keyword and the rule enforces it on no record: every write is ACCEPTED

(TAG stands for the block's JSX tag, RecordRelatedList in angle brackets, which this page's sanitizer would eat.)

Shared prose: written once (the dispatch's mechanism assumption 2)

shared sentence ids verdict clause (rule files) explanation (rule-explanations.ts)
the never-fire outcome flow-trigger-unknown-event (both arms), flow-trigger-unroutable (both arms), flow-time-relative-descriptor-unroutable (no-fallback arm) NEVER_FIRES — (each entry says what reports it, which differs)
the engine's trigger routing chain flow-time-relative-descriptor-unroutable, flow-trigger-unroutable — FLOW_TRIGGER_ROUTING
the record-trigger token grammar flow-trigger-unknown-event, flow-trigger-unroutable — RECORD_TRIGGER_GRAMMAR
the time-relative routing predicate (the partition) both flow-time-relative-descriptor-* — TIME_RELATIVE_PARTITION
where the never-fire family gates the four validateFlowTriggerReadiness ids — FLOW_GATE_REACH
a forwarded spec refusal, cut to its verdict flow-time-relative-descriptor-invalid, react-chart-drilldown-invalid, react-chart-aggregate-invalid schemaRefusalHead() CHART_PROP_REFUSAL (the two chart ids; the descriptor entry states its own)
the write path's fail-open skip validation-rule-regex-uncompilable, validation-rule-json-schema-uncompilable SKIPPED_ON_EVERY_WRITE VALIDATION_RULE_FAIL_OPEN
the runtime's ajv environment validation-rule-json-schema-uncompilable, validation-rule-json-schema-unknown-format — RUNTIME_AJV_ENVIRONMENT
what a JSON-stored column is measure-aggregate-field-type-refused, dimension-json-stored-field-refused declaredAs() / declarerOf() (existing) JSON_STORED_COLUMN
the member walk, the cube leg and the skips the same two — ANALYTICS_MEMBER_WALK
what an unprovisioned anchor is react-chart-field-unprovisioned and slice 2's dashboard-filter-field-unprovisioned unprovisionedAnchorVerdict() (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 to STRUCTURED_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 two groupBy keys) 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 patches Array.prototype.push to record every finding-shaped object (rule + message) of the files' ids, deduped by (rule, message), with its push site, in every vitest worker. Lengths are message alone; the printed line adds where and : . Rows from os lint's own printer (commands/lint.ts) are excluded.

  • packages/lint suite at the base: 135 files, 6,231 passed (the CLI closure build put the dist/ 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-unknown 197, flow-trigger-unknown-object 156, flow-draft-status-ambiguous 161, react-chart-field-unknown 142, …); react-prop-deprecated fired in no suite, as slice 2 recorded.
  • packages/cli unit 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) need packages/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.ts with field-no-consumers added 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).
  • The example corpus, os validate (the source CLI) on app-crm, app-todo, app-showcase and app-multi-package: all exit 0 and none of the 15 ids fires; control: flow-draft-status-ambiguous, an id of validate-flow-trigger-readiness.ts, is recorded.
  • Runtime publish gate: the whole @objectstack/metadata-protocol suite (224 files, 221 run, 28,047 passed), which reads @objectstack/lint from its built dist/. The before reading was taken on a lint dist/ built from the base text of the six slice sources (proved in dist/: 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.
rule id lint suite before: messages · longest lint suite after: messages · range runtime-gate suite before → after
flow-time-relative-descriptor-invalid 6 · 796 6 · 103–194 —
flow-time-relative-descriptor-unroutable 11 · 532 11 · 169–184 —
flow-trigger-unroutable 10 · 518 10 · 164–183 —
flow-api-trigger-secret-missing 10 · 336 10 · 131–166 336 → 166
flow-trigger-unknown-event 9 · 222 9 · 126–159 —
react-chart-drilldown-invalid 5 · 784 5 · 54–109 —
react-chart-aggregate-invalid 18 · 507 17 · 53–155 —
react-chart-field-unprovisioned 1 · 429 1 · 166 —
react-block-needs-record-context 6 · 267 6 · 148–163 —
react-page-source-unparseable 2 · 211 2 · 133–175 —
measure-aggregate-field-type-refused 382 · 809 364 · 155–183 504 → 160
dimension-json-stored-field-refused 58 · 531 58 · 137–176 —
validation-rule-json-schema-uncompilable 11 · 517 5 · 140–186 389 → 148
validation-rule-regex-uncompilable 11 · 482 5 · 117–136 —
validation-rule-json-schema-unknown-format 49 · 1,049 12 · 131–140 —

All 19 push sites fire before and after. The message counts fall where the old message echoed what where or path carries: the validation rule's label and object, the format's pointer, the measure's name; react-chart-aggregate-invalid loses 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 is commands: ALL; validateFlowTriggerReadiness and validateFlowApiTriggerSecret are CLI_AND_RUNTIME with runtimeTypes: ['flow'], validateDatasetMeasureAggregates with ['dataset'], validateRuleCompilability and validateRuleSchemaFormats with ['object']; validateReactPageProps is 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, which init.ts and generate.ts run):

door ids what changes
os validate, os build (and os compile, which os dev runs per compile), os lint, os verify, the os init / os generate scaffold check all 15 the text-face verdict line; the rule: line gains the os explain pointer; os validate --json errors and os build --json author-time issues carry the new message
runtime publish gate (Studio, REST /meta, MCP), flow writes the 5 flow-* ids, all errors the 422 issue message and the OS_ALLOW_UNLINTED_METADATA_WRITES refusal log line
the same gate, dataset writes the 2 dataset ids (a dataset write carries no analyticsCubes, so the cube leg speaks on the CLI only) the same
the same gate, object writes the 3 validation-rule-* ids the same
never at the runtime gate the 5 react-* ids their rule runs there only on a flow write, whose snapshot carries no pages
unchanged every hint; every other rule id

Every row is named in the changeset.

Tests

The rule suites import the rule source; the CLI and runtime-gate suites read the rebuilt @objectstack/lint dist (marker: never binds, so the flow never runs 1 hit each in dist/index.js, index.cjs, runtime.js, runtime.cjs; reached through this 0; flow-time-relative-descriptor-invalid 2 hits each in dist/rule-explanations.js and .cjs).

  • Each rule's own suite pins the new shape, as in slices 2–4. All five test files wrap their rule import and record every finding their cases fire; a final case in each holds every recorded verdict of its ids to one line of at most 200 characters, behind a coverage control (each id fired). A second block per file pins, per id, that explainRule(id) exists and still names what the verdict stopped saying; the explanation lists are held to their spec sources (above). schemaRefusalHead has its own cases (rename kept, bullet and history dropped, one sentence quoted whole). The receiver meta-test in validate-rule-compilability.test.ts excuses the four new verdict-helper strings as plumbing. Every refusal assertion, rule id, severity, path, where and 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.
  • Lint suite: pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 at 1a40fc23e9 (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).
  • Lint build + typecheck: pnpm --filter @objectstack/lint build && pnpm --filter @objectstack/lint run typecheck: VERDICT command-exit 0; check:test-typecheck OK, 2 files, 6 errors and 2 pinned signatures held.
  • CLI, against the rebuilt lint dist: 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.ts iterates every RULE_EXPLANATIONS key, so all 15 new ids resolve through os explain and explainPointer; node bin/run-dev.js explain flow-trigger-unroutable prints the entry. migrate-meta-engine-guidance.test.ts is the tracker-id refusal (mechanism assumption 4).
  • Runtime gate: the whole @objectstack/metadata-protocol suite, 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.
  • Spec repo project (mechanism assumption 3): pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2 gave Test Files 55 passed (55), Tests 971 passed (971); lock VERDICT command-exit 0, at 858cc21400 (the head). Two of its tests walk packages/lint/src as a corpus: src/shared/retired-key-migrate-sentence.test.ts (the os migrate meta sentence scanner; the new text carries no such sentence, grep 0) and src/identity/position-delegatable-enforcer.pin.test.ts (exported security-* rule id constants; this slice declares none). The other lint/src mentions under packages/spec/src name validate-component-props.test.ts or are comments.
  • Tracker ids (mechanism assumption 4): no # plus digits in any printed message or explanation (rule-explanations.test.ts refuses one in every paragraph); the citations stay in // comments.
  • Ablation (one-shot, from the committed state 858cc21400, through scripts/ablation-replace.mjs wrap mode under the verify lock, plus a shell trap restoring HEAD). 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, blob 9f1728087ba8 to 218b907d4de9. 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.ts gave 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 restored RecordDetails verdict, 258 characters). Restored: blob after restore 9f1728087ba8 == blob at HEAD, git diff HEAD empty, git status --porcelain empty.
  • ESLint, narrowed: npx eslint --no-inline-config --format json over the 11 changed .ts files gave 11 files in the report, 0 errors, 0 warnings. eslint.config.mjs never enables type-aware linting (no parserOptions.project, no typed rules), so no untouched file's verdict can move. A control-character scan of every changed file found no match.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack with no paths, on the head 858cc21400 (merge base cb3bb9333f), 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. --ran gave Run 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 at 858cc21400: all exited 0 (14 are checker-health --self-test rows, 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-debt ran without --re-measure. The long ones on the shared box: check:type-check-debt 431s, check:query-options-erasure 275s.

Remaining for later slices — 56 ids in packages/lint, by file

PR #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-composite 480 cli, relationship/master-detail-required 462, unique/unscoped-declared-index 427, unique/double-declaration 402 cli, rollup/non-numeric-aggregand 364, relationship/delete-behavior 201 cli
  • validate-ai-agent-authoring.ts (3): default-agent-legacy-alias 466, default-agent-outside-roster 388, agent-authoring-withdrawn 352
  • validate-predicate-path-refs.ts (3): predicate-rhs-path-shaped 648, predicate-path-unrooted 434, predicate-path-unresolved 371
  • validate-searchable-fields.ts (3): searchable-field-unprovisioned 512, searchable-field-unsearchable 449, searchable-field-unknown 295
  • validate-sortable-fields.ts (3): sort-field-unprovisioned 844, sort-field-unsortable 369, sort-field-unknown 287
  • lint-view-refs.ts (2): view-ref-nav-view-missing 437, view-key-collision 278 cli
  • validate-approval-approvers.ts (2): approval-approvers-may-resolve-empty 451, approval-approver-not-membership-tier 272
  • validate-chart-bindings.ts (2): chart-measure-unknown 442, chart-axis-not-selected 327
  • validate-dashboard-action-refs.ts (2): dashboard-action-route-unresolved 242, dashboard-action-target-undefined 239
  • validate-empty-combinators.ts (2): filter-empty-combinator 352, filter-empty-node 226
  • validate-list-view-field-refs.ts (2): list-view-field-dotted 445, list-view-field-unknown 355
  • validate-translation-references.ts (2): translation-target-unknown 345, translation-option-key-unknown 230
  • lint-flow-credential-literals.ts (1): flow-credential-literal 390
  • validate-action-dispatch-contract.ts (1): action-dispatch-contract-mismatch 927
  • validate-action-name-refs.ts (1): action-name-undefined 409
  • validate-ai-surface-affinity.ts (1): ai-skill-surface-mismatch 281
  • validate-ai-tool-references.ts (1): ai-skill-tool-unresolved 441
  • validate-capability-references.ts (1): capability-reference-unknown 219
  • validate-component-types.ts (1): component-type-unknown 807
  • validate-flow-filter-tokens.ts (1): flow-filter-token-unknown 278
  • validate-managed-api-methods.ts (1): object/managed-api-method-unaffordable 412
  • validate-mapping-target-fields.ts (1): mapping-target-field-unknown 466
  • validate-nav-access.ts (1): nav-object-ungranted 353
  • validate-nav-object-servability.ts (1): nav-object-unservable 528
  • validate-nav-target-refs.ts (1): nav-target-unresolved 426
  • validate-object-field-refs.ts (1): object-field-ref-unknown 320
  • validate-object-references.ts (1): object-reference-unregistered-platform 324
  • validate-org-axis-red-lines.ts (1): org-axis-cross-org-bu-grant 365
  • validate-page-visualization-bindings.ts (1): page/visualization-without-binding 629
  • validate-preset-comparands.ts (1): filter-preset-comparand 669
  • validate-retired-permission-residue.ts (1): permission-retired-lifecycle-residue 235
  • validate-seed-replay-safety.ts (1): seed-insert-mode-duplicates-on-replay 222
  • validate-seed-state-machine.ts (1): seed-value-outside-state-machine 320
  • validate-semantic-roles.ts (1): semantic-role-field-unprovisioned 291
  • validate-translatable-sections.ts (1): translation-section-name-missing 553
  • validate-view-containers.ts (1): view-container-shape 290

The 26 ids slice 2 fenced, the 9 ids owned by packages/cli, and the action-governance.ts boot-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

  • The fix: lines are unchanged, and two of them now point at text the verdict no longer carries in full. react-chart-drilldown-invalid's and react-chart-aggregate-invalid's hint ends "the rejection above carries the fix", and flow-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 / runAs on a descriptor, drilldown / mode / report / view / sort on a drill block, five aggregate and two groupBy keys) 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 the fix: line, is the card's own later call (slices 2–4's notes; slice 4's sharing-rule-runtime-variable-condition hint is 2,280 characters).
  • A forwarded refusal can still lengthen the line. The 200 bound holds on every variant the suites fire, but the quoted parts are the author's and the producer's: a long key name, a long format name, 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's config.timeRelative descriptor") is 55 characters of it.
  • schemaRefusalHead() reads the producer's sentence shape (strictUnknownKeyError in @objectstack/spec: the key sentence, then an optional Did 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 through describeIssue; its slice can lift schemaRefusalHead / unionRefusal into zod-issue-format.ts beside it, which this slice's file surface did not include.
  • Process: the CLI closure build held the shared lock 6m52s; the base packages/cli unit-tier census 9m42s; the showcase closure build (for the example corpus) 6m18s; each @objectstack/metadata-protocol census about 5m40s (the before leg 6m06s with its two lint builds); the spec repo project 15m02s.

Generated by Claude Code

…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>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 54 documentable anchor(s).

16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json d8830c28056b9f466adfe9d59cd179b1d0fa6784.

⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d8830c28056b9f466adfe9d59cd179b1d0fa6784 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from fb50a6b74cc03c9c8fd195dac68930d48e6764d2 — the merge of head 858cc2140028faeabf9f07b986d0d3a17845b06c into base d8830c28056b9f466adfe9d59cd179b1d0fa6784, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs d8830c28056b9f466adfe9d59cd179b1d0fa6784 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing pre-checks at 858cc21400, by the owning seat: every check green, queued

domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-10T17:30Z · holder of claim 6099119809 on #22161.

  • The review: the seat's ACCEPT 6100097165 names this head. The diff is message prose only: dropped destructured fields, the refusal-cut helpers and declarerOf's words. ACCEPTED_TYPES_BY_AGGREGATE still gates at both call sites. No contract review is owed, as for slices 3 and 4.
  • CI: 34 check-runs: 31 success, 3 skipped, nothing else. check-expected-skips --pr 22700 reads every skip in the roster.
  • Governed: check-governed-merges --pr objectstack-ai/objectstack#22700 reads NOT governed; +1194 / −226.
  • Closing keywords: the body carries Part of #22161 alone, and no commit message carries one. The card stays open for its later slices.
  • main drift since the merge base cb3bb9333f: main (now d8830c2805) moved none of the PR's 12 paths. GitHub reports mergeable: true (clean).

pr_ready and automerge_enable follow.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 17:31
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 10, 2026 17:31
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit 61bea24 Oct 10, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22161-s2-lint-slice-5 branch October 10, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants