Repository navigation
Commit 9fbabd7
fix(lint): one-line fix lines for 17 more author-time rule ids (slice 16);
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>os explain RULE_ID carries what they explained (#23194)1 parent 8323cad commit 9fbabd7
26 files changed
Lines changed: 1016 additions & 253 deletions
File tree
- .changeset
- packages/lint/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
0 commit comments