Skip to content

feat(lint,cli): one-line author-time rule verdicts, and os explain RULE_ID for the reasoning - #22339

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-22161-rule-message-one-line
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-22161-rule-message-one-line

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Part of #22161
Clause-②: yes (widening: os explain accepts a rule id, and packages/lint exports the rule explanations)

What changes

  • field-no-consumers and security-owd-unset print one verdict sentence and one fix. Their long reasoning moves into one static explanation per rule id, RULE_EXPLANATIONS / explainRule() in @objectstack/lint (new module packages/lint/src/rule-explanations.ts, exported from the root barrel and from a new import-free entry, @objectstack/lint/rule-explanations). Nothing else carries a copy.
  • os explain RULE_ID (the maintainer's spelling, one positional, no rule sub-word): a schema name resolves exactly as before; otherwise an exact rule id resolves to its explanation (--json prints { rule, covers, paragraphs }). The no-argument listing also names the rule explanations (--json adds rules: [{ id, covers }]). An unknown id exits 1 and names both lists.
  • The rule: line carries the pointer, spelled once — explainPointer() / authoringFindingDetailLines() in packages/cli/src/utils/format.ts, used by the build advisory printer, the gating-error printer (validate, build, verify, init), the validate advisory list, and os lint's rule line. It appears only for a rule id the table holds, so it never names a command that would answer "unknown". The hint line is labelled fix:.
  • os explain's schema lookup reads own keys only. os explain constructor / __proto__ printed Schema: Object … undefined and threw schema.required is not iterable on main. They are now refused as unknown ids (6d2eb857c; pinned in test/explain-rule-id.test.ts; with the own-key check reverted → 1 failed | 10 passed).
  • The fix: line is always a fix (patch round 2). expression-invalid's authored source is a quote, not a fix, so it now ends the finding's message as — source: `…` and its hint is empty: the CLI prints no fix: line for it, and the source still reaches the text face and the runtime 422 issue. Four other hints that carried no instruction now open with one: component-props-invalid (its consequence moved into the message), flow-time-relative-descriptor-invalid, react-prop-missing-required (the contract-description branch), liveness-experimental-property.

The maintainer's shape, as os validate and os build now print it on a tutorial-shaped project:

  ⚠ object "my_app_ticket" · field "description": declared, but nothing in this stack displays or reads it (inert)
    fix: add it to a view column or a form section, or remove the declaration
    rule: field-no-consumers  at objects[1].fields.description — `os explain field-no-consumers` for what counts as a consumer
  • object "my_app_ticket": custom object declares no sharingModel (OWD); the runtime falls back to 'private', but the baseline must be an authored decision
      fix: declare sharingModel: 'private' (owner + shares; recommended), 'public_read', 'public_read_write', or 'controlled_by_parent' (master-detail children)
      rule: security-owd-unset  at objects[1].sharingModel — `os explain security-owd-unset` for why the baseline must be declared

Measured (local CLI built from this branch; tutorial-shaped project: my_app_note + my_app_ticket, a grid view on title/status)

before (59d993c97) after
os validate, field-no-consumers one line, 852 chars; no fix, no rule id 114 / 77 / 126 chars (verdict / fix / rule)
os build, field-no-consumers 852 + 692 + 62 chars 114 / 77 / 126
os validate, security-owd-unset 374 + 175 + 58 chars 156 / 160 / 130
one os dev --compile run printed once (the compile child), not again at serve printed once, same shape
  • H1 holds: the message and the build-time "Give … a consumer" text (the finding's hint) are built in packages/lint/src/validate-field-consumers.ts. os validate printed the registry advisory as its ⚠ line only (commands/validate.ts), with no fix and no rule line; os build printed message, hint and rule line through printAuthoringAdvisories.
  • H2 holds, with one addition: the rule: line is the CLI printer's, not the rules' (utils/format.ts, two printers). The pointer is spelled there once. os validate's advisory list had no rule line at all, so it now renders the same two lines through the same helper (below).
  • H3 holds: 16 os explain schema names, 214 rule id constants exported from packages/lint — intersection empty (no rule id is a single word). Pinned in packages/cli/test/explain-rule-id.test.ts (lowercased, against every exported rule id constant).
  • H4 does not hold: one os dev --compile -p PORT --fresh run printed the warning once (grep -c field-no-consumers = 1, before and after). No printer change was made for it.
  • H5: far more than 8 over-long rules (below), so this PR builds the mechanism and shortens field-no-consumers and security-owd-unset only. The dead-button action-governance line is not an author-time rule: it is the boot-time logger.warn in packages/objectql/src/action-governance.ts ([action-governance] declared script actions with NO handler … — 163 chars as the source writes it, plus a {count, actions} payload; its sibling "registered handlers with NO declaration" line is 628). It lives outside packages/lint and outside the CLI printer, so it is named here and not edited.

Landing outside the claim's file surface, and why

  • packages/cli/src/utils/format.ts — the H2 printer: explainPointer() and authoringFindingDetailLines(); both printers render through them.
  • packages/cli/src/commands/validate.ts — measured: os validate printed a registry warning with no fix and no rule line, so a shortened message would have reached the maintainer's first-named command with no pointer. The text face now prints the two lines under each registry advisory via the same helper. The warnings list --strict and --json read is unchanged.
  • packages/cli/src/commands/lint.ts — os lint prints the same shortened message; its rule line gains the same pointer (one call to explainPointer).
  • packages/lint/package.json, packages/lint/tsup.config.ts, packages/lint/src/rule-id-barrel-exports.test.ts — the new ./rule-explanations entry. format.ts is documented as "a pure formatter with no rule-engine import", and every command imports it; loading the @objectstack/lint root barrel after @objectstack/spec measured 456–547 ms (three runs), which every command (os explain object included) would otherwise pay. The entry's module imports nothing (pinned by a source scan); its keys and the field-no-consumers roots list are literals held to the rule's constants by rule-explanations.test.ts.
  • packages/cli/README.md — the os explain row.
  • Tests updated for the new text: packages/cli/src/utils/author-time-rules.test.ts (read the field from where, not message), packages/cli/test/truncation-remainder-notices.test.ts (fix: label), packages/cli/test/validate-build-gate-parity.test.ts (classifies authoringFindingDetailLines as presentation). No test outside packages/lint / packages/cli pins either old message.

Tests, round 1 (all local, this branch; head 315a26618 unless a run names another; round 2's readings are under ## Patch round 2)

New pins: packages/lint/src/rule-explanations.test.ts (every key is an exported rule id under its own key; covers fits the pointer; no tracker number in the text; the roots paragraph equals CONSUMER_ROOTS / CARRIER_ROOTS; exact-id lookup; the module imports nothing), the shape pins in validate-field-consumers.test.ts and validate-security-posture.test.ts (verdict line and fix line, exact), packages/cli/test/explain-rule-id.test.ts (H3 disjointness; every pointer target resolves through Explain.run; the printed verdict / fix: / rule: lines of each rule's REAL finding; schema lookup unchanged; unknown id exits 1), and packages/cli/test/rule-line-explain-pointer.e2e.test.ts (spawns os validate and os build; a *.e2e file, so the nightly tier).

  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 → Test Files 128 passed (128), Tests 5853 passed (5853) (at ed786eb3e; no lint file changed after it).
  • pnpm --filter @objectstack/lint run typecheck → exit 0, check:test-typecheck: OK — … 2 file(s) / 6 error(s) / 2 pinned signature(s) held.
  • pnpm --filter @objectstack/cli run typecheck → exit 0, check:test-typecheck: OK — … 3 file(s) / 28 error(s) / 6 pinned signature(s) held (at 315a26618).
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 --shard=N/3 (the full unit tier, in three foreground shards because one run exceeds the container's foreground cap under load): shard 1 89 passed / 1523 passed and shard 2 88 passed, 1 failed (at ed786eb3e); shard 3 89 passed / 1264 passed (at 315a26618). The shard-2 failure was src/utils/author-time-rules.test.ts reading the field name from message; fixed in 315a26618 and re-run with test/lint-per-package-authoring-seam.test.ts → 2 passed / 10 passed.
  • --project integration (declared to CI as a whole), the ten files that spawn validate / build / verify / lint and read their text: test/build-text-face-advisory-count, verify-author-time-stage, validate-per-package-authoring-parity, union-fold-command-parity, authoring-rule-command-parity, validate-view-container-name, build-view-container-name, picklist-reference-doors, lint-per-package-authoring-parity, validate-lint-mapping-connector-source → Test Files 10 passed (10), Tests 62 passed (62).
  • OS_TEST_TIERS=nightly … vitest run test/rule-line-explain-pointer.e2e.test.ts → 2 passed; validate-json-warning-parity.e2e.test.ts (the ⚠ line still pairs with --json) → 3 passed (both on the ed786eb3e tree).
  • Ablation (one-shot, nothing kept): scripts/ablation-replace.mjs replaced explainPointer's return with '' in packages/cli/src/utils/format.ts (anchor 1 → 0, blob 9d90c98c409d → 4427f41a866d), test/explain-rule-id.test.ts → 3 failed | 7 passed; restored, blob 9d90c98c409d == HEAD, git diff HEAD empty.
  • Cross-package type read: packages/cli builds against @objectstack/lint/rule-explanations, an entry that exists only in the rebuilt dist/ (dist/rule-explanations.{js,cjs,d.ts,d.cts}), so the CLI build read the rebuilt declarations. CJS require and ESM import of the entry both load (['field-no-consumers', 'security-owd-unset']).
  • ESLint, narrowed to the diff: npx eslint --no-inline-config --format json over the 18 changed .ts files → 18 files in the JSON report, 0 errors, 0 warnings; eslint.config.mjs never enables type-aware linting (no parserOptions.project, its own comment at :327), so this diff cannot move a verdict on an untouched file. Repo-wide pnpm lint is CI's.

Gates

node scripts/pm/dispatch-gates.mjs --commands (no paths) at 315a26618 derived 78 commands, a superset of the 51 at dispatch. Ran all 78: 76 exit 0; pnpm check:dual-build-cjs-loads and pnpm check:i18n-coverage exit 3, PREREQUISITE NOT MET (packages outside the CLI's build closure have no dist/ in this worktree) — NOT MEASURED, CI's. Reconciliation: ✓ dispatch-gates --ran: 78 derived famil(ies) accounted for — 76 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3). Also run, exit 0: check:authz-resolver, check:error-code-casing, check:filter-alias-parity (the three artifact-roster gates whose roster sits under packages/), check:published-readme-exports, check:published-readme-links, check:cli-examples-parity. Control-character scan over every changed file: no match.

Second stage — the over-long rules this PR does not shorten

Measured by running the whole packages/lint suite at 59d993c97 (127 files, 5843 tests) with a scratch hook that recorded, per rule id, the longest message of any finding pushed: 240 rule ids fired, 157 with a message over 200 characters. Lengths are the message alone (the printed line adds where and : ). A rule id that fired in no test is not in this count.

Second stage, in packages/lint (not touched here) — 124 rule id(s):

  • validate-rls-predicate-enforceability.ts: rls-predicate-unparseable 2056, rls-predicate-unenforceable 1679, rls-predicate-unknown-user-variable 1544, rls-predicate-unknown-field 1399, rls-predicate-over-budget 1259
  • validate-sharing-rule-enforceability.ts: sharing-rule-unlowerable-condition 1093, sharing-rule-object-not-shareable 774, sharing-rule-object-controlled-by-parent 660, sharing-rule-runtime-variable-condition 492
  • validate-rule-schema-formats.ts: validation-rule-json-schema-unknown-format 1049
  • validate-action-dispatch-contract.ts: action-dispatch-contract-mismatch 927
  • validate-component-props.ts: component-props-invalid 920, component-props-unknown-key 844
  • validate-sortable-fields.ts: sort-field-unprovisioned 844, sort-field-unsortable 369, sort-field-unknown 287
  • validate-dataset-measure-aggregates.ts: measure-aggregate-field-type-refused 809, dimension-json-stored-field-refused 531
  • validate-component-types.ts: component-type-unknown 807
  • validate-flow-trigger-readiness.ts: flow-time-relative-descriptor-invalid 796, flow-time-relative-descriptor-unroutable 532, flow-trigger-unroutable 518, flow-api-trigger-secret-missing 336, flow-trigger-unknown-event 222
  • validate-hook-body-writes.ts: hook-body-write-unprovisioned-anchor 792, hook-body-write-unknown-field 465, hook-body-source-unparseable 212
  • validate-react-page-props.ts: react-chart-drilldown-invalid 784, react-chart-aggregate-invalid 507, react-chart-field-unprovisioned 429, react-block-needs-record-context 267, react-page-source-unparseable 211
  • validate-action-body-writes.ts: action-body-write-unprovisioned-anchor 778, action-body-write-unknown-field 433, action-record-write-discarded 293, action-body-source-unparseable 212
  • validate-flow-node-writes.ts: flow-node-write-unprovisioned-anchor 739, flow-node-write-unknown-field 421
  • validate-security-posture.ts: security-controlled-by-parent-ambiguous-relation 697, security-fls-unknown-field 593, security-controlled-by-parent-no-relation 526, security-master-detail-ungranted 449, security-owd-alias 354, security-delegation-missing-reason 210
  • validate-preset-comparands.ts: filter-preset-comparand 669
  • validate-visibility-predicates.ts: visibility-predicate-unknown-function 655, visibility-predicate-over-budget 507, visibility-bare-identifier 411, visibility-predicate-syntax 341, visibility-root-mislayered 304
  • validate-predicate-path-refs.ts: predicate-rhs-path-shaped 648, predicate-path-unrooted 434, predicate-path-unresolved 371
  • validate-page-visualization-bindings.ts: page/visualization-without-binding 629
  • validate-translatable-sections.ts: translation-section-name-missing 553
  • validate-nav-object-servability.ts: nav-object-unservable 528
  • validate-searchable-fields.ts: searchable-field-unprovisioned 512, searchable-field-unsearchable 449, searchable-field-unknown 295
  • validate-widget-bindings.ts: dashboard-filter-field-unprovisioned 509, chart-field-unknown 407, dashboard-filter-field-not-included 370, widget-filter-field-unknown 364, dashboard-filter-field-unknown 333, chart-dimensions-missing 306, widget-filter-field-not-included 282, widget-measures-missing 255, widget-sortby-unselected 242, chart-measures-missing 224, widget-legacy-analytics-unrenderable 205
  • validate-rule-compilability.ts: validation-rule-json-schema-uncompilable 489, validation-rule-regex-uncompilable 482
  • validate-page-field-bindings.ts: page-field-unprovisioned 484, page-section-group-unknown 214
  • data-model-rules.ts: unique/legacy-organization-composite 478, unique/unscoped-declared-index 427, unique/double-declaration 381
  • validate-mapping-target-fields.ts: mapping-target-field-unknown 466
  • validate-ai-agent-authoring.ts: default-agent-legacy-alias 466, default-agent-outside-roster 388, agent-authoring-withdrawn 352
  • validate-readonly-hook-writes.ts: hook-api-update-readonly-field 464, hook-api-update-readonly-when-field 267
  • validate-approval-approvers.ts: approval-approvers-may-resolve-empty 451, approval-approver-not-membership-tier 272
  • validate-dataset-references.ts: dataset-field-not-included 446, dataset-field-unknown 296, dataset-filter-field-unknown 294, dataset-include-unknown 260
  • validate-list-view-field-refs.ts: list-view-field-dotted 445, list-view-field-unknown 355
  • validate-chart-bindings.ts: chart-measure-unknown 442, chart-axis-not-selected 327
  • validate-ai-tool-references.ts: ai-skill-tool-unresolved 441
  • lint-view-refs.ts: view-ref-nav-view-missing 437, view-key-collision 257
  • validate-nav-target-refs.ts: nav-target-unresolved 426
  • validate-managed-api-methods.ts: object/managed-api-method-unaffordable 412
  • validate-readonly-flow-writes.ts: flow-update-readonly-field 410, flow-update-readonly-when-field 317
  • validate-action-name-refs.ts: action-name-undefined 409
  • validate-readonly-action-writes.ts: action-api-update-readonly-when-field 396
  • lint-flow-credential-literals.ts: flow-credential-literal 390
  • validate-filter-tokens.ts: filter-token-unknown 386
  • validate-print-page-blocks.ts: print-page-block-unprintable 368
  • validate-org-axis-red-lines.ts: org-axis-cross-org-bu-grant 365
  • validate-nav-access.ts: nav-object-ungranted 353
  • validate-empty-combinators.ts: filter-empty-combinator 352, filter-empty-node 226
  • validate-translation-references.ts: translation-target-unknown 345, translation-option-key-unknown 230
  • validate-object-references.ts: object-reference-unregistered-platform 324
  • validate-object-field-refs.ts: object-field-ref-unknown 320
  • validate-seed-state-machine.ts: seed-value-outside-state-machine 320
  • validate-semantic-roles.ts: semantic-role-field-unprovisioned 291
  • validate-view-containers.ts: view-container-shape 290
  • validate-ai-surface-affinity.ts: ai-skill-surface-mismatch 281
  • validate-flow-filter-tokens.ts: flow-filter-token-unknown 278
  • validate-dashboard-action-refs.ts: dashboard-action-route-unresolved 242, dashboard-action-target-undefined 239
  • validate-retired-permission-residue.ts: permission-retired-lifecycle-residue 235
  • validate-seed-replay-safety.ts: seed-insert-mode-duplicates-on-replay 222
  • validate-capability-references.ts: capability-reference-unknown 219
  • validate-form-layout.ts: form-section-group-unknown 214

Excluded this round — files open PRs #22268, #22315, #22319 edit — 19 rule id(s):

  • validate-expressions.ts: expression-invalid 2027
  • lint-flow-patterns.ts: flow-multi-write-unfiltered 656, flow-decision-mode-invalid 528, flow-loop-body-uncontained 522, flow-try-catch-without-catch 520, flow-approval-revise-target-not-service-owned 366, flow-decision-unconditional-branch 342, flow-error-label-not-fault 315, flow-inert-node-condition 286, flow-runas-unscoped 284, flow-branch-label-unmatched 272, flow-decision-inclusive-overlap 250, flow-default-edge-with-condition 239, flow-multiple-default-edges 211, flow-time-relative-antipattern 208, flow-date-equality-filter 208
  • validate-flow-template-paths.ts: flow-template-field-unprovisioned 440, flow-template-lookup-traversal 349, flow-template-unknown-field 258

Message lives outside packages/lint — packages/spec/src/kernel/functional-completeness.ts (named, not edited) — 8 rule id(s):

  • functional-completeness.ts: view/row-color-without-colors 793, view/layout-without-binding 673, webhook/without-triggers 594, view/tree-without-parent-field 592, field/summary-without-operations 319, field/formula-without-expression 259, field/choice-without-options 250, field/relationship-without-reference 239

Not an author-time registry rule — 4 rule id(s):

  • lint-startup-registry-verdict.ts (the repo gate check:startup-registry-verdict): startup-open-vocabulary-verdict 779, startup-verdict-assertive-wording 731
  • data-model-rules.ts lintDataModel (os lint's own data-model rubric): relationship/master-detail-required 462, rollup/non-numeric-aggregand 364

Each second-stage rule takes the same shape: move the long text into RULE_EXPLANATIONS (the pointer then appears on its rule: line by itself), leave one verdict sentence and one fix, and pin the new shape in the rule's own test. The excluded three files can follow once #22268, #22315 and #22319 land.

Acceptance notes

  • The fix: label now prefixes the hint under every author-time finding the CLI prints (build, validate, verify, init), not only the two shortened rules. Round 2 measured every hint producer for text that is not a fix and changed five (listed under ## Patch round 2); every other rule's hint text is unchanged. Borderline rows were counted as fixes, because each carries an instruction or a spelling to write: validate-component-types.ts:150, validate-flow-trigger-readiness.ts:632, runtime-gate.ts:1020, lint-liveness-properties.ts:254 / :273, and the fix snippets in functional-completeness.ts.
  • expression-invalid's runtime 422 issue now carries hint: ''; its message carries the source. objectui's save-advisory toast already skips an empty hint (saveAdvisoryToast.ts:97). The one place that prints the bare value is the deduped operator log line in metadata-protocol runtime-authoring-gate.ts:1130 (… (${advisory.hint})), which now ends in () for an expression-invalid warning. It is cosmetic, server-log only, and not changed here.
  • os validate's text face now shows fix: and rule: lines under every registry warning (it showed neither before); the warnings list --strict and --json read is unchanged, so validate-json-warning-parity.e2e.test.ts still pairs the faces.
  • Runtime publish gate: security-owd-unset also runs at the metadata write door, so a Studio / REST / MCP refusal carries the shorter message and hint too; the explanation is reachable from the CLI only.
  • origin/main e9a1f5c40 is merged (d33862bde, a merge commit).
  • No new gate and no length ratchet (the ruling); each shortened rule's own test pins its shape.

Patch round 2 (seat order 6067462250 → 74bed8f56)

Written into this body by the domain:spec seat 2 at 2026-10-08T20:46Z from the dev's report 6068666691; the role file reserves a later body edit to the seat.

  • Measured, non-fix hints (static read of the 274 hint: values in packages/lint/src, plus the fix: values in functional-completeness.ts and the shared hint helpers). Five, at the stop condition's limit, none in the three excluded files:
    • authoring-rules.ts:687, expression-invalid: quoted the source. The source now ends the message, and the hint is empty.
    • validate-component-props.ts:336, component-props-invalid: context only. The hint is the fix, and the consequence moved into the message (a CLI-only rule).
    • validate-flow-trigger-readiness.ts:497, flow-time-relative-descriptor-invalid: context only. The hint opens with the instruction.
    • validate-react-page-props.ts:1166, react-prop-missing-required: the hint was the binding's description alone. It is now Pass REQ={…}: DESCRIPTION.
    • lint-liveness-properties.ts:247, liveness-experimental-property: the hint was a statement. It now opens with an instruction.
  • Pin: packages/cli/test/explain-rule-id.test.ts runs the real registry adapter on the tutorial's action and prints through printAuthoringRuleErrors. It asserts exactly two lines, the source inside the verdict line and no fix: line. Ablated with the dist leg (adapter reverted, lint rebuilt): 1 failed | 11 passed. Restored to the HEAD blob, and the rebuilt dist carries no marker.
  • Docs blocks re-rendered from the printer: content/docs/getting-started/build-with-claude-code.mdx :309–:313 and content/docs/ui/react-pages.mdx :361–:363, :403–:405. The :403 block was already stale before this PR (the fallback hint where the contract has a description). Cross-lane on [PM seat] domain:devx @ objectstack — 🟢 os-bill · session_01LYXc6ckoWuZyVZpWYizdMh #6023.
  • Changeset: it names the runtime-wire message change for expression-invalid. Its count of the reworded rules that reach a runtime response is corrected in patch round 3, below.
  • Readings:
    • lint: 128 files / 5853 passed, and typecheck exit 0, both at e84732425.
    • cli: unit 4 files / 120 passed and integration 4 files / 21 passed (the spawn tests that print or read expression-invalid), and typecheck exit 0, all at 819444f50.
    • ESLint on the 7 changed .ts files: 0 errors / 0 warnings.
    • dispatch-gates --commands (no paths) at 819444f50: 106 derived (round 1's 78 plus 28 docs/spec families), all 106 exit 0.
    • The three dist-reading gates exited 3 on the fresh worktree and exit 0 after the remaining packages were built.
    • --ran: 106 run, 0 NOT-MEASURED. The 20 changeset/text families re-ran on 74bed8f56: exit 0.
    • Round 1's two NOT-MEASURED gates (check:dual-build-cjs-loads, check:i18n-coverage) also exit 0 at 6d2eb857c.
  • Line budget: round 2 is 10 files, +87 / -18. The whole PR against e9a1f5c40 is 28 files, +919 / -81. Governed paths touched: 0.

Patch round 3 (contract review FAIL 6068965879 → seat order 6068983639 → 1e016895c)

Written into this body by the domain:spec seat 2 at 2026-10-08T21:10Z from the dev's direct report; the role file reserves a later body edit to the seat. One commit, .changeset/22161-rule-message-one-line.md only (+4 / -2). Each sentence was checked against surfaces, runtimeTypes and severity in the code before it was written.

  • The reworded hints at the gate. Only flow-time-relative-descriptor-invalid reaches a 422 hint: it is an error on flow writes.
    • liveness-experimental-property is always a warning on email_template, mapping and datasource writes, so it would ride the 2xx advisories. No ledger row on those types is experimental today, so it reaches no runtime response yet. This corrects the seat's own order, which put it on the 422.
    • component-props-invalid is CLI-only.
    • react-prop-missing-required judges no page write at the gate.
  • security-owd-unset at the object write door. A custom object (neither isSystem nor sys_-named) with no sharingModel is refused with a 422, and its issue now carries the new message and hint, both quoted verbatim. where and path still carry the object; os explain security-owd-unset prints the incident.
  • expression-invalid: the source rides the issue message at the gate for flow, action, hook and object writes. An error lands in the 422, a warning in the 2xx advisories. The runtime hint is ''.
  • os explain unknown id: it still exits 1. The text changes from Unknown schema: "X" to Unknown schema or rule id: "X", followed by a Rules with an explanation: … line. The --json error changes the same way.
  • Gates at 1e016895c: the 20 families dispatch-gates --commands derives for the changeset, plus check-changeset-fixed.mjs, all exit 0. No PREREQUISITE NOT MET. main was not merged (the push was accepted).

Generated by Claude Code

claude added 3 commits October 8, 2026 17:09
… pointer

field-no-consumers and security-owd-unset print one verdict sentence and
one fix; their long reasoning moves into rule explanations keyed by rule id
(@objectstack/lint, also published as the import-free
`@objectstack/lint/rule-explanations` entry). `os explain` resolves a rule
id after the schema names, and the CLI's `rule:` line names the command for
the rules that have an explanation, from one spelling in utils/format.ts.
`os validate` now prints the fix and rule lines under each registry warning,
as `os build` does.

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
…-time-rules test reads the field from where

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 8, 2026
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/lint, touching 22 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/cli/README.md, packages/lint/package.json, packages/lint/src/index.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture))
  • content/docs/data-modeling/index.mdx (via public_read_write (literal, a string literal in validateSecurityPosture))
  • content/docs/data-modeling/objects.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture), public_read (literal, a string literal in validateSecurityPosture), public_read_write (literal, a string literal in validateSecurityPosture))
  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object))
  • content/docs/getting-started/common-patterns.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture), public_read (literal, a string literal in validateSecurityPosture), public_read_write (literal, a string literal in validateSecurityPosture))
  • content/docs/getting-started/examples.mdx (via public_read_write (literal, a string literal in validateSecurityPosture))
  • content/docs/kernel/runtime-services/sharing-service.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture))
  • content/docs/permissions/access-matrix.mdx (via public_read_write (literal, a string literal in validateSecurityPosture))
  • content/docs/permissions/authorization.mdx (via validateSecurityPosture (symbol, a top-level function), controlled_by_parent (literal, a string literal in validateSecurityPosture), public_read (literal, a string literal in validateSecurityPosture), public_read_write (literal, a string literal in validateSecurityPosture))
  • content/docs/permissions/index.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture), public_read_write (literal, a string literal in validateSecurityPosture))
  • content/docs/permissions/permissions-matrix.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture), public_read (literal, a string literal in validateSecurityPosture), public_read_write (literal, a string literal in validateSecurityPosture))
  • content/docs/permissions/rls.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture), public_read (literal, a string literal in validateSecurityPosture))
  • content/docs/permissions/sharing-rules.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture), public_read (literal, a string literal in validateSecurityPosture), public_read_write (literal, a string literal in validateSecurityPosture))
  • content/docs/protocol/kernel/error-handling.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture))
  • content/docs/protocol/objectql/security.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture), public_read (literal, a string literal in validateSecurityPosture), public_read_write (literal, a string literal in validateSecurityPosture))

⛔ 8 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture), public_read (literal, a string literal in validateSecurityPosture), public_read_write (literal, a string literal in validateSecurityPosture))
  • content/docs/releases/v13.mdx (via validateSecurityPosture (symbol, a top-level function), public_read_write (literal, a string literal in validateSecurityPosture))
  • content/docs/releases/v15.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture), public_read (literal, a string literal in validateSecurityPosture))
  • content/docs/releases/v16.mdx (via public_read_write (literal, a string literal in validateSecurityPosture), os explain (command, read off packages/cli/src/commands/explain.ts))
  • content/docs/releases/v17/17-1.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture))
  • content/docs/releases/v17/17-2.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture), public_read (literal, a string literal in validateSecurityPosture), public_read_write (literal, a string literal in validateSecurityPosture))
  • content/docs/releases/v17/17-3.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture))
  • content/docs/releases/v17/index.mdx (via controlled_by_parent (literal, a string literal in validateSecurityPosture))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/cli/README.md, packages/lint/package.json, packages/lint/src/index.ts, …) — pages documenting those are invisible to this run
  • 2 anchor(s) matched too much of the corpus to be a work list: os lint (command, 34 pages), os validate (command, 61 pages)
  • 8 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 — 30 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 54c3ce10ce8cf89290b67813c34d1f10dbab6938 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a93abdc9daee6b74fa6de19c5609a5e71f061da5 — the merge of head 1e016895c333ac726d83467331777fa84da91849 into base 54c3ce10ce8cf89290b67813c34d1f10dbab6938, 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 a93abdc9daee6b74fa6de19c5609a5e71f061da5 && git checkout a93abdc9daee6b74fa6de19c5609a5e71f061da5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 54c3ce10ce8cf89290b67813c34d1f10dbab6938 1e016895c333ac726d83467331777fa84da91849 && git checkout -B drift-repro 54c3ce10ce8cf89290b67813c34d1f10dbab6938 && git merge --no-ff 1e016895c333ac726d83467331777fa84da91849

node scripts/docs-audit/affected-docs.mjs --json 54c3ce10ce8cf89290b67813c34d1f10dbab6938

⚠️ 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 54c3ce10ce8cf89290b67813c34d1f10dbab6938 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

claude added 3 commits October 8, 2026 18:52
…` / `__proto__` are refused, not crashed

The schema lookup this change restructured read `SCHEMAS[name]` bare, so
`os explain constructor` printed "Schema: Object … undefined" and threw
`schema.required is not iterable` (exit 1, stack trace). Both lookups are
own-key reads now; a prototype key is an unknown id like any other.

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
claude added 2 commits October 8, 2026 19:36
…s message; four context-only hints state their fix

`os validate` / `os build` label every finding's hint `fix:`. Five
producers put something else there: expression-invalid quoted the authored
source (printed as `fix: source: …`), and component-props-invalid,
flow-time-relative-descriptor-invalid, react-prop-missing-required (the
contract description branch) and liveness-experimental-property carried
context only. The source now ends the expression-invalid message
(` — source: …`, the flow engine's spelling) with an empty hint; the other
four lead with the instruction.

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
…l, source in the verdict); changeset line

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl and removed size/l labels Oct 8, 2026
… reworded hints that run at the publish gate

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 74bed8f56ca26da15e3b25be7a74402c0fe946c3
Local-runs: none

Read: card #22161 (body and all six comments), PR #22339 (body, the 28-file list, the net diff against its base e9a1f5c — 28 files, +919 / -81), the check-runs on the head, and the files the diff touches as they stand at the head (git show, no checkout, no build, no test).

① Derived judgments

  • @objectstack/lint public surface widens — RULE_EXPLANATIONS, explainRule() and the type RuleExplanation from the root barrel, plus the new subpath @objectstack/lint/rule-explanations (package.json#exports, a third tsup entry; files: dist covers it; rule-id-barrel-exports.test.ts registers the entry). The module imports nothing, pinned by a source scan. Right.
  • os explain accept-set widens — one positional: an own-key, lowercased schema lookup first, then an exact, case-sensitive rule id; the two sets are pinned disjoint; --json prints { rule, covers, paragraphs } for a rule, and the no-argument listing adds rules: [{ id, covers }]. Right.
  • os explain constructor / __proto__ / toString — crashed on main, now exit 1 as unknown; a fix on an accidental accept, pinned. Right.
  • The unknown-id error text changes on both faces — Unknown schema: X becomes Unknown schema or rule id: X, including the --json error field. Right in behaviour; the changeset does not name the string change — fold it into the changeset fix under ②.
  • field-no-consumers — message is one verdict sentence without the location, hint is the fix (carrier sites appended on carrier-only); verdict / carriers / rootsScanned unchanged; validateFieldConsumers is CLI_ONLY in the registry (authoring-rules.ts:1445), so no runtime wire changes; os validate --json warnings keep where: message. Right.
  • security-owd-unset — one verdict sentence, one fix; validateSecurityPosture is CLI_AND_RUNTIME with runtimeTypes ['seed', 'permission', 'book', 'object'] (:2014), so an object write refused at the metadata door now carries the new message and hint on its 422. Right in substance; the changeset does not say so — ②, FAIL reason 2.
  • expression-invalid — message ends with — source: ... when the source is non-blank, hint is ''; CLI_AND_RUNTIME (:678). The wire issue's hint is z.string() with no minimum (packages/spec/src/api/protocol.zod.ts:604), so an empty hint is contract-legal; printAuthoringRuleErrors skips it; the fix, where the rule has one, rides the message. Right. The deduped operator log in packages/metadata-protocol/src/runtime-authoring-gate.ts:1130 now renders () for this rule — cosmetic, another lane's file; escalated under ③.
  • component-props-invalid — message gains its consequence clause, hint is now an instruction; validateComponentProps is CLI_ONLY (:1198), as the changeset says. Right.
  • flow-time-relative-descriptor-invalid — hint leads with the instruction; validateFlowTriggerReadiness is CLI_AND_RUNTIME (:1359), so the 422 hint text changes, and the changeset names it. Right.
  • react-prop-missing-required — hint is now Pass REQ={…}: DESCRIPTION (or the contract pointer). The rule runs only as a member of reference-integrity-suite.ts (:631); that suite's registry entry validateReferenceIntegrity is CLI_AND_RUNTIME with runtimeTypes ['flow', 'view', 'object', 'dataset', 'report'] (:1152) — no page write dispatches it, and runtime-gate.ts:570 selects by runtimeTypes, so this rule can never appear on a 422. The hint change is right; the changeset's claim that it reaches the runtime publish gate's 422 issue hint is wrong — ②, FAIL reason 1.
  • liveness-experimental-property — default hint leads with an instruction; lintLivenessProperties is CLI_AND_RUNTIME (:1750), named in the changeset. Right.
  • CLI text faces — every printed hint is labelled fix: through printAuthoringAdvisories and printAuthoringRuleErrors (build, validate, verify, init); the rule: pointer is spelled once in explainPointer() and is '' for an id the table lacks; os validate now prints the two lines under each registry ⚠ line — the index map is keyed at push time and warnings is only appended to between the push and the print loop (read at the head); the --json / --strict list is unchanged; os lint gains the pointer. format.ts now imports the import-free entry, and @objectstack/lint is already a workspace:* dependency of the CLI. Right.
  • Docs — build-with-claude-code.mdx keeps its hand-wrap with — source: on a continuation line; the two react-pages.mdx blocks are re-rendered (:403 was stale on main); the README row. Right.
  • Tests — new pins rule-explanations.test.ts, explain-rule-id.test.ts, rule-line-explain-pointer.e2e.test.ts; three updated pins; validate-build-gate-parity classifies authoringFindingDetailLines as presentation. No pin of either old message survives at the head outside packages/lint/CHANGELOG.md history and docs/audits/**. Right.
  • Not changed, correctly — build.ts / dev.ts (H4 measured false), the action-governance boot log in packages/objectql, the second-stage rules the PR body lists, and the three files open PRs edit.

② Semver level

.changeset/22161-rule-message-one-line.md: @objectstack/lint minor, @objectstack/cli minor, Clause-②: yes (widening). The level matches the diff: new exports, a new subpath, a widened CLI accept-set; nothing an author can write is removed or renamed, so no migration and no ADR-0087 marker is owed. The PR body's Clause-②: yes (widening: …) carries prose inside the arm's parentheses; the changeset's line is the clean closed-pair form and Check Changeset is green — acceptable.

The changeset's text does not match what the diff publishes on the runtime wire:

  1. It says the reworded hints reach "the runtime publish gate's 422 issue hint for the three of them that run there: the time-relative, react-prop and liveness rules". react-prop-missing-required has no runtime path (①), so the CHANGELOG that ships would claim a wire change the diff does not make. Two rules run there, not three. The round-2 report (6068666691) and the seat check (6068765071) repeat the count unverified.
  2. It does not name the security-owd-unset 422 message and hint text change the diff does make on every refused object write. The PR body's Acceptance notes say it; the changeset, which ships, does not — and the seat's own order (6067462250, items 2 and 5) requires a runtime-wire change to be stated there.

Both are text fixes to the changeset, one commit; name the os explain unknown-id error-string change in the same edit. The level stays minor.

③ Boundary flags

  • open_questions: [] in both reports (6067329013, 6068666691) — nothing to answer.
  • Round-1 deviations — the landings outside the claim's surface (format.ts, validate.ts, lint.ts, the lint entry files, the README): each measured, accepted by the seat in 6067462250, and verified here; the validate.ts index map is sound; the own-key explain.ts fix is pinned and in the changeset; the branch behind origin/main was merged in round 2. Answered.
  • Round-2 deviation, expression-invalid hint: '' — answered in ①: contract-legal, the fix rides the message, the printer skips it. The () operator log line in metadata-protocol is escalated as a follow-up for stage 2 or the metadata-protocol lane; not a blocker here.
  • Round-2 deviation, the component-props-invalid message change — CLI_ONLY, verified. Answered.
  • The item-1 stop condition — I read the borderline rows the dev named as fixes (validate-component-types.ts:150, validate-flow-trigger-readiness.ts:632, runtime-gate.ts:1020, lint-liveness-properties.ts:254 / :273); each carries an instruction or a spelling to write, so five was the measured count and item 2 was owed. Answered, agreed.
  • out_of_scope_findings — the second stage stays on [maintainer] validate: the field-no-consumers warning 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 (the PR is Part of; the card returns to pm:queue): right. react-pages.mdx :403 stale on main: fixed here. The security-owd-unset runtime note is the one that belonged in the changeset — FAIL reason 2.
  • The objectui saveAdvisoryToast.ts:97 trace (an empty hint is skipped) is the seat's reading; it is outside this brief's inputs and taken as reported.
  • Check-runs on the head, read at 2026-10-08T21:00Z: 41 completed success, 6 skipped (the duplicate-event runs of the label and size jobs, and the two opt-in jobs), and 2 still in progress — Test Core (1/6) and Lint & Repo Gates. Their conclusions are the gate verdicts; none was re-run here. This record does not wait on them: the verdict stands on ② either way.

Implemented-by: claude/issue-22161-rule-message-one-line
Reviewed-by: session_01DhTqaEHqPVSVnAkjG3jywn

VERDICT: FAIL

FAIL reasons:

  1. The changeset names react-prop-missing-required among the rules whose new hint reaches the runtime 422; the rule has no runtime path (validateReferenceIntegrity runtimeTypes exclude page), so the shipped CHANGELOG would claim a wire change the diff does not make.
  2. The changeset omits the security-owd-unset 422 message / hint text change the diff does make (validateSecurityPosture is CLI_AND_RUNTIME, runtimeTypes include object).

Next: one changeset commit correcting both (naming the os explain unknown-id error-string change with them), then a fresh ## Contract review on the new head.


Generated by Claude Code

…ed hint reaches a 422, security-owd-unset at the object door, the os explain unknown-id string

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1e016895c333ac726d83467331777fa84da91849
Local-runs: none

Written 2026-10-08T21:18Z. Read: card #22161 (body and all seven comments, the prior record 6068965879 on the PR included), PR #22339 (body, the 28-file list, the net diff against its merge base e9a1f5c40 — 28 files, +921 / -81; the round-3 delta 74bed8f56..1e016895c is the changeset alone, +4 / -2), the check-runs on the head, and the files the diff touches as they stand at the head (git show, no checkout, no build, no test, no gate re-run). Not read: the dispatch order or the dispatching seat's conclusions.

① Derived judgments

  • @objectstack/lint public surface widens — RULE_EXPLANATIONS, explainRule() and the type RuleExplanation from the root barrel, plus the subpath @objectstack/lint/rule-explanations (package.json#exports, a third tsup entry, registered in rule-id-barrel-exports.test.ts); the module imports nothing, pinned by a source scan. Right.
  • os explain accept-set widens — one positional: an own-key, lowercased schema lookup first, then an exact, case-sensitive rule id (explain.ts); the two sets pinned disjoint; --json prints { rule, covers, paragraphs } for a rule; the no-argument listing adds rules: [{ id, covers }]; constructor / __proto__ / toString exit 1 as unknown instead of crashing. Right.
  • Unknown-id error text — Unknown schema: X becomes Unknown schema or rule id: X on the text face and in the --json error field, with a second Rules with an explanation: line. Right in behaviour, and now stated in the changeset (the prior record's open item).
  • field-no-consumers — message is one verdict sentence without the location, hint the fix (carrier sites appended on carrier-only); verdict / carriers / rootsScanned unchanged; validateFieldConsumers is CLI_ONLY (authoring-rules.ts:1445), so no runtime wire changes. Right.
  • security-owd-unset — one verdict sentence, one fix; validateSecurityPosture is CLI_AND_RUNTIME with runtimeTypes ['seed', 'permission', 'book', 'object'] (:2014–:2015), the finding is severity: 'error' (validate-security-posture.ts:482), and the gate judges state: 'active' only (runtime-authoring-gate.ts:1037), so an active object write without sharingModel carries the new message and hint on its 422; toIssue (:841) keeps where and path. The changeset now states exactly this, with both strings verbatim. Right.
  • expression-invalid — message ends with — source: ... when the source is non-blank, hint is ''; validateStackExpressions is CLI_AND_RUNTIME, runtimeTypes ['flow', 'action', 'hook', 'object'], severity: i.severity ?? 'error' (:678–:682), so an error lands in the 422 and a warning in the 2xx advisories — as the changeset says. The wire hint is z.string() with no minimum (packages/spec/src/api/protocol.zod.ts:604), so '' is contract-legal; printAuthoringRuleErrors skips it. Right.
  • component-props-invalid — message gains its consequence clause, hint is an instruction; validateComponentProps is CLI_ONLY (:1198), as the changeset says. Right.
  • flow-time-relative-descriptor-invalid — hint leads with the instruction; validateFlowTriggerReadiness is CLI_AND_RUNTIME, runtimeTypes ['flow'] (:1359–:1360), the finding is severity: 'error' (validate-flow-trigger-readiness.ts:489), so the new hint reaches the 422 hint on flow writes — as the changeset says. Right.
  • liveness-experimental-property — default hint leads with an instruction; lintLivenessProperties is CLI_AND_RUNTIME, runtimeTypes ['email_template', 'mapping', 'datasource'], always severity: 'warning' (:1750–:1754); the only "status": "experimental" row in packages/spec/liveness/*.json at the head is in tool.json:45, so no runtime response carries it today — as the changeset says. Right.
  • react-prop-missing-required — hint is Pass REQ={...}: DESCRIPTION or the contract pointer. validateReactPageProps runs only as a member of reference-integrity-suite.ts (:631), whose registry entry validateReferenceIntegrity has runtimeTypes ['flow', 'view', 'object', 'dataset', 'report'] (:1152–:1153); the gate's own page-source path emits only jsx-* and page-requires-disagrees-with-source. No page write dispatches it, so the text reaches the CLI only — as the changeset now says (the prior record's FAIL reason 1, corrected). Right.
  • CLI text faces — every hint printed through printAuthoringAdvisories and printAuthoringRuleErrors (compile/build, validate, verify, init) is labelled fix:; the os explain pointer is spelled once in explainPointer() and is '' for an id the table lacks; os validate prints the two lines under each registry ⚠ line via an index map keyed at push time, and warnings is only appended to between the push (validate.ts:858–:869) and the print loop (:961), so the indices hold; the --json / --strict list is unchanged; os lint gains the pointer. format.ts imports the import-free entry; @objectstack/lint is already a workspace:* dependency of the CLI. Right.
  • Docs — build-with-claude-code.mdx keeps its hand-wrap with — source: on a continuation line; the two react-pages.mdx blocks match the printer, and the :403 fix line quotes the contract's own description (react-blocks.ts:239, "The object this block binds to (server-connected)."); the README row. No hand-written page or skill at the head still describes os explain as schema-only or quotes the old warning (release-owned pages and docs/audits/** are history, left alone). Right.
  • Tests — rule-explanations.test.ts, explain-rule-id.test.ts, rule-line-explain-pointer.e2e.test.ts (nightly tier; its one out-of-package path lands in node_modules), three updated pins, validate-build-gate-parity classifying authoringFindingDetailLines as presentation. No pin of either old message survives at the head outside packages/lint/CHANGELOG.md history. Right.
  • Not changed, correctly — build.ts / dev.ts (H4 measured false), the action-governance boot log in packages/objectql, the second-stage rules the PR body enumerates, the three files open PRs edit. Governed paths touched: 0; 1,002 changed lines.

② Semver level

.changeset/22161-rule-message-one-line.md: @objectstack/lint minor, @objectstack/cli minor (both released, 17.7.0). The level matches the diff: new exports, a new subpath, a widened CLI accept-set, text changes on the CLI faces and on the runtime wire, nothing an author can write removed or renamed — so no migration and no ADR-0087 marker is owed, and no other released package changes. Every runtime-wire claim in the changeset now matches the code (①): the two rules whose new hint reaches the gate, security-owd-unset's 422 message / hint verbatim, expression-invalid's message suffix and empty hint, and the os explain unknown-id string on both faces — the three items the prior record and seat order 6068983639 required, all present and correct. Check Changeset is green on this head (both runs).

Clause-②: yes (widening) — right: the diff widens (os explain accepts a rule id; packages/lint exports the explanations) and narrows nothing. The changeset carries the clean closed-pair spelling; the PR body's line adds prose inside the arm's parentheses, which the gate accepted.

③ Boundary flags

  • open_questions: [] in both dev reports (6067329013, 6068666691) — nothing to answer.
  • Round-1 deviations — the landings outside the claim's surface (format.ts, validate.ts, lint.ts, the lint entry files, the README), each measured and accepted by the seat in 6067462250, verified here; the own-key explain.ts fix pinned and in the changeset; the branch behind origin/main merged in round 2 (d33862bde, merge base e9a1f5c40). Answered.
  • Round-2 deviation, expression-invalid hint: '' — contract-legal (①); the fix, where the rule has one, rides the message; the printer skips an empty hint. The deduped operator log line in packages/metadata-protocol/src/runtime-authoring-gate.ts:1130 now renders () for this rule — cosmetic, server-log only, another lane's file: escalated as a follow-up for stage 2 or the metadata-protocol lane, not a blocker.
  • Round-2 deviation, the component-props-invalid message change — CLI_ONLY, verified, stated in the changeset. Answered.
  • The item-1 stop condition (five non-fix hints, none in the three excluded files) — the borderline rows the dev named each carry an instruction or a spelling to write; five was the count and item 2 was owed. Answered, agreed.
  • out_of_scope_findings — the second stage stays on [maintainer] validate: the field-no-consumers warning 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 (the PR is Part of; the card returns to pm:queue): right. react-pages.mdx :403 stale on main: fixed here. The security-owd-unset runtime note: now in the changeset. Answered.
  • The objectui saveAdvisoryToast.ts:97 trace (an empty hint is skipped) is the seat's reading, outside this brief's inputs, taken as reported.
  • Prior record 6068965879 FAIL reasons 1 and 2, and seat order 6068983639's three items: each corrected in the round-3 commit, verified above. Answered.
  • Check-runs on the head, read 2026-10-08T21:16Z: 31 completed success (Build Core, Build Docs, Dogfood Verify CLI, Dogfood Regression Gate 1–3/3, Temporal Conformance, Test Core 2/6 and 3/6, Type Check source / consumer / debt ledger, Governed Surface Queue Guard, Check Changeset ×2, Spec property liveness, Validate Package Dependencies, the claim and single-writer guards, among them), 4 skipped (duplicate-event label/size jobs, Console Pin Gate, the opt-in tarball smoke), and 6 still in progress: Test Core 1/6, 4/6, 5/6, 6/6, Type Check · workspace, Lint & Repo Gates. Their conclusions are the gate verdicts; none was re-run here, and this record does not wait on them. Landing still requires every check green.

Implemented-by: claude/issue-22161-rule-message-one-line
Reviewed-by: session_01DhTqaEHqPVSVnAkjG3jywn

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 21:39
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 21:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 54d32e5 Oct 8, 2026
45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file 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