Skip to content

Commit 9c74d40

Browse files
fix(lint): one-line fix lines for the last eight fenced author-time rule ids (slice 21); os explain carries what they explained (#24292)
Part of #22161 Clause-②: no Slice 21 of the card. It covers the other 8 author-time `packages/lint` ids that slice 2 fenced. Those ids are inside the card's close condition (triage answer A, comment `6219576699`), and this slice uses the shape of slices 11–20. Each finding's `hint` for these 8 ids (the CLI's `fix:` line, and the runtime finding's `hint`) is now one remedy line of at most 200 characters. What the old lines explained is now in each id's new `RULE_EXPLANATIONS` entry, which `os explain RULE_ID` prints. The card stays open. The last section lists what is left. Claim: `6247309763`. It is the newest `Claim:` on the card and names this branch; I checked that before any edit. This run resumes from `claude/issue-22161-s21-validate-hints@c4eca88`, which a dead earlier run pushed with no PR and no report. It never started over from zero. I reviewed that head's three commits (`fe9bb145c` fix lines and entries, `8160ab7da` a `main` merge, `c4eca88ab` changeset) hunk by hunk and kept them, with the corrections listed under "On top of the inherited head". Every reading below was taken again in this run. ## What changes **One remedy line per finding.** The table lists every hint the lint suite fires for the eight ids, before and after, as distinct hints · length range. The instrument is a census preload over the whole lint suite: `Array.prototype` push / unshift / map / filter / forEach / find / some / every / flatMap / concat / slice hooks that record every finding-shaped object, with its `hint`, `fix`, `message`, `where`, `path` and `severity`. "Before" is this branch's tests run against `main`'s rule sources, which are `93b4acb57` blobs written into the tree and restored under a trap (method below). "After" is this branch at its final head. The runtime column comes from a scratch probe of the runtime gate (below), FROM → TO. | rule id | lint suite, before → after | runtime door (`runRuntimeAuthoringRules`) | |---|---|---| | `flow-template-field-unprovisioned` | 1 · 362 → 1 · 175 | `flow` write, 2xx advisory, 362 → 175 | | `flow-template-lookup-traversal` | 5 · 251–316 → 5 · 125–139 | `flow` write, 2xx advisory, 249 → 131 (trigger root), 314 → 126 (flow variable) | | `page-field-unprovisioned` | 1 · 362 → 1 · 175 | does not fire on a `page` write (a reference-integrity member on the frozen `flow` default) | | `page-section-group-unknown` | 4 · 188–279 → 4 · 118–151 | does not fire on a `page` write (same) | | `print-page-block-unprintable` | 1 · 362 → 1 · 170 | `page` write, 422 issue, 362 → 170 | | `form-section-group-unknown` | 5 · 178–275 → 5 · 104–147 | CLI only (`surfaces: CLI_ONLY`) | | `filter-token-unknown` | 5 · 132–259 → 5 · 132–170 | CLI only (`surfaces: CLI_ONLY`) | | `component-props-invalid` | 33 · 164–258 → 33 · 157–188 | `page` write, 422 issue, value line 248 → 157 (`page:tabs`), 250 → 159 (`object-grid`), 253 → 162 (`record:details`), 258 → 167 (`record:related_list`) | **Measured on `main` first, as the order asked.** I ran `main` itself at `93b4acb57`, with its own rule sources and its own tests. All eight ids fire over 200 there, at the lengths the seat's record `6217435246` lists: 362, 316, 362, 279, 362, 275, 259 and 258. On `main` the census finds 11 ids over 200: these eight, plus the 3 repo-gate ids outside the condition. Some lines did not change. `filter-token-unknown`'s four "Did you mean" lines (132–161) were one remedy line already. So were `component-props-invalid`'s retired-key line (164–181) and its node-binding line (188). Only the value-rejection line (248–258 → 157–167) moves. **What each line says now.** - The two anchor ids print the shared one-line anchor fix (`unprovisionedAnchorFix()`) that the other unprovisioned-anchor ids already print. - The lookup-traversal line names the start node's `config.expand` opt-in on the trigger root, and never on a variable root. - The two section-group ids name the nearest declared group ("Did you mean") instead of listing every group the object declares. For an object with no groups, the line is: declare a group, or enumerate the section's `fields`. - The print line points at `os explain print-page-block-unprintable` for the printable list. This finding is also refused at the save door, where no `rule:` line carries the pointer. - The filter line names the two vocabularies briefly. - The props line asks for a value the props schema accepts, or for a schema fix. **`os explain` carries the rest.** Each of the eight ids gains a `RULE_EXPLANATIONS` entry, so its `rule:` line now carries the `os explain` pointer. What the lines stopped saying is there: - how to vouch for an unprovisioned anchor through `columnMap`, or opt the object out (`ownership: 'none'`, `systemFields: { audit: false }`), shared with the earlier anchor entries; - what `config.expand` does: a one-hop re-read of the triggering record as the run's identity. It covers the trigger record alone; - how a section inherits a field group's members, and that membership is declared on the field; - the printable block list, held equal to the spec's `PRINTABLE_PAGE_COMPONENT_TYPES` by the rule's test, and the three reasons a block is refused; - the context tokens (held equal to `CONTEXT_TOKENS` by the rule's test) and the date macros. It also says where `{record_id}` resolves, and that the runtime refuses an unresolvable placeholder; - where a page component's props schema lives, why the page, not the schema, is almost always what to fix, and the retired-key and node-binding cases. I checked every fact in the eight entries against the rule source and its docblock, and against the spec where they cite it: `date-macros.zod.ts`, `PRINTABLE_PAGE_COMPONENT_TYPES` and `ComponentPropsMap`. I also checked the runtime they describe: the automation engine's `expandDeclaredLookups` for `config.expand`, and `@objectstack/core`'s `UnknownFilterTokenError` for the 400. Why `Clause-②: no` holds: - the eight rule-id constants were already root-barrel exports; - the new entry constants are module-local; - `RULE_EXPLANATIONS` (already published) gains eight keys. So no published name is added. The one removed function, `unprovisionedAnchorHint`, was module-exported from `system-fields.ts` but is reached by no package entry. `node scripts/check-platform-checklist.mjs --changed-from origin/main` reads OK (1 anchor judged, 0 lost). **Nothing else moves.** Every `message`, rule id, severity and `path` is unchanged, and so is what each rule accepts or refuses. The census proves this mechanically: it records the (`message`, `where`, `path`, `severity`) tuple of every finding. For each of the eight ids, the set of those tuples is identical with `main`'s sources and with this branch's: 1, 8, 8, 4, 55, 6, 27 and 123 tuples, 0 differing. Every other id's rows are identical too: 4067 rows, run under the same instruments, with one extra row explained in the Verification section. ### The two files the claim named conditionally - **`packages/lint/src/object-field-groups.ts`: kept.** `checkSectionGroupRefs()` builds the `hint` of both section-group ids. `validate-form-layout.ts` and `validate-page-field-bindings.ts` call it with their rule id, so the one-line form lives there. That is why `validate-form-layout.ts` itself has no diff. - **`packages/lint/src/system-fields.ts`: kept.** `unprovisionedAnchorFix()`, which builds both anchor ids' hint, lives there. The edit deletes `unprovisionedAnchorHint()`, the long form those two ids called. They were its last two callers. The edit also rewrites the shared fix's docblock. ## On top of the inherited head - **`main` merged first** (`1e1554e15`, clean). Slice 20's `rule-explanations.ts` entries (`b6805ac0f`) merged without a conflict, and all 20 entries are present: the twelve `lint-flow-patterns.ts` ids plus these eight. - **Two entry sentences made exact.** - The form entry named only `data.object` as a form's own binding. The rule reads `objectName`, `object` or `data.object` (`viewObjectName`). - The page entry skipped the binding a nested component inherits from its container (`walkPageComponents`). - **A docblock that overclaimed.** `unprovisionedAnchorFix()`'s docblock said every unprovisioned-anchor rule in the package prints that wording. Two (`validate-expressions.ts`, `validate-semantic-roles.ts`) still word their own fix, as the cause docblock beside it already notes. It now says so. - **The changeset carries this run's numbers.** The retired-key range is 164–181 on the merged tree; it was 164–180. The flow lookup-traversal door readings are this run's probe: 249 → 131 and 314 → 126. ### File surface: the claim's The diff touches six rule files, but only five have a diff. `validate-form-layout.ts` has none, because its hint is built in `object-field-groups.ts`. The rest of the diff: - each rule file's test; - `rule-explanations.ts`, entries for these eight ids only; - `object-field-groups.ts` and `system-fields.ts`, as explained above; - `.changeset/22161-s21-validate-fix-lines.md`. It does not touch `lint-flow-patterns.ts`, `lint-startup-registry-verdict.ts` or `content/docs/**`. 15 files, +674 / −76. ## Verification Final head `49e48cd21`. Heavy runs went through `scripts/pm/os-verify-lock.sh` (slot `issue-22161-s21`), and each reading below is its `VERDICT` line. Every exit code was captured before any pipe. - **Lint suite** at `49e48cd21`: 143 files / 6847 passed, VERDICT 0. The census run is the same run. - **The `main` leg.** - Method: `main`'s `93b4acb57` blobs of the 14 changed lint files were written into the tree only. Their landing was proved by blob hash. They were restored from `HEAD` by absolute path under an EXIT/INT/TERM trap, and the restore was proved by blob hash, an empty `git diff HEAD` and a clean status. - Result: 143 files / 6820 passed | 5 skipped, VERDICT 0. The 5 skips are the dist-dependent `lazy-deps` / `runtime-lazy-deps` cases. The lint dist did not exist yet, and it does now. - **Reverse verification.** Only `main`'s 8 rule sources were swapped in (same method), with this branch's tests: 6 files / 70 tests red, exactly this slice's six test files. The pins fail on the old text, so they can fail. Restore proved the same way. This one leg is also the census "before" column above. - **Lint build and typecheck** at `49e48cd21`, VERDICT 0 each. - Build: `tsup` plus `check-dts-emitted`, 6/6 declaration files. - Typecheck: `tsc` plus `check:test-typecheck` OK (2 files / 6 errors / 2 pinned, unchanged). - `tsc -p tsconfig.test.json --listFiles` lists all six changed test files. - **Spec `repo` project** (`--project repo`, whose cross-package pins read `packages/lint/src`): - The 11 files that read `packages/lint`: 11 / 201 passed, VERDICT 0. - The whole project: 68 files / 1125 passed, VERDICT 0 (the lock held 15m30s on a shared box). - **Runtime probe.** 9 writes through `runRuntimeAuthoringRules` with `tsx`: 6 `page` and 3 `flow`. It ran with `main`'s 8 rule sources swapped in (same method), then on this branch. Results are in the table above. No write changes channel. Each `page` case ran 4 rules and each `flow` case 9 rules, on both sides. - **The one extra census row.** At the final head, one `validation-rule-json-schema-uncompilable` row appears that the earlier legs did not record. It comes from the five dist-dependent cases, which ran once the lint dist existed. It is not one of the eight ids. - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, no paths, at `49e48cd21`: 64 families over 15 paths since the merge base `93b4acb57`. Each family was run one by one and its exit code recorded. 62 exit 0. 2 exit 3 (`PREREQUISITE NOT MET`) and are NOT MEASURED: `check:dual-build-cjs-loads` needs every package built, and `check:lean-entry-closure` needs `packages/objectql/dist`. `--ran` reports 64 derived, 62 run, 2 NOT-MEASURED, 0 UNRUN. This diff adds no import edge, so CI's full build gives those two their reading. For every family CI runs, this PR's own check-runs are the reading. `check:dev-prereqs` and `check:skill-frame-freshness` were not run: the diff touches neither a `packages/spec` build input nor the four-axis frame files. - **Control characters:** `grep -naP` over every changed file and this body finds nothing (exit 1). A planted control byte is found (exit 0). ## Acceptance notes - **`filter-token-unknown`'s verdict sentence contradicts the runtime it describes.** It says an unresolvable placeholder "is sent to the data engine as a literal string, matches no record, and the surface renders empty". So does the rule file's module header. The runtime refuses such a filter instead: `UnknownFilterTokenError` (`FILTER_TOKEN_UNKNOWN`, 400, `packages/core/src/utils/filter-tokens.ts`). That is the spec's own declared behaviour (`date-macros.zod.ts`: "the runtime resolver throws"). `packages/objectql/src/engine-filter-token-today-spelling.test.ts` pins the refusal. The new entry states the refusal. This slice keeps messages unchanged by its shape, so the sentence is not edited here. It was not measured through `os validate` in this run (the CLI was not built), so it is noted, not filed. - **The 200-character bound is measured over the suite's fixtures, as in slices 11–20.** Some lines interpolate names: - the group line: about 88 characters plus two copies of the key and the object name, and a nearest-group suggestion when one is close; - the shared anchor line: 155 characters plus the field and object names. Long enough names can push either past 200. The anchor line's wording predates this slice. `component-props-invalid`'s test re-measures its two type-naming lines at the longest type `ComponentPropsMap` declares. - **The census sees what the lint suite fires.** It measures 213 of the 215 id-shaped rule constants the root barrel exports. The other two never fire in the suite. `chart-config-missing` has no emit site in `packages/lint/src`. `react-prop-deprecated`'s hint is the react-blocks contract's own `note` for a deprecated prop. - **`main` moved after the one merge.** The branch carries `main` at `93b4acb57`, and `origin/main` is now at `e15897c0f`. None of the 5 newer commits touches `packages/lint`. Per the order, I merge again only for a conflict, a `main`-caused red or a queue removal. - `validate-expressions.test.ts` still names the deleted `unprovisionedAnchorHint` in a historical comment about an earlier card. It is outside this slice's surface and left as written. - **Not run locally, declared to CI.** - The `packages/cli` tiers. `test/explain-rule-id.test.ts` holds every `RULE_EXPLANATIONS` key disjoint from the `os explain` schema names and resolves each through the command. - `os validate` / `os explain` through a built CLI. - Both need the CLI build closure: 62 packages, most of them downstream of `@objectstack/lint`. ## Remaining for this card The census at `49e48cd21` covers the whole lint suite: 239 ids fire, and 3 are over 200. All 3 are outside the close condition (repo gate, not author-time): `lint-startup-registry-verdict.ts`'s `startup-open-vocabulary-verdict` 972, `startup-verdict-assertive-wording` 972 and `startup-source-unparseable` 254. No author-time `packages/lint` id fires a hint or `fix:` over 200 characters in the suite. The close-out slice still owes two things: the census re-measured on `main` after this lands, and the `content/docs/ui/react-pages.mdx` transcript carry from slice 15's landing record `6136999237`. --- _Generated by [Claude Code](https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ef65646 commit 9c74d40

15 files changed

Lines changed: 674 additions & 76 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): the last eight fenced 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 8 rule ids now carries a `hint` (the CLI's `fix:` line, the runtime finding's `hint`) of one remedy sentence of at most 200 characters on every variant the lint suite fires (lengths are the `hint` alone; the CLI prints it after `fix: `). The longest of each, before → after:
10+
- flow template holes (`validate-flow-template-paths.ts`): `flow-template-field-unprovisioned` (362 → 175) and `flow-template-lookup-traversal` (316 → 139);
11+
- page bindings (`validate-page-field-bindings.ts`): `page-field-unprovisioned` (362 → 175) and `page-section-group-unknown` (279 → 151);
12+
- `print-page-block-unprintable` (362 → 170), `form-section-group-unknown` (275 → 147), `filter-token-unknown` (259 → 170) and `component-props-invalid` (258 → 188; its value-rejection line 248–258 → 157–167, while its retired-key line, 164–181, and its node-binding line, 188, were one remedy already and are unchanged).
13+
- **Examples.** `print-page-block-unprintable` FROM ``Replace or remove the block. The printable blocks are `page:section`, `page:card`, … `object-metric`. If the page is not a document, delete its `print` declaration instead.`` (362) TO ``Replace the block with a printable one (`os explain print-page-block-unprintable` lists them) or remove it; if the page is not a document, delete its `print` declaration.`` (170). `flow-template-lookup-traversal` on a flow variable FROM ``A record read into a flow variable is not expanded either — config.expand is the START node's opt-in and covers the trigger record alone. Reference the foreign-key id directly ('{{ currentCase.owner_id }}'), project the value via a formula field on 'crm_case', or add a get_record node that reads the related record.`` (316) TO ``Write '{{ currentCase.owner_id }}' (the id) or a formula field on 'crm_case', or read the related record with a get_record node.`` (128).
14+
- **Lists are no longer echoed.** The two section-group ids no longer append the object's declared field groups: the line names the nearest declared group ("Did you mean") instead. An object with no field groups gets one line: declare `fieldGroups: [{ key, label }]` and `group` on the member fields, or enumerate the section's `fields`. `print-page-block-unprintable` no longer lists the thirteen printable blocks; `os explain` does. `filter-token-unknown`'s line for a placeholder with no near miss names the two vocabularies briefly, and its `os explain` entry in full.
15+
- **The anchor ids converge.** `page-field-unprovisioned` and `flow-template-field-unprovisioned` now print the shared one-line anchor fix the other unprovisioned-anchor ids print (`If the remote table carries 'owner_id', declare it in ext_customer's own fields (mapped through columnMap); otherwise drop the reference or opt the object out of the injection`, 175).
16+
- **`os explain RULE_ID` carries the rest.** Each of the 8 ids gains a `RULE_EXPLANATIONS` entry in `@objectstack/lint`, so its `rule:` line now carries the `os explain` pointer. What the fix lines stopped saying is there: how to vouch for an unprovisioned anchor through `columnMap` or opt the object out of the injection (`ownership: 'none'`, `systemFields: { audit: false }`); what the start node's `config.expand` does (a one-hop re-read of the triggering record as the run's identity) and why it covers the trigger record alone; how a section inherits a field group's members, and that membership is declared on the field; the printable block list, held equal to the spec's `PRINTABLE_PAGE_COMPONENT_TYPES`, and the three reasons a block is refused; the context tokens and date macros a filter value resolves, where `{record_id}` resolves, and that a literal shaped like a placeholder must be renamed; and where a page component's props schema lives, why the page and not the schema is what to fix in almost every case, and the retired-key and node-binding cases.
17+
- **At the runtime publish gate** (Studio, REST `PUT /api/v1/meta`, and the MCP and AI authors that write through that door), FROM → TO measured at the door. On a `page` write, as 422 `issues[].hint`: `print-page-block-unprintable` 362 → 170, `component-props-invalid`'s value-rejection line 248 → 157 (`page:tabs`), 250 → 159 (`object-grid`), 253 → 162 (`record:details`) and 258 → 167 (`record:related_list`). On a `flow` write, as 2xx `advisories[].hint`: `flow-template-field-unprovisioned` 362 → 175, `flow-template-lookup-traversal` 249 → 131 (trigger root) and 314 → 126 (flow variable), on a probe hopping through `crm_case.account`. The two page-binding ids do not fire on a `page` write, and `filter-token-unknown` and `form-section-group-unknown` run at the CLI only. No write changes channel.
18+
- **Unchanged:** every `message` (the verdict), rule id, severity and `path`, and what each rule accepts or refuses.

‎packages/lint/src/object-field-groups.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
* #13855, where it is offered for contract review.)
4040
*/
4141

42-
import { recordsOf } from './object-graph.js';
42+
import { recordsOf, suggestName } from './object-graph.js';
4343

4444
type AnyRec = Record<string, unknown>;
4545

@@ -149,13 +149,16 @@ export function checkSectionGroupRefs(
149149
`section references field group "${ref.key}", which object "${objectName}" does not `
150150
+ 'declare — the section inherits its members from that group, so it resolves to no '
151151
+ 'fields and the whole section silently does not render',
152+
// [#22161] One remedy line, for both host rules' ids. The object's
153+
// declared groups are no longer echoed — the nearest one is named instead
154+
// ("Did you mean") — and how a section inherits its members from a group
155+
// is each id's `os explain` entry.
152156
hint:
153-
`Fix the key, or declare it: \`fieldGroups: [{ key: '${ref.key}', label: '…' }]\` on `
154-
+ `${objectName}, with each member field carrying \`group: '${ref.key}'\`. `
155-
+ (declared.size > 0
156-
? `Declared groups on ${objectName}: ${[...declared].sort().join(', ')}.`
157-
: `${objectName} declares no field groups at all, so no section on it can reference one `
158-
+ 'yet — enumerate the members with `fields` until it does.'),
157+
declared.size > 0
158+
? `Fix the key, or declare '${ref.key}' in ${objectName}'s \`fieldGroups\` and set `
159+
+ `\`group: '${ref.key}'\` on its member fields.${suggestName(ref.key, declared)}`
160+
: `Declare \`fieldGroups: [{ key: '${ref.key}', label: '…' }]\` on ${objectName} and `
161+
+ `\`group: '${ref.key}'\` on its member fields, or enumerate the section's \`fields\`.`,
159162
});
160163
}
161164
return findings;

0 commit comments

Comments
 (0)