Skip to content

fix(lint): one-line verdicts for the approval-approver, data-model, agent-authoring, view-reference and chart-binding rules; os explain RULE_ID carries their reasoning - #22742

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-22161-s2-lint-slice-7
Oct 11, 2026
Merged

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

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Part of #22161
Clause-②: yes (widening: three rule id constants exported from the barrel — RELATIONSHIP_MASTER_DETAIL_REQUIRED, RELATIONSHIP_DELETE_BEHAVIOR, ROLLUP_NON_NUMERIC_AGGREGAND; corrected by the seat on #22161, the changeset moves to minor)

Stage 2 of the card, slice 7: the 15 rule ids of five whole packages/lint source files — the approval-approver rules, the data-model rules (the three ADR-0120 uniqueness ids and three of os lint's data-model sweep), the AI agent-authoring rules, the view-reference rules and the chart-binding rules. The card stays open for the later slices listed under "Remaining for later slices" below.

What changes

  • 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 197 characters or fewer, the whole packages/cli unit tier's 188 or fewer, and os validate on the four example apps' 191 or fewer; the longest of each id was 201 to 480 before (199 to 478 in the rules' own suites).
  • Where the verdict keeps a name, it is the one the author needs to find the defect. The data-model ids keep their "object" / "object.field" subject, because os lint prints a lintDataModel issue's message with only a positional at objects[N]… beside it; the uniqueness ids keep the index name or its column list for the same reason. view-ref-nav-view-missing keeps the object's list views (they are the fix), quoted to at most three and then (and N more); chart-axis-not-selected quotes at most three names of the chart's selection, then and N more.
  • The manager arm of approval-approvers-may-resolve-empty stays conditional. Its verdict says what happens WHERE sys_user.manager_id is unset and asserts nothing about the column, which a static check cannot read; the sentence that said so explicitly is now the explanation's, and the pin that held it (does not claim a runtime fact it did not read) holds the conditional verdict and the explanation together.
  • Three rule id constants are published (the dispatch's mechanism assumption 2, verified: relationship/master-detail-required's constant was module-private, and rollup/non-numeric-aggregand and relationship/delete-behavior were string literals). RELATIONSHIP_MASTER_DETAIL_REQUIRED, RELATIONSHIP_DELETE_BEHAVIOR and ROLLUP_NON_NUMERIC_AGGREGAND are exported from data-model-rules.ts beside the UNIQUE_* constants and re-exported from the root barrel (packages/lint/src/index.ts), so each RULE_EXPLANATIONS key is held to an exported constant (rule-explanations.test.ts) and rule-id-barrel-exports.test.ts stays green. The id strings are unchanged. Level: minor, as the seat's correction 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 (6103196579) orders: three new named exports widen the published surface, and the contract review 6103394547 (②) confirmed it.
  • 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 (for the three data-model sweep ids, on os lint's own printer too). 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; it ran in the unit tier below); node bin/run-dev.js explain unique/double-declaration prints the entry. The 87 entries already in the table are byte-equal (the diff of rule-explanations.ts is additions only).
  • 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 constant, or the removal of a value that only fed the old message. The plumbing hunks, each message-only:
    • validate-approval-approvers.ts: STAYS_LOCKED, the lock clause both empty-slate arms end with under the default lockRecord: true; two comments that described the old wording are corrected (the manager arm is conditional, and the explanation says why). MANAGER_ONLY_REMEDY and MANAGER_ONLY_ROUTES feed only the hint and are untouched (the dispatch's mechanism assumption 3: message and hint share no constant).
    • data-model-rules.ts: CBP_ORPHAN_CONSEQUENCE, the consequence the three controlled_by_parent shapes of R2 end with; cbpMasterReferenceFinding loses derivation and flagWords (both fed only the message); lintUniqueDeclarations loses globalSide / orgSide and lintLegacyOrganizationComposites loses spelling (each fed only the message); the module-private MASTER_DETAIL_REQUIRED becomes the exported RELATIONSHIP_MASTER_DETAIL_REQUIRED and two literals become constants (above).
    • lint-view-refs.ts: viewRoster() replaces listNames() in the nav verdict (the listNames import goes).
    • validate-chart-bindings.ts: UNKNOWN_CONSEQUENCE and UNSELECTED_CONSEQUENCE (records of strings, message-only) are cut to one clause each; selectionRoster() replaces list() in the not-selected verdict (list() still feeds the hints).
    • No helper changed shape (no string → boolean); no truth table is involved.
  • .changeset/22161-lint-slice-7-one-line.md: @objectstack/lint minor, naming every door that prints the new text (below). Clause-②: yes (widening: three rule id constants exported from the barrel), as line 2 says. Slice 1's no / patch reading covered RULE_EXPLANATIONS entries as data in an existing export; the three new constants are new named exports, so they are a widening (seat order 6103196579, contract review 6103394547 ②).
  • No rider was needed: packages/cli/test/data-model-rules.test.ts asserts fragments of the uniqueness and delete-behavior messages, and every fragment it pins (ADR-0120, the index names, CONTRADICT, silently dead, declared twice, NULL-distinct, the NULL-row clause, (cascade/restrict, the leading "object.field") is in the new verdicts, so it passes unedited (the whole unit tier ran, below). No packages/metadata-protocol file and no content/docs page quotes these messages (grep).

The verdict forms, the longest of each arm as the suites fired them (census below):

every approver on this node routes to a group (position/team/department), so if none is staffed the request opens on an empty slate and waits forever, and (lockRecord) the record stays locked
every approver on this node is { type: 'manager' }, so where sys_user.manager_id is unset the request opens on an empty slate and waits forever, and (lockRecord) the record stays locked
approver { type: 'org_membership_level', value: 'sales_manager' } names no org-membership tier (owner/admin/delegated_admin/member), so it matches nobody and the request stalls
"sys_account" index 'uniq_org_email' [organization_id, email] has bare `unique: true`, an unstated scope protocol 18 refuses (ADR-0120): it built the index installation-wide
"crm_product.sku" field `unique: true` and index 'uniq_product_sku' `unique: 'global'` CONTRADICT: the installation-wide one wins, so the per-organization intent is silently dead
"crm_product.sku" field `unique: 'organization'` and index `unique: 'organization'` both ask for per-organization uniqueness: the same index declared twice
"pp_order" index 'pp_order_name_uq' [name, organization_id] lists the organization column, and SQL UNIQUE is NULL-distinct: on every row whose 'organization_id' is NULL it enforces nothing
master_detail "work_order_item.order" → work_order must be required and not marked readonly or system: a controlled_by_parent detail saved without its master is readable by nobody
master_detail "work_order_item.order" → work_order is marked readonly, which skips its required check: a controlled_by_parent detail saved without its master is readable by nobody
master_detail "crm_quote_line_item.quote" → crm_quote should declare deleteBehavior (cascade/restrict): left unset, deleting the master deletes its details
summary field "invoice.rolled_up" rolls up max(invoice_line.shipped_at), a datetime field: max answers in the child field's own type, but a summary field stores a finite number
This stack declares the agent "sales_copilot", but app-package agents were withdrawn (ADR-0063 §2): the runtime filters it out of the agent catalog and refuses to load it, so it never runs
This stack declares an agent named "build", which is a PLATFORM agent id, so the runtime serves its own record for that name and this declaration has no effect
app "studio" pins `defaultAgent` to "metadata_assistant", the RETIRED alias of platform agent "build": it still resolves, but only through the alias registry
app "crm" pins `defaultAgent` to "sales_copilot", which is not a platform agent (ask, build), so it silently falls back to the platform default and the pin has no effect
Navigation entry opens view 'edit' on object 'duly_task', which has no such list view (the name resolves to a FORM view), so it silently opens the default view. List views: board, default, schedule
View key collision: the form view 'task.default' was renamed to 'task.default_2', so every reference to 'task.default' resolves to the OTHER view
"estimate_hours" is not a measure declared by dataset "task_metrics", so this series comes back empty (result rows are keyed by measure name, not the base field)
"estimate_hours" is not a measure declared by dataset "task_metrics": this `yAxis[].field` is axis PRESENTATION and the plotted columns come from `values`, so it re-points nothing
"task_count" is a declared measure of "task_metrics" outside this chart's selected values (est_hours): this display-name override pairs only with `chart.yAxis`, so it lands on nothing

Shared prose: written once (the dispatch's route)

shared sentence ids verdict clause (rule files) explanation (rule-explanations.ts)
the record stays locked both arms of approval-approvers-may-resolve-empty STAYS_LOCKED — (the entry states the recovery once)
a controlled_by_parent detail without its master is unreadable the three error shapes of relationship/master-detail-required CBP_ORPHAN_CONSEQUENCE — (the entry states the derivation once)
the ADR-0120 scope words unique/unscoped-declared-index, unique/double-declaration — UNIQUE_SCOPE_WORDS
where lintDataModel's sweep reports relationship/master-detail-required, relationship/delete-behavior, rollup/non-numeric-aggregand — DATA_MODEL_SWEEP_REACH
the platform agent roster and its retired aliases all three agent ids — PLATFORM_AGENT_ROSTER
which chart surfaces are judged chart-measure-unknown, chart-axis-not-selected — CHART_BINDING_SURFACES

The explanation module imports nothing, so a fact it writes out is held to its source by the rule's test: the agent aliases (data_chat → ask, metadata_assistant → build) by running the rule on each, the membership tiers by the verdict reading BUILTIN_MEMBERSHIP_ROLES, and each shared paragraph by asserting it is one text under every id it serves.

Census (taken first, before any edit, at the base e5899a67d6)

Method: slice 4's scratch preload (NODE_OPTIONS=--import, never committed), which patches Array.prototype.push to record every finding-shaped object (rule + message) of the five files' ids, deduped by (rule, message), with its push site, in every process. Lengths are message alone; the printed line adds where and : . Rows from os lint's own printer (commands/lint.ts) are excluded. Positive control in every run: field-no-consumers (or, in the runtime-gate suite, the approval file's other ids) added to the recorded set and recorded.

  • packages/lint suite, base 135 files / 6,304 tests (after the CLI and example closure build put the dist/ its dist-reading tests wait for in place): all 15 ids fire, from 17 push sites (source lines), every one over 200 except relationship/delete-behavior (199 here).
  • packages/cli unit tier, the whole project (280 files, 4,162 tests, all loaded — packages/cli/dist was built): fires six of the 15 — agent-authoring-withdrawn, relationship/delete-behavior (201, the one reading over 200 only this tier reached), relationship/master-detail-required (warning arm only), and the three unique/* ids (packages/cli/test/data-model-rules.test.ts). Control field-no-consumers recorded. view-key-collision does not fire here (slice 2's list carried a cli-only 278 for it). The integration tier is declared to CI.
  • Runtime publish gate: the whole @objectstack/metadata-protocol suite (226 files, 223 run, 28,101 passed), which reads @objectstack/lint from its built dist/, before on the base-built dist and after on the rebuilt one. It fires none of the 15 ids; control: the approval file's approval-expression-invalid and approval-expression-no-empty-policy recorded in both runs.
  • Example apps: os validate (the built CLI) on app-crm, app-todo, app-showcase and app-multi-package, all exit 0: app-showcase fires approval-approvers-may-resolve-empty (both arms, with and without the lock clause); control field-no-consumers recorded.
  • After (lint dist rebuilt from the slice, marker so every reference to 1 hit each in dist/index.js, index.cjs, runtime.js, runtime.cjs, old clashes with another view 0): the same runs at 0ed57745b0 (lint source byte-identical to the head 4077426704, which changes two numbers in the changeset).
rule id lint suite: before (msgs · longest) → after (msgs · range) cli unit tier: before → after os validate examples: before → after
approval-approvers-may-resolve-empty 4 · 451 → 4 · 143–191 — 3 · 451 → 3 · 149–191
approval-approver-not-membership-tier 3 · 272 → 3 · 160–176 — —
unique/unscoped-declared-index 3 · 427 → 3 · 139–173 4 · 418 → 4 · 134–164 —
unique/double-declaration 1 · 381 → 5 · 144–178 9 · 402 → 9 · 142–178 —
unique/legacy-organization-composite 1 · 478 → 4 · 163–190 5 · 480 → 5 · 163–188 —
relationship/master-detail-required 7 · 462 → 7 · 119–179 2 · 122 → 2 · 115–122 —
relationship/delete-behavior 3 · 199 → 3 · 152–153 2 · 201 → 2 · 148–155 —
rollup/non-numeric-aggregand 43 · 364 → 43 · 171–181 — —
agent-authoring-withdrawn 12 · 352 → 12 · 157–188 1 · 350 → 1 · 186 —
default-agent-outside-roster 4 · 388 → 4 · 160–169 — —
default-agent-legacy-alias 2 · 466 → 4 · 141–157 — —
view-ref-nav-view-missing 4 · 437 → 5 · 161–197 — —
view-key-collision 2 · 257 → 2 · 136–145 — —
chart-measure-unknown 16 · 442 → 16 · 152–179 — —
chart-axis-not-selected 4 · 327 → 5 · 160–183 — —

All 17 push sites fire before and after. A higher message count after is a case this slice added (the unique-verdict file's quadrants, the agent roster probe, the selection-roster case, the three-list-view case); relationship/master-detail-required's warning arm (115–122) is unchanged text. The runtime-gate suite is not a column: it fires none of the 15 before or after.

Doors that print the new text

Read from the registry (authoring-rules.ts: validateApprovalApprovers is commands: ALL, CLI_AND_RUNTIME, runtimeTypes: ['flow']; lintViewRefs and lintUnscopedDeclaredIndexes are commands: ALL, CLI only; lintUniqueDeclarations and lintLegacyOrganizationComposites are ['validate', 'build'], CLI only, and reach os lint through lintDataModel; the reference-integrity suite, CLI_AND_RUNTIME, dispatches validateChartBindings on ['flow', 'report'] and validateAiAgentAuthoring on the default ['flow']), the runtime gate's split (runtime-authoring-gate.ts: errors → the 422, everything else → 2xx advisories plus the deduped [Protocol] authoring advisory log line), and the CLI's callers (commands/validate.ts, compile.ts, lint.ts; os verify runs the validate set and the os init / os generate scaffold check the build set):

door ids what changes
os validate, os build (and os compile, which os dev runs per compile), os lint, os verify, the scaffold check the approval, agent, view-reference and chart ids and the three unique/* ids 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
os lint only (and the metadata-generation rubric's lint half) relationship/master-detail-required, relationship/delete-behavior, rollup/non-numeric-aggregand the verdict line and the pointer, as above
runtime publish gate, flow write approval-approvers-may-resolve-empty (info), approval-approver-not-membership-tier (warning) the 2xx advisories entry's message and the advisory log line
runtime publish gate, report write chart-measure-unknown at a report's values or chart yAxis (error); at a report chart's series[] (warning); chart-axis-not-selected (warning) the 422 issue's message and the OS_ALLOW_UNLINTED_METADATA_WRITES refusal log line for the error; the advisories entry and the advisory log line for the warnings
never at the runtime gate the agent ids (a flow write's snapshot carries no agents or apps), the view-reference and unique/* ids (CLI only), the sweep ids (os lint only), the chart ids at a list-view or page chart (a report write carries neither surface)
unchanged every hint; every other rule id

Every row is named in the changeset.

Tests

The rule suites import the rule source; the CLI, runtime-gate and example runs read the rebuilt @objectstack/lint dist. Every heavy run went through scripts/pm/os-verify-lock.sh; its VERDICT lines are quoted.

  • Each rule's own suite pins the new shape, as in slices 2–6. validate-approval-approvers.test.ts, validate-ai-agent-authoring.test.ts, lint-view-refs.test.ts, validate-chart-bindings.test.ts, data-model-rules.master-detail-required.test.ts and data-model-rules.summary-rollup.test.ts wrap their rule import and record every finding their cases fire; a final block in each holds every recorded verdict of its ids to one line of at most 200 characters, behind a coverage control (each id fired, and each arm: both empty-slate arms with and without the lock clause, both declaration arms, both nav arms, the query and three presentation positions, R2 at both severities). The new data-model-rules.unique-verdicts.test.ts does the same for the three uniqueness ids over every arm. Exact pins hold one verdict per arm; explanation pins hold, per id, that explainRule(id) exists and names what the verdict stopped saying, that each shared paragraph is one text under every id it serves, and that the aliases the explanation spells are the ones the rule resolves. Every refusal assertion, rule id, severity, path, where and hint pin is unchanged; the one prose pin that read the old text (does not claim a runtime fact it did not read) now holds the conditional verdict and the explanation.
  • Lint suite: pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 at 0ed57745b0: Test Files 136 passed (136), Tests 6,334 passed (6,334); VERDICT command-exit 0 (base 135 / 6,304; one new file, 30 new cases).
  • Lint build + typecheck: pnpm --filter @objectstack/lint build && pnpm --filter @objectstack/lint run typecheck: VERDICT command-exit 0; check-dts-emitted 6/6; check:test-typecheck OK, 2 files / 6 errors / 2 pinned signatures held (the test layer compiles under tsconfig.test.json, which reaches the new test file).
  • CLI unit tier, against the rebuilt lint dist: pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: Test Files 280 passed (280), Tests 4,162 passed (4,162), before and after; VERDICT command-exit 0 both. It includes test/explain-rule-id.test.ts (iterates every RULE_EXPLANATIONS key, so the 15 new ids resolve through os explain, explainPointer and the listing) and test/data-model-rules.test.ts (unedited). node bin/run-dev.js explain unique/double-declaration prints the entry.
  • Runtime gate: the whole @objectstack/metadata-protocol suite, before and after: Test Files 223 passed | 3 skipped (226), Tests 28,101 passed | 19 skipped, both runs; VERDICT command-exit 0.
  • Spec corpus walkers: pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/shared/retired-key-migrate-sentence.test.ts src/identity/position-delegatable-enforcer.pin.test.ts (the two spec tests that read packages/lint/src: the os migrate meta sentence scanner, whose one hit in these files is the unchanged unique/unscoped-declared-index fix, and the exported security-* constants, none added): Test Files 2 passed (2), Tests 19 passed (19).
  • Tracker ids: no # plus digits in any printed message or explanation (rule-explanations.test.ts refuses one in every paragraph); the citations stay in // comments and test names.
  • Ablation (one-shot, from the committed state 0ed57745b0, through scripts/ablation-replace.mjs wrap mode under the verify lock, plus a shell trap restoring HEAD by absolute path). The rule suites import the rule source, so there is no dist leg. view-key-collision's pre-slice message was restored verbatim (the replacement block read from the base file) through the anchor of its new message: anchor x1 → x0, blob 483ffe09850e → 057794818e4b. Predicted before the run: exactly two cases red — the 200-character bound pin and that id's exact-text pin; every other assertion on the id pins fragments the old text also carries ('task.default', 'task.default_2', 'task.mine'). Observed: src/lint-view-refs.test.ts Test Files 1 failed (1), Tests 2 failed | 36 passed (38); the two red cases are exactly those two (the bound pin read 257 characters). Restored: blob 483ffe09850e == blob at HEAD, git diff HEAD empty, git status --porcelain empty.
  • ESLint, narrowed: npx eslint --no-inline-config --format json over the 14 changed .ts files gave 14 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 4077426704 (merge base a360cee92e; the derived set is identical to the one at d9f3e019c8), derived 62 commands. I ran 61 of them sequentially, each with its own log and its exit code captured before any pipe: all 61 exited 0. One is NOT MEASURED, as the dispatch directs: pnpm check:type-check-debt, which is check-type-check-coverage.mjs --re-measure in package.json (it builds every package); its non-re-measuring half is check:type-check-coverage, which ran and exited 0. --ran gave Run reconciliation — 62 derived, 61 run, 0 NOT-MEASURED, 1 UNRUN (that one). I also ran 34 families of the "Artifact rosters" block the same derivation prints (the 38 non-self-test rows, less check:engine-double-contract, which is also derived, and the three PR-context guards, which run against this PR once it exists): all exited 0, the four rostered under a directory this diff touches (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity) included. On a first full pass at 0ed57745b0, three refused rather than measured: check:dual-build-cjs-loads and check:published-readme-exports with PREREQUISITE NOT MET (six packages' dist/ absent in this worktree; I built exactly those six — client-react, embedder-openai, knowledge-ragflow, organizations, studio, service-cluster-redis — lock VERDICT 0, and both then exited 0), and check-plugin-teardown-shape once, reading a temp config the concurrently running CLI suite had just deleted (re-run, exit 0); the whole union then reran green at 4077426704, first time. The long ones on the shared box: check:query-options-erasure 291s, check:slot-lookup 152s, check-comment-mask-corpus 115s.

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

PR #22717's list minus this slice's five files and 15 ids. The lengths are slice 2's (PR #22448, census at 05c7c3fa3b), not re-measured here.

  • validate-dashboard-action-refs.ts (2): dashboard-action-route-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-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-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-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, and react-prop-deprecated as PR #22700's notes it. This slice touched none of them.

Acceptance notes

  • One more hint for the card's open hint call: the manager arm of approval-approvers-may-resolve-empty prints a 2,159-character fix: line (MANAGER_ONLY_REMEDY + MANAGER_ONLY_ROUTES + the escape), the same class as sharing-rule-runtime-variable-condition's 2,280. It points at nothing the verdict cut — it is self-contained — and is unchanged here, as every slice leaves hints. No other hint of the 15 ids refers to text the verdict cut.
  • field-no-consumers (stage 1's id) still prints a 341-character verdict on os validate of examples/app-showcase: its "a consumer of the same name on …" clause lists every other object that consumes the field name, unbounded. Measured by this slice's census control, not touched here; it is this card's own subject, for a later slice.
  • A dead clause, kept: UNSELECTED_CONSEQUENCE.query is unreachable since the report chart.yAxis stopped carrying a not-selected check (chart-axis-not-selected now fires only at presentation positions; every query position IS the selection). The record's type keys every MeasurePosition, so the entry stays, shortened like the rest; removing it is a type change outside a message-only slice. The chart-axis-not-selected explanation says a query position is never reported.
  • view-key-collision did not fire in the packages/cli unit tier at the base, though slice 2's list marked its 278 as a cli-only reading; the lint suite fires it at 257 → 145. Recorded as measured.
  • One file outside the claim's file surface: packages/lint/src/index.ts, three barrel lines for the three new constants. The dispatch's mechanism assumption 2 prescribes "an exported constant beside the others", and rule-id-barrel-exports.test.ts fails a rule id constant no published barrel re-exports, so the constant cannot land without the barrel line. No open PR touches the file.
  • A new test file: packages/lint/src/data-model-rules.unique-verdicts.test.ts pins the three uniqueness ids' verdicts over every arm (each spelling, named and unnamed index, a declared tenant column, all four scope quadrants of the double declaration). Their behaviour tests live in packages/cli/test/data-model-rules.test.ts, outside the rule's own package, so the shape pin has no existing lint file to join; the other three data-model ids join data-model-rules.master-detail-required.test.ts and data-model-rules.summary-rollup.test.ts.
  • A long name can still lengthen a line. The 200 bound holds on every variant the suites fire; the object, field, index, view, dataset and measure names in a verdict are the author's.

Generated by Claude Code

@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

40 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (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.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 4 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 f59a73c3950550c7e1085d06682cc9c0fdb54c5d → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json f59a73c3950550c7e1085d06682cc9c0fdb54c5d

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

…e id constants join the barrel

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4bd706494beae8a54e0f0874aafffba2b9a12680
Local-runs: none

Written 2026-10-10T23:38Z. Read: card #22161 (body and all 42 comments — the maintainer's dispatch quoted in the body, stage 1's FAIL 6068983639 and PASS 6069243709, slice 1's record 6072212345 with its ② ruling, slices 2–6's reports, ACCEPTs and landings, this slice's claim 6101937040, dev report 6103165581, seat order 6103196579, patch-round report 6103257449 and ACCEPT 6103267850), PR #22742 (body, the 15-file list, its one comment, the net diff against main at the merge base a360cee92e: 15 files, +1227 / -127, six commits including one merge of origin/main), the check-runs on the head, and — to verify sentences — the five rule sources, rule-explanations.test.ts, rule-id-barrel-exports.test.ts, authoring-rules.ts, reference-integrity-suite.ts, explain.ts, format.ts, object-graph.ts, field.zod.ts, objectql/src/engine.ts and packages/cli/test/data-model-rules.test.ts as they stand at origin/main, all by API read or git show — no checkout, build, test, gate or ablation. The seat's comments were read as claims, not findings.

① Derived judgments

  • Accept set unchanged — right. In the five rule files every changed line is a message expression, a comment, an import, a module-private string constant (STAYS_LOCKED, CBP_ORPHAN_CONSEQUENCE), one of the two roster helpers, the removal of a local that fed only the old message (derivation, flagWords, globalSide, orgSide, spelling), or a rule: value that moves from a literal or a private constant to the exported constant with the same string. A grep of the diff's added and removed lines for hint, fix:, severity, path:, if (, return, continue, && and || finds only those rule: renames and the two helpers' own return lines. Each firing condition is the one at main: both empty-slate arms (routable.every(GROUP_ROUTED_TYPES); !managerChainWired && routable.every(manager); lockRecord !== false default), the tier check (!MEMBERSHIP_TIERS.has(value.toLowerCase())), R2's if (!missingRequired && flags.length === 0) return undefined, R3's deleteBehavior === undefined, R13's min / max plus summaryRollupAnswerFitsColumn, the three uniqueness walks, the collision loop, the nav miss (resolvesViewId false after the requiresObject / recordId / interpolation / empty-namespace skips), and the chart checks (!ds.measures.has(name); a present, non-empty selected with !selected.has(name)). Severities (info, warning, error, suggestion, position === 'query' ? 'error' : 'warning'), every path, every hint / fix, and every rule id string are byte-identical.
  • The two roster helpers are not refactors of a condition — right. viewRoster() (replacing listNames()) and selectionRoster() (replacing list()) are called only inside the message expression, after the firing decision. They sort, keep three names and count the rest; listNames / list printed every name. The one behavioural fork — (none) for an empty set — is unreachable at both sites: the nav finding returns on ids.size === 0 before building available, and the not-selected finding runs only when selected.size is positive. list() still feeds the chart hints unchanged. Same inputs fire, same inputs pass; only the printed text differs.
  • Three new barrel exports — right, and forced. packages/lint/src/index.ts adds RELATIONSHIP_MASTER_DETAIL_REQUIRED, RELATIONSHIP_DELETE_BEHAVIOR, ROLLUP_NON_NUMERIC_AGGREGAND to the existing export { UNIQUE_* } from './data-model-rules.js' block, the idiom every earlier rule id constant uses. Two tests at main make the line unavoidable once a RULE_EXPLANATIONS key exists for these ids: rule-explanations.test.ts:40 refuses a key that is not an exported rule id constant, and rule-id-barrel-exports.test.ts:175 refuses an export const rule id no published barrel re-exports. At main MASTER_DETAIL_REQUIRED was module-private with no user outside the file, and the other two ids were literals. The id strings are unchanged. Nothing else is newly exported: the barrel diff is those three lines, and the new helpers and constants in the rule files are module-private.
  • Explanations say what the old text said, keyed by the right id — right. The 15 rule: strings match the constants' values at main, and the map keys by .rule. Each sentence the verdicts dropped is in its entry: the group arm's "members are runtime data" and "no in-product recovery"; the manager arm's "a static check cannot read that column … does not assert the slate IS empty"; sys_member.role and better-auth; the unscoped index's "over exactly its fields … while reading like unique per organization" and the unique: 'global' conversion; D5b's "physically stricter and wins … silently dead" and "redundant … single home"; S6's composite, the NULL-row clause and "every row on a single-organization deployment"; R2's derived read filter, "refused on every later write" and the readonly / system skip; R13's NUMERIC_VALUE_TYPES and the column the roll-up is stored in; the roster ask / build, "parses, validates and ships as inert metadata", the alias registry, the weaker pin and the silent fallback; the sidebar's label and icon and the view switcher; ADR-0021's measure-name keying and each presentation position's exact behaviour. Every fact the entries add beyond the old messages was checked against main and holds: onEmptyApprovers: 'fallback' not silencing the arm (validate-approval-approvers.ts:171–177), stackWiresManagerChain reading seeded sys_user.manager_id and the runtime gate carrying no seeds (:284–:310), record[value] ?? record.owner_id (:552–:554), the bad-value-wins ordering over approval-approver-type-deprecated (:435–:460), BUILTIN_MEMBERSHIP_ROLES, ADR-0108 and the case-insensitive compare (:51, :254, :440); fieldUniqueScope (true is organization) and indexUniqueScope (true is global) (data-model-rules.ts:385, :398), D5a / D7 and the two refusal channels (:402–:420), organization_id kernel-injected, tenancy.tenantField, COALESCE(organization_id, '__global__'), D5c's no auto-fix and the D4 ceremony (:364–:371, :580–:605); ADR-0055, assertControlledByParentWrite, ObjectSchema.create() forcing required: true, the builder never reading readonly / system, one finding per field (:260–:310); the min / max scope, the boolean admission, the analytics table returning rather than storing, silence on an unresolvable child (:118–:161, :785–:820); the aliases data_chat for ask and metadata_assistant for build with the one-way registry (validate-ai-agent-authoring.ts:117–:126), listAgents() / loadAgent() / 404, the ADR-0063 §1 fallback, the snake_case key and the enum walked back, ADR-0078 (:4–:62); resolveViewId's three directions, all not special-cased, defaultViewId || views[0] and the browser-console warning, the _2 rename (lint-view-refs.ts:22, :56–:67, :109); the joined report and report block drawing no chart, the react ObjectChart judged by validate-react-page-props, the report chart.yAxis carrying no not-selected check (validate-chart-bindings.ts:22, :36–:41, :291, :546). Two facts are corrections the old text did not carry: R3's new verdict clause "left unset, deleting the master deletes its details" is the engine's deleteBehavior === 'restrict' ? 'restrict' : 'cascade' (objectql/src/engine.ts:17640), and the entry's "set_null … refused at the parse" is field.zod.ts:2608 (the old "not honored" wording predates that refinement). The DATA_MODEL_SWEEP_REACH sentence on the metadata-generation rubric repeats the module doc at data-model-rules.ts:304–:307; lintDataModel's only non-test caller is commands/lint.ts:604, and scoreMetadata reaches the lint config through lint/score.js. The shared paragraphs (UNIQUE_SCOPE_WORDS, DATA_MODEL_SWEEP_REACH, PLATFORM_AGENT_ROSTER, CHART_BINDING_SURFACES) are one text each, and the tests hold them equal under every id they serve. Nothing invented; the diff of rule-explanations.ts is additions only, so the 87 earlier entries are byte-equal.
  • Information the verdicts now encode differently, judged acceptable. The contradiction verdict no longer labels which side is installation-wide; it prints both spellings, and the entry's scope words decode them. The nav verdict lists at most three list views where it listed all, and the fix: line's Did you mean still names the nearest. Both are the card's shape as slices 4–6 applied it.
  • Tests pin the new shape — right. Each of the six touched suites and the new data-model-rules.unique-verdicts.test.ts wrap the rule, record every finding of the converted ids, and hold each to no newline and at most 200 characters behind a coverage control (both empty-slate arms with and without the lock clause; both declaration arms; both nav arms; all four scope quadrants; the query and the three presentation positions; R2 at both severities). Exact pins hold one verdict per arm; explanation pins hold the moved facts per id, the shared paragraphs' identity, and the aliases by running the rule. The one prose pin that read the old text (does not claim a runtime fact it did not read) now holds the conditional verdict and the explanation together. The bound holds on every fixture; the names inside a verdict are the author's, as the PR body says. Outside packages/lint, packages/cli/test/data-model-rules.test.ts is unedited and every message fragment it asserts survives in the new verdicts ((cascade/restrict, ADR-0120 on the unscoped index, the index names, CONTRADICT, silently dead, declared twice, both unique: spellings, NULL-distinct, the NULL-row clause); the four markers validate-chart-bindings.test.ts keeps (comes back empty, DISPLAY-NAME override, REPLACES the authored array, axis PRESENTATION) are all in the new text.
  • os explain and the rule: pointer need no CLI change — right. explain.ts:7, :478, :492, :525 and format.ts:2007 read RULE_EXPLANATIONS wholesale, so the 15 ids resolve, list and gain the pointer by data alone. Every covers string is under the test's 60-character bound.
  • Public surface: the three constants above; no other export, subpath, type or exports-map change.

② Semver level

.changeset/22161-lint-slice-7-one-line.md: @objectstack/lint minor, Clause-②: yes (widening: three rule id constants exported from the barrel). The PR body's line 2 and the claim 6101937040 (corrected in place by the seat order 6103196579) carry the same value and arm. Judged right: three new named exports on the . entry enlarge the published surface, which is yes (widening) by the seat's rule and by stage 1's own reading, where new exports counted toward minor; slice 1's no / patch ruling covered RULE_EXPLANATIONS entries as data in an existing export and does not reach a new export. The patch precedent 5e3c83bd0b the dev cited does not bind that reading. Release state at main: pre mode next, @objectstack/lint in the single fixed group where stage 1's pending minor already sets the bump, so the level moves the CHANGELOG heading and no version number. The changeset's prose matches the diff — the 15 ids, the bounds measured per suite, fix / id / severity / path / accept set unchanged, the id strings unchanged, "match on rule and path", and every door — verified against the registry at main: validateApprovalApprovers commands: ALL, CLI_AND_RUNTIME, runtimeTypes: ['flow'] (both ids non-error, so 2xx advisories); lintViewRefs and lintUnscopedDeclaredIndexes ALL, CLI_ONLY; lintUniqueDeclarations and lintLegacyOrganizationComposites ['validate', 'build'], CLI_ONLY, reaching os lint through lintDataModel; the reference-integrity suite CLI_AND_RUNTIME dispatching validateChartBindings on ['flow', 'report'] (query positions error to the 422, presentation positions warning to advisories) and validateAiAgentAuthoring on the frozen ['flow'] default; the three sweep ids on os lint only. Check Changeset is green on this head.

One body inconsistency, escalated (no code): two bullets of the PR body predate the seat's correction and still say the opposite of line 2 and the changeset — under "What changes", the bullet ".changeset/22161-lint-slice-7-one-line.md: @objectstack/lint patch … Clause-②: no follows the contract review on slice 1", and the sentence "Precedent for the level … shipped as @objectstack/lint patch" in the constants bullet. The gate reads line 2 and the changeset file, both right, so this does not move the verdict; the seat, who owns body edits, corrects the two bullets before landing.

③ Boundary flags

  • open_questions (report 6103165581): Clause-② A or B. Answered B by the seat order 6103196579; this review reaches B independently (② above). Patch round 1 (6103257449) changed the changeset only, +2 / -2, and the head's lint source is byte-identical to the reviewed 4077426704. Answered.
  • out_of_scope_findings (four). (1) The manager arm's fix: line is 2,159 characters: MANAGER_ONLY_REMEDY / MANAGER_ONLY_ROUTES feed only the hint (verified; message and hint share no constant), it points at nothing the verdict cut, and hints are held unchanged by every slice — to the card's open hint call, as the seat routed it. (2) field-no-consumers prints 341 characters on os validate of app-showcase: stage 1's id, this card's own subject, a later slice. (3) UNSELECTED_CONSEQUENCE.query is unreachable: consistent with main (a query position is itself the selection, and the report chart.yAxis carries no not-selected check, :291); the record is typed over every MeasurePosition, so removing it is a type change outside a message-only slice, and the chart-axis-not-selected entry says a query position is never reported. Acceptance note. (4) view-key-collision not firing in the CLI unit tier: an observation; the lint suite fires it (257 to 145). Answered.
  • Deviations (seven). index.ts outside the claim's surface — forced by the two tests named in ①, declared with the mechanism the dispatch prescribed; right. The new test file — the three uniqueness ids' behaviour tests live in packages/cli/test, so the shape pin had no lint file to join; right. origin/main merged (9f4d15d2c6, packages/cloud-connection only) before the after-census; the merge base is a360cee92e and the diff is clean. Trailers — all five non-merge commits carry Claude-Session and Co-authored-by: Claude, no model identifier; right. The two light checks outside the verify lock, the killed first lint run and the queue-timeouts — process notes; every recorded measurement went through the lock. The stray /build-base.pid — outside the repository, nothing in the diff; the user deletes it. The CLI integration tier declared to CI — the Test Core shards carry it. Answered.
  • Escalated to the seat: (a) the two stale PR-body bullets (② above); (b) the needs:contract-review marker — this review makes no label write; the seat removes it on reading this PASS, as slice 1's record had it.
  • Check-runs on the head, read 2026-10-10T23:33Z: 32 runs, every one on 4bd706494b, none failed or cancelled — 23 success (Auto Label, Build Core, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate and its 1–3/3, Dogfood Verify CLI, Flag docs affected by code changes, Governed Surface Queue Guard, the two claim guards, the Part-of guard, the closing-branch guard, Temporal Conformance, Test Core 1/6 and 6/6, Type Check consumer gates, debt ledger and source gates, filter), 3 skipped (Build Docs, Console Pin Gate, the opt-in Packed-tarball smoke — the roster's expected skips), and 6 in progress at review time: Lint & Repo Gates, Test Core (2/6), Test Core (3/6), Test Core (4/6), Test Core (5/6), Type Check workspace. 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-s2-lint-slice-7
Reviewed-by: session_01S3aAf11JjbW1mSGL1EhfFj

VERDICT: PASS

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