Skip to content

Commit 9fbabd7

Browse files
fix(lint): one-line fix lines for 17 more author-time rule ids (slice 16); os explain RULE_ID carries what they explained (#23194)
Part of #22161 Clause-②: no Stage 2 of the card, slice 16: the over-long `fix:` lines, continued in slices 11 to 15's shape. Each finding's `hint` (the CLI's `fix:` line, the runtime issue's `hint`) of 17 more `packages/lint` rule ids, in twelve rule files, is now one remedy line of at most 200 characters, and what the lines used to explain is each id's `RULE_EXPLANATIONS` entry behind `os explain RULE_ID`. The card stays open: 24 author-time ids still carry a hint over 200 characters (the last section). ## What changes - **One remedy line per finding.** The 17 ids, every hint the lint suite fires, before → after (count of distinct hints · length range): | rule id | file | lint suite, before → after | runtime door | |---|---|---|---| | `approval-approver-not-membership-tier` | `validate-approval-approvers.ts` | 2 · 297–313 → 3 · 130–168 | `flow` write, 2xx advisory | | `approval-expression-no-empty-policy` | same | 1 · 294 → 1 · 121 | `flow` write, 2xx advisory | | `approval-expression-invalid` | same | 5 · 102–292 → 5 · 90–144 | `flow` write, 422 (the `resolveAs` arm a 2xx advisory) | | `dataset-include-unknown` | `validate-dataset-references.ts` | 4 · 188–336 → 2 · 94–97 | `dataset` write, 422 | | `dataset-field-unknown` | same | 9 · 164–206 → 6 · 131–139 | `dataset` write, 422 | | `title-format-retired` | `validate-record-title.ts` | 1 · 267 → 1 · 148 | CLI only | | `title-unresolvable` | same | 1 · 237 → 1 · 122 | CLI only | | `list-view-field-unknown` | `validate-list-view-field-refs.ts` | 4 · 94–258 → 4 · 57–62 | `view` / `object` write, 422 | | `list-view-field-dotted` | same | 4 · 145–233 → 4 · 97–158 | `view` / `object` write, 422 | | `translation-target-unknown` | `validate-translation-references.ts` | 85 · 45–317 → 30 · 45–164 | CLI only | | `seed-insert-mode-duplicates-on-replay` | `validate-seed-replay-safety.ts` | 1 · 309 → 1 · 131 | CLI only | | `chart-measure-unknown` | `validate-chart-bindings.ts` | 9 · 91–285 → 8 · 78–115 | `report` write, 422 (presentation positions a 2xx advisory) | | `object-field-ref-unknown` | `validate-object-field-refs.ts` | 25 · 155–284 → 8 · 116–186 | `object` write, 422 | | `view-container-shape` | `validate-view-containers.ts` | 3 · 158–256 → 3 · 158–187 | CLI only | | `seed-value-outside-state-machine` | `validate-seed-state-machine.ts` | 4 · 247–251 → 5 · 135–139 | CLI only | | `ai-skill-surface-mismatch` | `validate-ai-surface-affinity.ts` | 6 · 218–235 → 7 · 126–171 | CLI only | | `page-source-className-tailwind` | `validate-page-source-styling.ts` | 2 · 195–231 → 2 · 127–167 | CLI only | A count that falls is a list that no longer makes each hint distinct; a count that rises is a case this slice added (a long position, object, skill or page name fired to read the bound on it). - **Lists are no longer echoed** (slices 11, 12 and 14's rule: a list the verdict's "Did you mean" already covers). - `dataset-include-unknown`, `dataset-field-unknown`, `list-view-field-unknown` and `object-field-ref-unknown` no longer append the object's whole field list (`account.detail`); `chart-measure-unknown` no longer lists the dataset's measures, and keeps its own "Did you mean" after the remedy. - `translation-target-unknown` no longer lists the declared names on any leg. Two facts a list used to carry stay as one bounded clause: a stale `_views` key is told that a nameless default `list` is keyed `default` (the #6038 steer the declared-view list gave a `_views.list` key), and a `_tabs` key over an object with no preset bar is told to declare the tab in a page's `interfaceConfig.userFilters.tabs`, because a list view's own `tabs` has no renderer (the mis-fix the old tail guarded). - **The reasoning moves, the remedy stays.** - The approval ids: what the three expression roots hold, why `record` and `previous` are unbound at an approval node, and what `resolveAs` does are the new `approval-expression-invalid` entry; what each `onEmptyApprovers` policy does is the new `approval-expression-no-empty-policy` entry (the nudge still names all four policies); the membership-tier line names the value once and points at the tiers the verdict lists, and why a business role is always a position was already the entry's. - The dataset ids: the three-hop limit, which field types join, that `a.b` includes `a`, and that a join is derived from the object graph (no ON clause) are the entries'. - The title ids gain entries: why `titleFormat` is retired (render-only, neither returned nor queryable, `nameField` wins) and how a record title resolves, with its `Record #ID` fallback. - `list-view-field-dotted` keeps both doors' remedies in their own words ("denormalise the value onto … and name that"); what a denormalised field is moves to the entry, in a paragraph naming each door's ways out. - `chart-measure-unknown`: what `series[].name` and `yAxis[]` carry at a presentation position, and that none of these surfaces has a `suppressWarnings` key, are the entry's. - The seed ids: the `externalId` examples and that the seed exemption never licenses an undeclared state are the entries'. - `view-container-shape`: that machine-assembled manifests carry a ViewItem under `viewItems:` was already the entry's. - `ai-skill-surface-mismatch`: when `surface: 'both'` is right is the entry's. - `page-source-className-tailwind` gains an entry: why a Tailwind class in page source produces no CSS, the theme-token examples and the drawer/modal overlay advice. - **Five ids gain an `os explain` entry**, so their `rule:` line now carries the pointer: `approval-expression-invalid`, `approval-expression-no-empty-policy`, `title-format-retired`, `title-unresolvable` and `page-source-className-tailwind`. Each already had a rule id constant on the root barrel, so no published name is added (the closure reading below). Ten existing entries gain text. Read from the built tables, none removed: 146 → 151 against the base `d206d08ca`, 145 → 150 against `main` at `e540e7ffb` (which #23154 left one entry shorter). - **Enumerations held to their sources.** An entry that spells a list out is pinned to it by the rule's test: the empty-slate policies (and the nudge's) to `ApprovalNodeConfigSchema`'s `onEmptyApprovers` enum, the expression roots to `APPROVER_EXPRESSION_ROOTS`, and the title-eligible types to `TITLE_ELIGIBLE_TYPES`. - **Nothing else moves.** - Every `message`, rule id, severity and `path` is unchanged, and so is what each rule accepts or refuses. - Every hunk in the twelve rule files is a `hint` expression, a comment, or the removal of the now-unused `SHAPE_HINT` table in `validate-chart-bindings.ts`. Two hint expressions choose between two remedy texts on a fact the rule already read: the `_tabs` leg on whether the object has a preset bar (it chose between two list tails before), and `checkFieldPath` keeps the field list for `dataset-filter-field-unknown`, outside this slice. - `.changeset/22161-lint-slice-16-one-line-fix.md`: `@objectstack/lint` `patch`, `Clause-②: no`, with the runtime `hint` FROM → TO for every id that fires at the runtime gate. ### File surface: the claim's The claim (`6137662991`) names the twelve rule files, `rule-explanations.ts`, the shared one-line producers where an id converges on them, a text-only test rider in `packages/cli` / `packages/metadata-protocol`, and the changeset. - The edits stay inside it: the twelve rule files, their twelve test files, `rule-explanations.ts` and the changeset. - `system-fields.ts` and `checked-parse.ts` needed no edit: none of the 17 is an anchor or a parse id. - No `packages/cli` or `packages/metadata-protocol` rider was needed: no test there reads these hints' text, and both suites pass unchanged on the branch (below). ## The runtime wire Measured at the door with `runRuntimeAuthoringRules`, on the base dist and on the rebuilt one: a scratch probe of 20 writes across `flow`, `dataset`, `object`, `view`, `report`, `seed`, `page` and `agent`. - a `flow` write with an approval node: - `approval-approver-not-membership-tier` (2xx `advisories[].hint`): 313 → 138; - `approval-expression-invalid` (422 `issues[].hint`): an empty expression 216 → 90, a `record` root 292 → 144; a parse failure (129), a bare root (110) and the `resolveAs` advisory (102) unchanged; - `approval-expression-no-empty-policy` (2xx advisory): 294 → 121. - a `dataset` write: `dataset-include-unknown` (422), an entry naming nothing / an ordinary field, 239 / 187 → 93 / 93; `dataset-field-unknown` (422), a dimension / a measure, 184 / 182 → 132 / 130. - an `object` write: `object-field-ref-unknown` (422) 180 → 128 — the same text is the `fix` of the publish receipt's `object_field_ref_unknown` runtime probe issue (`build-probes.ts`); on its `listViews`, `list-view-field-unknown` (422) 110 → 57 and `list-view-field-dotted` (422) 231 → 157. - a `view` write: `list-view-field-unknown` (422) 110 → 57; `list-view-field-dotted` (422), a projection / a filter, 231 / 143 → 157 / 95. - a `report` write: `chart-measure-unknown`, 422 at the query position 117 → 106, 2xx advisory at a presentation position 301 → 113. - No write reaches the other eight: the two title ids, both seed ids, `view-container-shape` and `page-source-className-tailwind` are `CLI_ONLY` in the registry, and `translation-target-unknown` and `ai-skill-surface-mismatch` are reference-suite members on the frozen `flow` default (a flow snapshot carries neither translations nor skills). Readers of `hint`, read-only against objectui `main` at `fd4872f9` (fetched into a ref of this session's own, `git grep` for `.hint` reads): - Studio's `saveAdvisoryToast.ts` `formatFinding` renders the rule, where, message and hint for every 2xx advisory, so the three approval advisories and the chart presentation advisory shorten there. - `DraftChangesPanel.tsx` reads `hint` only for its in-browser `validateSecurityPosture` pass (none of these ids). - No objectui source reads a 422 issue's `hint`. - REST (`PUT /api/v1/meta`) forwards `issues` / `advisories` as JSON (`toIssue` copies `hint` verbatim). `packages/mcp` registers no metadata write tool (its tools are the record, action, run and expression tools), so an MCP or AI author reaches the gate through REST. - **No reader in either repository matches on `hint` text.** ## Census (taken first, before any edit, at the base `d206d08ca`) Method: the earlier slices' scratch preload (`NODE_OPTIONS=--import`, never committed), which records every finding-shaped object, deduped by (rule, hint) per process, with its push site. Lengths are the `hint` alone. - **`packages/lint` suite**: the 17 ids re-measured at the base equal the claim's lengths exactly (313, 294, 292, 336, 206, 267, 237, 258, 233, 317, 309, 285, 284, 256, 251, 235, 231). - Base: 240 ids fire, 65 over 200 = 21 fenced + 3 repo-gate + 41 author-time. The 41 are slice 15's remaining list, id for id. - After, at `0a66efe55` (before the merges): 240 ids fire, 48 over 200 = 21 fenced + 3 repo-gate + 24 author-time, and every hint of the 17 is at most 200 (the table). The 24 are the 41 less these 17. - At the final head `abe4ce231` (after both merges, rebuilt): 239 ids fire (#23154 retired one), 48 over 200, the same 21 + 3 + 24, and the 17 ids' lengths equal the pre-merge census id for id. `main` at `e540e7ffb`, built in the base worktree, fires the 17 at exactly the base lengths. - **Runtime publish gate**, the whole `@objectstack/metadata-protocol` suite (which reads `@objectstack/lint` from source), in a base worktree and at the branch: `approval-expression-no-empty-policy` 294 → 121, `dataset-field-unknown` 188 → 136, `list-view-field-unknown` 102 → 58, `object-field-ref-unknown` 171–202 → 128–161; `approval-expression-invalid` fires there only on its parse arm (129, unchanged); none of the other 12 fires there. - **`packages/cli` unit tier**, base worktree and branch: `title-unresolvable` 237 → 122; `view-container-shape` fires on its flat arm only (187, unchanged); none of the other 15 fires there. - **`os validate`** (the built CLI) on `app-crm`, `app-todo`, `app-showcase` and `app-multi-package`, base worktree `d206d08ca` against the branch at `0a66efe55`, and again `main` `e540e7ffb` against the final head `abe4ce231`, with the same result both times: - exit 0 each, and each prints the same 9 / 7 / 70 / 3 `rule:` lines; - `app-crm`, `app-todo` and `app-multi-package` are byte-identical once the worktree path and the millisecond timings are normalised; - `app-showcase` differs in exactly two lines, both of `title-unresolvable` on `showcase_project_membership`: its `fix:` line (237 → 122 characters of hint) and its `rule:` line, which gains the pointer to `os explain title-unresolvable`. Only this id of the 17 fires on the four apps. ## Tests Every heavy run went through `scripts/pm/os-verify-lock.sh` (slots `issue-22161-s16`, `issue-22161-s16-base`). Its VERDICT lines are in the report. - **Each rule's own suite pins the new shape.** Each of the twelve test files gains a last block. It holds every recorded `fix:` line of its converted ids to one line of at most 200 characters, behind a coverage control naming each arm (every message arm of the three approval ids; each include, dimension, measure and joined-path arm of the dataset ids; both title ids on a long object name; the unknown and dotted list-view ids at both doors; all 27 hint arms of `translation-target-unknown`; every chart position, with and without a near miss; every object-field position; all four view-container arms; each page kind on a long page name). Exact pins read each new line, and a check per id asserts that `explainRule(id)` holds what the line stopped saying. - **Moved pins.** Pins that read a list out of a hint moved, text-only, to the exact new line or to the verdict's "Did you mean": - the translation suite's list-reading pins (30), its `enumeratedUniverse` read-back (now the verdict's suggestion, which only a collector that reached the name can offer) and the per-app contributed-id pin (now a silent control: the contributed id resolves under its own app); - the list-view, object-field and chart pins on the field or measure list; - the approval nudge's four-policy pin and the seed pins still read the line. - **Lint suite**: at the base `d206d08ca` Test Files 138 passed (138), Tests 6,654 passed (6,654); at `0a66efe55` 138 / 6,688; after the first merge (`04b1eb95b`) 138 / 6,688; at the final head `abe4ce231` Test Files 137 passed (137), Tests 6,654 passed (6,654), against `main` `e540e7ffb`'s 137 / 6,620 (the 34 added cases, on a `main` #23154 trimmed by one file). - **Lint build and typecheck**: `pnpm turbo run build` over the CLI, metadata-protocol and example closures at `0a66efe55` (lint's build script runs `check-dts-emitted`) and `pnpm --filter @objectstack/lint run typecheck` (`tsc --noEmit` + `check:test-typecheck` OK: 2 files / 6 errors / 2 pinned signatures held), VERDICT command-exit 0 each; again at the final head `abe4ce231`, after `pnpm install --frozen-lockfile` and a rebuild of the CLI, metadata-protocol and example closures, VERDICT command-exit 0 each. - **The built entries' export closure is unchanged** (the `Clause-②` check): the runtime export keys and the declaration exports of `index`, `runtime` and `rule-explanations`, read from the base dist (`d206d08ca`) and the branch's rebuilt dist (`0a66efe55`): 371 / 553, 6 / 11 and 2 / 3 names, 0 added and 0 removed. Read again at the final head `abe4ce231` against a `main` dist built at `e540e7ffb`: 369 / 550, 6 / 11 and 2 / 3 names (#23154 retired two), 0 added and 0 removed. - **CLI unit tier** and **metadata-protocol suite**, base worktree `d206d08ca` against the branch at `0a66efe55`, VERDICT command-exit 0 for all four runs. CLI unit tier: Test Files 285 passed (285), Tests 4,283 passed (4,283), in both. metadata-protocol: Test Files 230 passed, 3 skipped (233), Tests 28,244 passed, 19 skipped, in both. Rerun at the final head `abe4ce231`, because the second merge brought `packages/lint` changes: CLI unit tier 285 / 4,283, metadata-protocol Test Files 231 passed, 3 skipped (234), Tests 28,253 passed, 19 skipped, VERDICT command-exit 0 both, and the same per-id census as at `0a66efe55`. - **Ablation**: one-shot, from the committed head `515b093fd`, through `scripts/ablation-replace.mjs` under the lock, with a shell `trap` restoring `HEAD` by absolute path. The rule suite imports the rule source, so there is no dist leg. - Mutation: `object-field-ref-unknown`'s pre-slice hint restored in `judge()` (the prescription followed by `account.detail`). - Predicted before the run: exactly three cases in `validate-object-field-refs.test.ts` go red (the `highlightFields` exact-line pin, the `relatedListColumns` no-field-list pin and the new bound case), and every other case stays green. - Observed: Tests 3 failed, 62 passed (65), exactly those three. - The mutation landed (anchor x1 → x0, replacement x0 → x1, blob `ed7515826362` → `f3add49e4bd7`) and was restored (blob `ed7515826362` == HEAD, `git diff HEAD` empty, `git status --porcelain` empty). - **ESLint, narrowed**: `npx eslint --no-inline-config --format json` over the 25 changed `.ts` files: 25 files, 0 errors, 0 warnings. The population is `eslint.config.mjs`'s `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` block plus its `packages/**` blocks; the config never enables type-aware linting (no `parserOptions.project`), so no untouched file's verdict can move. A control-character scan of every changed file found no match. - **Gates**: two merges of `origin/main`, each through `scripts/pm/os-regen-merge.sh`, both without conflict; the branch edits no generated artifact. - `515b093fd` → `04b1eb95b`, onto `dc93781c1`: `main` brought `packages/spec`, `plugin-audit` and `service-messaging` changes and no `packages/lint`, `packages/cli` or `packages/metadata-protocol` file. - `04b1eb95b` → `abe4ce231`, onto `e540e7ffb`: #23154 (#19939 S6) had landed, editing `rule-explanations.ts` (one flow-filter entry and its map row removed) and other `packages/lint` files, so every measurement above was retaken at `abe4ce231`. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, with no paths, at `abe4ce231` (26 paths vs the merge base `e540e7ffb`) derived 62 commands, the same 62 at every head. - All 62 ran at `abe4ce231` but the one skipped by order, each exit code captured before any pipe, all 0. The longest: `check:query-options-erasure` 332s, `check:slot-lookup` 182s, `check-comment-mask-corpus` 135s. At `04b1eb95b` the same set ran all-0 as well, `check:dual-build-cjs-loads` there after a first refusal (exit 3, PREREQUISITE NOT MET: six packages had no dist) and a locked build of those six (VERDICT command-exit 0). - NOT MEASURED, by the dispatch's order: `check:type-check-debt`. - `--ran`: `62 derived famil(ies) accounted for — 61 run, 1 NOT-MEASURED`. - `origin/main` moved 11 commits after the second merge (`e540e7ffb` … `d4aaeaf0e`), touching `packages/lint` files outside this slice (`validate-component-props.ts`, `validate-page-field-bindings.ts`, `authoring-rules.ts`) and none of its files or `rule-explanations.ts`; not merged again, and a local `git merge-tree` onto `d4aaeaf0e` is clean. - The artifact-roster and PR-context families are CI's. ## Acceptance notes - **A long name can still lengthen a line.** The bound holds on every variant the suites, the runtime probe and the four example apps fire, long names included. The names in a hint are the author's: the approver value, the object, the dataset and measure, the skill, the head segment, the translation's owning app. The closest measured case is `view-container-shape`'s flat arm at 187 (fixed text, unchanged), then `object-field-ref-unknown` at 186 (the `relatedListColumns` prescription, fixed text). - **Some hints lose a clue a short list gave.** Where no near miss exists (a `field_10` in `highlightFields`, `estimate_hours` against measures named `est_hours`), the old line listed what exists and the new one names the remedy only. That is slices 11, 12 and 14's rule for a list the verdict's "Did you mean" covers; the two cases where the list carried the one fact an author needed (`_views.default`, the dead list-view `tabs` carrier) keep that fact as a bounded clause. - **`dataset-filter-field-unknown` keeps its field list.** It shares `checkFieldPath` with `dataset-field-unknown`, and it is not among the claimed ids (its hint stays at most 200 on every variant the suite fires), so `checkFieldPath` still appends the list for it. A later slice can drop it with one condition. - **`docs/adr/0079-record-display-name.md` quotes the old `title-format-retired` hint** ("titleFormat is a render-only template the server cannot return or query") as what `validate-record-title.ts` states. The file's module note still states that consequence, and the explanation now carries it; the ADR is a governed surface outside the claim, and the quotation records the implementation at decision time. Noted, not edited. - **Open PRs touching these files, read at the time of opening:** - #23154 (#19939 S6) landed during this slice and is merged in (above). - #23160 (#23082, draft, opened after the claim's census) edits one fixture of `validate-list-view-field-refs.test.ts` (a skip case's provider), away from this slice's hunks. ## Remaining for this card **24 author-time `packages/lint` ids still carry a hint over 200 characters** in the final lint census (at `abe4ce231`: 48 over 200 = 24 + 21 fenced + 3 repo-gate), by file, longest first: - `lint-flow-patterns.ts` (2): `flow-double-brace-interpolation` 1413, `flow-bare-dollar-reference` 1234 - `data-model-rules.ts` (6): `unique/legacy-organization-composite` 578, `unique/unscoped-declared-index` 380, `object/missing-name-field` 327, `rollup/non-numeric-aggregand` 282, `unique/double-declaration` 279, `field/select-missing-options` 207 - `validate-widget-bindings.ts` (11): `dashboard-filter-field-unprovisioned` 561, `chart-field-unknown` 415, `dashboard-filter-field-unknown` 403, `chart-measures-missing` 369, `widget-legacy-analytics-shape` 338, `widget-measures-missing` 330, `widget-filter-field-unknown` 322, `widget-legacy-analytics-unrenderable` 320, `table-count-only` 311, `chart-dimensions-missing` 291, `dashboard-filter-field-not-included` 218 - `validate-capability-references.ts` (1): `capability-reference-unknown` 273 - `lint-liveness-properties.ts` (1): `liveness-planned-property` 230 - `validate-page-field-bindings.ts` (2): `page-section-count-not-multi-value` 228, `page-field-unknown` 206 - `validate-list-view-mode.ts` (1): `list-view-filters-in-views-mode` 219 Not in the 24: the 26 fenced ids as PR #22448 lists them (21 fire a hint over 200), the 3 repo-gate ids (`startup-open-vocabulary-verdict`, `startup-source-unparseable`, `startup-verdict-assertive-wording`), and the `packages/cli` ids on #22841. --- _Generated by [Claude Code](https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8323cad commit 9fbabd7

26 files changed

Lines changed: 1016 additions & 253 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): 17 more author-time rule ids print a `fix:` line of one remedy sentence, and `os explain RULE_ID` carries what those lines used to explain
6+
7+
Clause-②: no
8+
9+
- **Shorter fix lines.** Each finding of these 17 rule ids now carries a `hint` (the CLI's `fix:` line, the runtime issue's `hint`) of one remedy sentence of at most 200 characters on every variant the lint suite fires (the lengths here are the `hint` alone; the CLI prints it after `fix: `). The longest of each, before → after:
10+
- approval nodes: `approval-approver-not-membership-tier` (313 → 168), `approval-expression-no-empty-policy` (294 → 121) and `approval-expression-invalid` (292 → 144);
11+
- datasets: `dataset-include-unknown` (336 → 97) and `dataset-field-unknown` (206 → 139);
12+
- record titles: `title-format-retired` (267 → 148) and `title-unresolvable` (237 → 122);
13+
- list views: `list-view-field-unknown` (258 → 62) and `list-view-field-dotted` (233 → 158);
14+
- translations: `translation-target-unknown` (317 → 164);
15+
- seeds: `seed-insert-mode-duplicates-on-replay` (309 → 131) and `seed-value-outside-state-machine` (251 → 139);
16+
- charts: `chart-measure-unknown` (285 → 115);
17+
- object field-name lists: `object-field-ref-unknown` (284 → 186);
18+
- stack views: `view-container-shape` (256 → 187);
19+
- agents: `ai-skill-surface-mismatch` (235 → 171);
20+
- source-tier pages: `page-source-className-tailwind` (231 → 167).
21+
- **Lists are no longer echoed.** `dataset-include-unknown`, `dataset-field-unknown`, `list-view-field-unknown` and `object-field-ref-unknown` no longer append the object's whole field list, `chart-measure-unknown` no longer lists the dataset's measures, and `translation-target-unknown` no longer lists the declared fields, views, sections, tabs, rules, actions, apps, navigation ids, dashboards, widgets, header actions, flows, screens, screen fields, refusing end nodes, params, outcomes or result paths: the verdict's "Did you mean" (on `chart-measure-unknown`, the fix line's own) names the nearest one. Two facts a list used to carry stay on the line as one bounded clause: a stale `_views` key is told that a nameless default `list` is keyed `default`, and a `_tabs` key over an object with no preset bar is told to declare the tab in a page's `interfaceConfig.userFilters.tabs`, because a list view's own `tabs` has no renderer. `dataset-filter-field-unknown`, outside this change, keeps its field list.
22+
- **One line, one remedy.** Where a fix line explained its remedy, the explanation moved: what an expression approver's three roots hold, why `record` and `previous` are unbound at an approval node, and what `resolveAs` does; what each `onEmptyApprovers` policy does; that a business role is always a position; that a dataset join is derived from the object graph, up to three hops, with `a.b` including `a`; why `titleFormat` is retired and how a record title resolves, with its fallback; what a denormalised field is; which presentation positions a chart measure lands on, and that none carries a `suppressWarnings` key; the composite `externalId` of a junction table; that the seed exemption never licenses an undeclared state; when a skill may declare `surface: 'both'`; and why a Tailwind class in page source produces no CSS, with the theme-token and overlay advice.
23+
- **`os explain RULE_ID` carries the rest.** What the fix lines stopped saying is each id's `RULE_EXPLANATIONS` entry in `@objectstack/lint`, which `os explain RULE_ID` prints. Five ids gain an entry, so their `rule:` line now carries the `os explain` pointer: `approval-expression-invalid`, `approval-expression-no-empty-policy`, `title-format-retired`, `title-unresolvable` and `page-source-className-tailwind`. Ten existing entries gain text. The enumerations an entry spells out are held to their sources by the rules' tests: the empty-slate policies to the spec's `onEmptyApprovers` enum, the expression roots to `APPROVER_EXPRESSION_ROOTS`, and the title-eligible types to `TITLE_ELIGIBLE_TYPES`.
24+
- **At the runtime publish gate** (Studio, REST `PUT /api/v1/meta`, and the MCP and AI authors that write through that door), 9 of the 17 fire; the `hint` FROM → TO, measured at the door:
25+
- a `flow` write with an approval node: `approval-approver-not-membership-tier` (2xx `advisories[].hint`, 313 → 138), `approval-expression-invalid` (422 `issues[].hint`: an empty expression 216 → 90, a `record` root 292 → 144; a parse failure 129, a bare root 110 and the `resolveAs` advisory 102 unchanged) and `approval-expression-no-empty-policy` (2xx advisory, 294 → 121);
26+
- a `dataset` write: `dataset-include-unknown` (422, an entry naming nothing / an ordinary field 239 / 187 → 93 / 93) and `dataset-field-unknown` (422, a dimension / a measure 184 / 182 → 132 / 130);
27+
- an `object` write: `object-field-ref-unknown` (422, 180 → 128; the same text is the `fix` of the publish receipt's `object_field_ref_unknown` runtime probe issue), and on its `listViews`, `list-view-field-unknown` (422, 110 → 57) and `list-view-field-dotted` (422, 231 → 157);
28+
- a `view` write: `list-view-field-unknown` (422, 110 → 57) and `list-view-field-dotted` (422, a projection / a filter 231 / 143 → 157 / 95);
29+
- a `report` write: `chart-measure-unknown` (422 at the query position, 117 → 106; 2xx advisory at a presentation position, 301 → 113).
30+
31+
Studio's save-advisory toast prints an advisory's `hint` after its message, so the three approval advisories and the chart presentation advisory shorten there; Studio renders no 422 issue's `hint`. No Studio, REST or MCP reader in this repository or in objectui matches on `hint` text; a tool that did should match on `rule` and `path` instead.
32+
- **Never at the runtime gate:** `title-format-retired`, `title-unresolvable`, `translation-target-unknown`, both seed ids, `view-container-shape`, `ai-skill-surface-mismatch` and `page-source-className-tailwind` run on the CLI only.
33+
- **Unchanged:** every `message` (the verdict), rule id, severity and `path`, and what each rule accepts or refuses.

0 commit comments

Comments
 (0)