Skip to content

Commit 23419ba

Browse files
fix(lint): one-line verdicts for the nav, object-reference, capability, action-name, mapping-target, view-container and interface-page rules; os explain RULE_ID carries their reasoning (#22799)
Part of #22161 Clause-②: no Stage 2 of the card, slice 9: the 10 rule ids of ten whole `packages/lint` source files — the three app-navigation rules, the object field-name-list rule, the platform-prefixed object-reference rule, the capability-reference rule, the name-bound action rule, the import-mapping-target rule, the view-container rule and the interface-page visualization rule. Plus a text-only correction of the pending slice 5, 6 and 7 changesets (one clause each, below). 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 10 ids prints a `message` of one verdict sentence, followed by `Did you mean "…"?` where the rule offers the nearest declared name. Every finding the rules' own suites fire is now 199 characters or fewer; the longest of each id was 227 to 629 before. The runtime publish gate's suite (the whole `@objectstack/metadata-protocol` suite) fires one of the 10, `object-field-ref-unknown`, at 95 or fewer (273 before). The whole `packages/cli` unit tier fires one arm of `view-container-shape` whose text is unchanged; `os validate` on the four example apps fires none of the 10, before or after. - **Where the verdict keeps a name, it is the one the author needs to find the defect.** The verdict no longer repeats what the finding's `where` already names (the import mapping and its object, the interface page). `nav-object-unservable` names the object's `enable` key and the status the list answers; `nav-target-unresolved` keeps the "NO pages at all" clause when `defineStack`'s own check is off; `action-name-undefined` names where a related-list id IS defined, at most three places, then `(and N more)`; `mapping-target-field-unknown` keeps the `transform "lookup"` fix on the reference arm, its only fix not on the `fix:` line. - **`object-field-ref-unknown` prints the family's shared field-path sentence alone** (`describeFieldPathVerdict`, unchanged, shared with four other rules), exactly as slice 8 left `list-view-field-unknown`: `highlightFields[1] "field_10" is not a field on object "proj_task".` A short per-position consequence clause was measured first and dropped: with it, the path's hop verdicts (a dotted name through a non-relationship field) printed 206 to 232 characters on probes, and the clause read "does not exist" on a field that exists. What a miss costs at each position (the redaction that fails OPEN, the index the SQL sync skips, the field `dependsOn` gates for good) is the explanation's, written out per position. - **The flat-list-view arm of `view-container-shape` no longer states a false mechanism.** It said `ViewSchema` strips the keys and the entry "parses to an EMPTY container — zero views register". The module's own header records that as measured false since `ViewSchema` went strict, and the registration loop refuses the shape (`isViewContainerShaped`, 422 `INVALID_METADATA`). The verdict now says the schema and the loop refuse it, and the rule's test holds both seams (`ViewSchema.safeParse` refuses the flat and the ViewItem entry; `isViewContainerShaped` refuses both and takes the empty container). - **The reasoning moves to `RULE_EXPLANATIONS`** (`packages/lint/src/rule-explanations.ts`), 10 new entries, so `os explain RULE_ID` prints it and the CLI's `rule:` line ends with the pointer for these ids. 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.js explain nav-object-unservable` prints the entry. The 111 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, skip or dedupe moved, and no export was added, removed or renamed: every hunk in the ten rule files is a `message` expression, a comment, a verdict helper, or the removal of a value that fed only the old message. The plumbing hunks, each message-only: - `validate-object-field-refs.ts`: the module-private `consequence` member of `ListPosition`, `FieldNameSlot` and `INDEX_POSITION` (and the `judge` parameter that carried it) goes; it fed only the message, and its text is the explanation's. - `validate-nav-object-servability.ts`: the local `condition` and `answer` strings go (message-only); `offendingKey` stays and is now the one name the verdict carries. - `validate-mapping-target-fields.ts`: `dottedReason()` becomes `dottedVerdict()` (still a string, one sentence per arm; the `objectName` argument it no longer reads goes), and `capitalize()`, which fed only it, goes. - `validate-action-name-refs.ts`: `atMostThree()` (new, module-private) bounds the "defined only on …" list; the `consequence` defaults of `check()` become clauses. - No helper changed shape (string in, string out); no truth table is involved. - `.changeset/22161-lint-slice-9-one-line.md`: `@objectstack/lint` `patch`, naming every door that prints the new text (below). `Clause-②: no`, as line 2 says: every rule id these entries key already had an exported constant on the root barrel (`index.ts` is untouched), and slice 1's contract review (② of its record) reads `RULE_EXPLANATIONS` entries as data in an existing export. - **The changeset rider** (the dispatch's DELIBERATE CORRECTION, see Acceptance notes): `.changeset/22161-lint-slice-5-one-line.md:17`, `22161-lint-slice-6-one-line.md:19` and `22161-lint-slice-7-one-line.md:20` each said "the scaffold check `os init` and `os generate` run print …". Re-measured at the base `996aa86e0a`: `validateScaffold` (`packages/cli/src/utils/scaffold-validate.ts`) has one caller, `commands/init.ts:1314`, and `commands/generate.ts` imports nothing from `@objectstack/lint` and calls no authoring rule (its only stack read is `authoringRuleUnionStack`, a fold). Each now reads "the scaffold check `os init` runs print …". One line per file; front matter, level and `Clause-②:` line byte-identical. All three are still pending at the base (not consumed into a CHANGELOG). - **The declared rider went unused:** no `packages/cli/test` or `packages/metadata-protocol` file asserts these ids' message text (they pin `rule`, `path` and planted rule ids), and no `content/docs` page quotes one of these messages as printed output (`content/docs/permissions/authorization.mdx` and `permission-sets.mdx` describe the capability rule's behaviour, "registered nowhere", which stays true). `examples/app-showcase/test/nav-and-detail-grants.test.ts` asserts `navigation exposes object "showcase_cascade"`, so the new `nav-object-ungranted` verdict keeps that opening; the test ran green on the rebuilt dist (below), unedited. The verdict forms, the longest of each arm the rule suites fired: ```text navigation exposes object "crm_secret_token", but no permission set this stack declares grants read on it, so opening the entry fails permission-denied for everyone but a wildcard admin Navigation targets object "crm_secret_token", which cannot serve a list (`objects[1].enable.apiEnabled` is false), so the server prunes the entry: its list answers 404 for every user Navigation targets object "crm_audit_row", which cannot serve a list (`objects[2].enable.apiMethods` lacks `list`), so the server prunes the entry: its list answers 405 for every user Navigation targets dashboard 'nope', which `dashboards` does not declare (there are NO dashboards at all, so `defineStack` skipped the entry), so the entry resolves to nothing when clicked Navigation targets page 'typo', which `pages` does not declare, so the entry resolves to nothing when clicked highlightFields[1] "field_10" is not a field on object "proj_task". fields.account.lookupColumns[0] "crm_account.region" is not a field on object "crm_account". Did you mean "region"? fields.account.lookupFilters[0].field "status.label" traverses "status", which is a `select` field on object "crm_account" and not a relationship — there is nothing to join through. field-backed param object "sys_approval_process" has a platform prefix, but no known package registers it and this stack does not define it, so it likely resolves to nothing. Did you mean "sys_app"? requiredPermissions names capability "clm_legal_workbnch.view", which no built-in, `defineCapability`, `systemPermissions` grant or `sys_capability` seed provides, so this gate fails closed Bulk-action menu names action "crm_convert_leads", which no action in this stack defines, so the button does nothing when clicked. Did you mean "crm_convert_lead"? Alert call-to-action names action "resend_verifcation_email", which no action in this stack defines, so the banner renders without its call-to-action button. Did you mean "resend_verification_email"? Page-header actions names action "covert_lead", which no action in this stack defines, so the header draws no button for it. Related-list actions names action "crm_merge_accounts", not an action of related object "crm_contact" (defined only on object "crm_account"), so the list draws no button for it. Related-list actions names action "crm_lgo_call", not an action of related object "crm_contact" (defined nowhere in this stack), so the list draws no button for it. Did you mean "crm_log_call"? Related-list actions names action "crm_sync_contact" of "crm_contact", which is placed at no location a related list draws, so the list draws no button for it. Related-list actions names action "crm_score_contact" of "crm_contact", which declares no `locations`, so the list draws no button for it. Target "emial" names no field of "crm_contact", so the import endpoint refuses this mapping (INVALID_FIELD) and it imports nothing. Did you mean "email"? Target "mailing_address.stret" names no part of the address field "mailing_address", so the import endpoint refuses this mapping (INVALID_FIELD). Did you mean "street"? Target "full_name.first" is dotted, but the text field "full_name" has no parts (only a compound field does), so the import endpoint refuses this mapping (INVALID_FIELD). Target "account.name" cannot traverse the lookup field "account", so the import endpoint refuses this mapping (INVALID_FIELD); map the column to "account" with transform "lookup". The compound field "mailing_address" is mapped both whole (fieldMapping[0].target) and by its part "street", so the two collide and the import endpoint refuses this mapping (INVALID_FIELD). A ViewItem record (`viewKind`) is not a view container: `views:` carries containers only, so the stack schema, this rule and the registration loop all refuse it. Flat list-view object is not a view container: its single-view keys sit where the container slots belong, so the stack schema and the registration loop refuse it. 'gantt' leads the whitelist, but its `startDateField + endDateField` derives from no field of "showcase_task" and no `sourceView` supplies it, so every visitor lands on its refusal screen 'kanban' is in the whitelist, but its `groupByField` derives from no field of "crm_note" and view "schedule" has no `kanban:` block, so the switcher SILENTLY drops it ``` ## Shared prose: written once (the dispatch's route, measured) | shared paragraph | ids | explanation constant (`rule-explanations.ts`) | held to its source by | |---|---|---|---| | a nav entry's grant and its servability are independent conditions | `nav-object-ungranted`, `nav-object-unservable` | `NAV_INDEPENDENT_CONDITIONS` | one text under both ids (each rule's test) | The dispatch expected the three nav rules and the two object-reference rules to share paragraphs. Measured: the nav rules walk different containers (`nav-object-ungranted` reads each area's `navigation` only; the other two read each area's `items` and `navigation`), and `nav-target-unresolved`'s facts (the `defineStack` size gate, no registry for pages) are its own, so one paragraph is shared, not a walk. `object-reference-unknown` prints one sentence of at most 175 characters at the base (lint suite; 188 is `os lint`'s printer row, which prefixes `where`), so it is not converted and has no entry to share with. The explanation module imports nothing, so each fact it writes out is held to its source by the rule's own test: the read bits by running `validateNavAccess` on each; the walked nav containers by firing each; the three nav target types and collections by running the rule on each; the object-field positions by the paths fired; the platform prefixes to `PLATFORM_OBJECT_PREFIXES`; the four capability sources by each one silencing the rule; the related-list drawn locations by running the rule over every `ACTION_LOCATIONS` member; the provisioned mapping columns by running the rule; the view-container refusals to `ViewSchema` and `isViewContainerShaped`; and the derivation paragraph to `OBJECTUI_DERIVATION_PREDICATES`, row by row. ## Census (taken first, before any edit, at the base `996aa86e0a`) Method: slice 4–8's scratch preload (`NODE_OPTIONS=--import`, never committed), which patches `Array.prototype.push` to record every finding-shaped object (`rule` + `message`) of the 10 ids (plus `object-reference-unknown`, the second id of `validate-object-references.ts`, to measure it), deduped by (rule, message) per process, with its push site. Lengths are `message` alone; the printed line adds `where` and `: `. Rows from `os lint`'s own printer (`commands/lint.ts:761`, which pushes `where: message`) are named as such. Positive control in every run: `field-no-consumers` (and, in the runtime-gate suite, `sort-field-unknown` and `searchable-field-unknown`), recorded in every run. - **`packages/lint` suite**, base 136 files / 6,374 tests (after the CLI, metadata-protocol and example closure build): **all 10 ids fire**, every one over 200 at its longest; the longest of each equals PR #22765's figure except `capability-reference-unknown`, 227 here (219 in slice 2's census, a longer capability name in a case added since). `object-reference-unknown`: 21 messages, longest 175. - **`packages/cli` unit tier**, the whole project (280 files, 4,170 tests, all loaded; `packages/cli/dist` built): fires `view-container-shape`'s empty-container arm only (127; 150 as `os lint`'s printer row), whose text this slice leaves unchanged, and `object-reference-unknown` (not converted). Control recorded. - **Runtime publish gate:** the whole `@objectstack/metadata-protocol` suite (227 files, 224 run, 28,116 passed), which reads `@objectstack/lint` from its built `dist/`: fires `object-field-ref-unknown` (4 messages, `protocol-publish-drafts-object-field-refs.test.ts` and `protocol.runtime-authoring-gate.test.ts`, which pin rule id, path and field name, never the text); none of the other 9. Controls recorded both runs. - **Example apps:** `os validate` (the built CLI) on `app-crm`, `app-todo`, `app-showcase` and `app-multi-package`, exit 0 each, before and after: **none of the 10 fire**; control recorded (22 messages), and every app prints the same number of `rule:` lines before and after (9 / 7 / 70 / 3). - **Extra, outside the dispatch's list:** three `packages/cli` integration-tier files that name these ids (`authoring-rule-command-parity.test.ts`, `verify-author-time-stage.test.ts`, `union-fold-command-parity.test.ts`; the fourth, `build-multi-package-artifact.e2e.test.ts`, is in no vitest project and is declared to CI), which fire `view-container-shape`'s flat arm. - **After:** the lint `dist/` rebuilt from the slice (marker `so the switcher SILENTLY drops it` 1 hit each in `dist/index.js`, `index.cjs`, `runtime.js`, `runtime.cjs`; the old `visualization, but nothing supplies its` 0 in each; `how a list page binds each visualization it allows` 1 in `dist/rule-explanations.js`). The cli-unit, runtime-gate, example and cli-integration after-runs read the dist built at `5beae52dd0`; the one lint-source change after it is the text of `action-name-undefined`'s related-list placement arm, an id none of those four runs fires, before or after. The lint-suite after-run below is at `f5bf79ea14`, whose lint source is the final head's. | rule id | severity | lint suite: before (msgs · longest) → after (msgs · range) | runtime-gate suite | cli integration (3 files) | |---|---|---|---|---| | `nav-object-ungranted` | warning | 3 · 353 → 3 · 177–185 | — | — | | `nav-object-unservable` | error | 1 · 528 → 3 · 180–183 | — | — | | `nav-target-unresolved` | warning | 6 · 426 → 7 · 109–188 | — | — | | `object-field-ref-unknown` | error | 36 · 320 → 40 · 65–181 | 4 · 273 → 4 · 67–95 | — | | `object-reference-unregistered-platform` | warning | 5 · 324 → 9 · 186–198 | — | — | | `capability-reference-unknown` | warning | 14 · 227 → 15 · 174–189 | — | — | | `action-name-undefined` | error | 24 · 409 → 30 · 117–199 | — | — | | `mapping-target-field-unknown` | error | 8 · 466 → 9 · 129–189 | — | — | | `view-container-shape` | error | 4 · 290 → 4 · 127–182 | — | 1 · 173 → 1 · 162 (the flat arm) | | `page/visualization-without-binding` | error / warning | 15 · 629 → 25 · 153–187 | — | — | The cli unit tier column is not drawn: it fires `view-container-shape`'s empty-container arm alone (127, and 150 as two `os lint` printer rows), before and after, with unchanged text. The example-app column is not drawn: none of the 10 fire there, before or after. `object-reference-unknown` (not converted): lint suite 21 · 175 before and after. All push sites fire before and after (`action-name-undefined` 3, `mapping-target-field-unknown` 2, `view-container-shape` 3, every other id 1). A higher message count after is a case this slice added (the servability arms, the every-site object-reference probe, the dotted object-field paths, the alert and header arms, the every-visualization probe). ## Doors that print the new text Read from the registry (`authoring-rules.ts`: `validateCapabilityReferences` and `validateViewContainers` are `commands: ALL`, CLI only; the reference-integrity suite, `commands: ALL`, CLI and runtime, dispatches `validateObjectFieldRefs` on `['flow', 'object']`, `validateObjectReferences` on `['flow', 'dataset']`, and the nav, action-name, mapping and page-visualization members on the default `['flow']`), the runtime gate's split (errors → the 422 issue, everything else → 2xx `advisories` plus the `[Protocol] authoring advisory` log line, both after the gate's differential), and the CLI's callers (`commands/validate.ts`, `compile.ts`, `lint.ts`, `verify.ts` through `judgeAuthorTimeRules` on the `validate` set, and `init.ts` through `validateScaffold` on the `build` set). Measured at the runtime door with `runRuntimeAuthoringRules` (scratch probe): a `dataset` write over `sys_approval_process` answers `object-reference-unregistered-platform` in `advisories`; an `object` write with a dangling `highlightFields` entry answers `object-field-ref-unknown` in `errors` (and its `lookup` to `sys_approval_process` is not judged there); a `flow` write answers none of the 10. | door | ids | what changes | |---|---|---| | `os validate`, `os build` (and `os compile`, which `os dev` runs per compile), `os lint`, `os verify`, the scaffold check `os init` runs | all 10 | 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` | | runtime publish gate, `object` write | `object-field-ref-unknown` (error) | the 422 issue's `message` and the `OS_ALLOW_UNLINTED_METADATA_WRITES` refusal log line | | runtime publish gate, `dataset` write | `object-reference-unregistered-platform` (warning, the dataset's base `object`) | the 2xx `advisories` entry and the advisory log line | | never at the runtime gate | `capability-reference-unknown`, `view-container-shape` (CLI-only rules); the three nav ids, `action-name-undefined`, `mapping-target-field-unknown`, `page/visualization-without-binding` (dispatched only for a `flow` write, whose snapshot carries no apps, pages, views or mappings of its own) | | | 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–8. The ten touched suites wrap their rule import and record every finding their direct calls fire; the LAST case in each file holds every recorded verdict of its id to one line of at most 200 characters, behind a coverage control (each arm: both servability conditions; all three nav target types with and without the empty-collection clause; every object-field position, the retired `compactLayout` spelling and both hop verdicts of a dotted path; all nine object-reference sites; every action-name surface and all four related-list arms; all five mapping arms; all four view-container arms; both severities, both remedy arms and every visualization with a deriver). Exact pins hold one verdict per arm; explanation pins hold, per id, that `explainRule(id)` exists and names what the verdict stopped saying, that the shared paragraph is one text under both ids it serves, and the source-held facts in the table above. Every refusal assertion, rule id, severity, `path`, `where` and hint pin is unchanged; the pins that read the old text moved to the new verdict or to the explanation (`size > 0` and `forced view type` to the explanation; `fails OPEN`, `stays gated for good` and `` `unique` index is then unenforced `` to the explanation; the mapping arms' and the related-list arms' fragments to the new sentences; the interface-page search description to the `fix:` line, where the hint already carries it). `validate-nav-object-servability.ts` had no test file of its own (its one pin was the suite's "every member actually runs" case); it gains `validate-nav-object-servability.test.ts`, which covers both conditions, the walked containers and the four skips besides the new shape. - **Lint suite** at `f5bf79ea14` (the final head `79747fd0ce` adds changeset text only): `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2`: Test Files 137 passed (137), Tests 6,416 passed (6,416); VERDICT command-exit 0 (base 136 / 6,374; one new file, 42 new cases). - **Lint build + typecheck:** `pnpm --filter @objectstack/lint build` (VERDICT command-exit 0, `check-dts-emitted` 6/6), and the merged closure build at `f5bf79ea14` (`turbo run build`, 62 tasks, VERDICT command-exit 0). `pnpm --filter @objectstack/lint run typecheck` at `f5bf79ea14`: VERDICT command-exit 0; `check:test-typecheck` OK, 2 files / 6 errors / 2 pinned signatures held. `tsc -p packages/lint/tsconfig.test.json --listFiles` lists all ten touched test files, the new one included. - **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,170 passed (4,170), before and after; VERDICT command-exit 0 both. It includes `test/explain-rule-id.test.ts` (iterates every `RULE_EXPLANATIONS` key, so the 10 new ids resolve through `os explain`, `explainPointer` and the listing). `node bin/run.js explain nav-object-unservable` prints the entry on the dist built at `f5bf79ea14`. The integration tier is declared to CI (no file in it asserts these ids' text, and the diff touches no spawn entry); its three files that name these ids ran as the census extra above: Test Files 3 passed (3), Tests 23 passed (23), before and after. - **Runtime gate:** the whole `@objectstack/metadata-protocol` suite, before and after: Test Files 224 passed | 3 skipped (227), Tests 28,116 passed | 19 skipped (28,135), both runs; VERDICT command-exit 0. - **The showcase pin on the `nav-object-ungranted` opening:** `pnpm --filter @objectstack/example-showcase exec vitest run test/nav-and-detail-grants.test.ts` on the final dist: Test Files 1 passed (1), Tests 4 passed (4); VERDICT command-exit 0. - **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 `f5bf79ea14`, 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. `nav-object-ungranted`'s pre-slice message block was restored verbatim (the replacement read from the base file) through the anchor of its new block: anchor x1 → x0, blob `5358057cddb2` → `efc20da6f54e`. Predicted before the run: exactly two cases red — the 200-character bound pin and the id's exact-text pin; every other assertion on the id pins fragments the old text also carries (`crm_forecast`, the path, the rule id). Observed: `src/validate-nav-access.test.ts` Test Files 1 failed (1), Tests 2 failed | 18 passed (20); the two red cases are exactly those two (the bound pin read 349 characters). Restored: blob `5358057cddb2` == blob at HEAD, `git diff HEAD` empty, `git status --porcelain` empty. - **ESLint, narrowed:** `npx eslint --no-inline-config --format json` over the 21 changed `.ts` files at `79747fd0ce` gave 21 files in the report, 0 errors, 0 warnings. The population is `eslint.config.mjs`'s `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` block plus the `packages/**/*.{ts,tsx,mts,cts}` blocks, which all 21 are in (the changesets are not a linted kind). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules; the config's own note says so), 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, at the final head `79747fd0ce` (merge base `e84aeb36ce`, after one merge of `origin/main`; `origin/main` has since moved two commits, an ADR and the new `packages/skills`, which touch no `packages/lint`, `packages/cli`, `packages/metadata-protocol` or `.changeset` file), derived 62 commands, the same set as at `51dc621510` before the merge. I ran 61 of them sequentially at that head, each with its own log and its exit code captured before any pipe: 60 exited 0, and `node scripts/check-empty-changeset.mjs --base origin/main` exited 1, the expected DELIBERATE CORRECTION reading on the three slice 5–7 changesets (Acceptance notes). One is NOT MEASURED, as the dispatch directs: `pnpm check:type-check-debt` (`--re-measure`, which builds every package); its non-re-measuring half, `check:type-check-coverage`, ran and exited 0. `--ran` over the exit-coded record: `Run reconciliation — 62 derived, 61 run, 1 NOT-MEASURED, 0 UNRUN.` The four artifact-roster families rostered under a directory this diff touches (`check-changeset-fixed`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`) also ran at the head: all exited 0. An earlier full pass at `f5bf79ea14` (the head before the last changeset-text commit) read the same: `check:dual-build-cjs-loads` there first answered `PREREQUISITE NOT MET` (six packages' `dist/` absent: `studio`, `client-react`, `embedder-openai`, `knowledge-ragflow`, `organizations`, `service-cluster-redis`), I built exactly those six under the lock (VERDICT command-exit 0), and it exited 0 then and at the final head. The long ones on the shared box: `check:query-options-erasure` 316s, `check:slot-lookup` 166s, `check-comment-mask-corpus` 136s. ## Remaining for later slices — 10 ids in `packages/lint`, by file PR #22765's list minus this slice's ten files and ids. The lengths are slice 2's (PR #22448, census at `05c7c3fa3b`), not re-measured here. - `lint-flow-credential-literals.ts` (1): `flow-credential-literal` 390 - `validate-ai-surface-affinity.ts` (1): `ai-skill-surface-mismatch` 281 - `validate-ai-tool-references.ts` (1): `ai-skill-tool-unresolved` 441 - `validate-flow-filter-tokens.ts` (1): `flow-filter-token-unknown` 278 - `validate-managed-api-methods.ts` (1): `object/managed-api-method-unaffordable` 412 - `validate-org-axis-red-lines.ts` (1): `org-axis-cross-org-bu-grant` 365 - `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 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 - **`Check Changeset` is red on this head by design.** The three slice 5–7 edits are the `check-empty-changeset` DELIBERATE CORRECTION class (pending notes corrected in place before the release that publishes them); `pr-automation.yml` route 0 reds on any edit to an existing `.changeset/*.md`. The files are not restored and `skip-changeset` is not applied (this PR adds a changeset of its own). **Written confirmation is requested** from the review at `CONTRACT_REVIEW_TIER`: that the three one-clause edits are the measured correction and nothing else (`git diff 996aa86 -- .changeset/22161-lint-slice-{5,6,7}-one-line.md`: 3 files, 3 insertions, 3 deletions, one hunk each). - **Hints, for the card's open hint call:** none of the 10 ids' hints points at text the verdicts cut. Each was read against the cut text: the nav hints (grant, `enable` and declaration remedies), the object-field prescriptions with their `Fields on …` list, the object-reference hint (with the per-site consequence it carries), the capability hint, the action-name hints (the related-list ones list the drawn locations), the mapping hints (with the compound parts), the view-container wrap-it hints and the interface-page hint (which says what the derivation looks for) are each self-contained. Unchanged here, as every slice leaves hints. One inverse case, kept: `mapping-target-field-unknown`'s reference arm carries the `transform "lookup"` fix in its verdict, because its hint does not say it. - **`object-reference-unknown` is not converted** (175 at its longest in the lint suite, 153 in the cli unit tier, before and after; the dispatch's mechanism assumption: include it only if it exceeds about 200). - **`field-no-consumers` (stage 1's id)** still prints its 341-character verdict on `os validate` of `examples/app-showcase` (this census's control), as PRs #22742 and #22765 recorded; untouched, a later slice's. - **`view-container-shape`'s flat-arm `looksFlat` fingerprint** reads `type`, `columns`, `data`, `filter`, `sort`, while the registration loop's `isViewContainerShaped` also refuses `sections`. A container-less entry with only `sections` set reaches this rule's empty-container arm instead of the flat one; observation only, no wrong verdict follows (both arms are errors with the wrap-it hint). - **A long name can still lengthen a line.** The 200 bound holds on every variant the suites fire; the object, field, action, page, view, capability and mapping names in a verdict are the author's. --- _Generated by [Claude Code](https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 14f4912 commit 23419ba

25 files changed

Lines changed: 1473 additions & 182 deletions

‎.changeset/22161-lint-slice-5-one-line.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ Clause-②: no
1414

1515
A verdict no longer repeats what the finding's `where` and `path` already name: the validation rule and its object, the JSON Pointer of a `format`, the measure or dimension name, the source of an uncompilable `regex` (the `fix:` line still quotes it). A refusal the verdict quotes is quoted to its verdict. `TimeRelativeTriggerSchema`'s refusal of a descriptor is quoted for ONE issue (an unrecognized key first) and the rest are counted, `(and N more)`; ajv's metaschema refusal is quoted to its first violation, with the rest counted; a `@objectstack/spec` unknown-key refusal is quoted to the key and the schema's rename (`Did you mean …?`), without the wrong-layer prescription and the history sentence that follow it; and when no form of an `<ObjectChart aggregate>` `groupBy` union matches, the verdict keeps the forms whose complaint is about the value's content. A measure or dimension over a column its join chain reaches names a `joined object` (it read "(reached through this dataset's join chain)"). The `fix` (the CLI's `fix:` line, the runtime issue's `hint`), every rule id, severity and `path`, and what each rule accepts or refuses are unchanged. A tool that matched the old message text should match on `rule` and `path` instead.
1616
- **`os explain <rule-id>` takes these 15 ids**, for example `os explain flow-trigger-unroutable`. It prints the reasoning the verdicts no longer carry: the engine's trigger routing chain, the record-trigger token grammar and the time-relative routing predicate, what nothing reports at run time, and where the publish gate refuses the flow; the inbound api secret (ADR-0041) and why the engine refuses to register a flow without one; the write path's fail-open skip of a rule whose `regex` or `schema` does not compile, the runtime's ajv environment, and what ajv does with an unregistered `format` name; each aggregate's accepted field types (the `AGGREGATE_FIELD_TYPE_COMPATIBILITY` rows, written out), what a JSON-stored column is, and why analytics refuses to group by one; how a forwarded `<ObjectChart drillDown>` or `aggregate` refusal is quoted and which keys the schema answers with a prescription instead of a rename (`schedule` and `runAs` on a time-relative descriptor, `drilldown`, `mode`, `report`, `view` and `sort` on a drill block, five aggregate and two `groupBy` keys); what an unprovisioned anchor is; why a `record:*` block renders empty on a react page; and why an unparseable react source is a warning. Paragraphs shared across ids are one text, printed under every id they explain. The `rule:` line under each of these findings now ends with `` — `os explain <rule-id>` for … ``. The no-argument listing and its `--json` `rules` array list the 15 ids, and so does the unknown-id error's `Rules with an explanation:` line. `RULE_EXPLANATIONS` in `@objectstack/lint` gains the 15 entries.
17-
- **Where the new text prints.** On the CLI, all 15 ids: `os validate`, `os build` (and `os compile`, which `os dev` runs on every compile), `os lint`, `os verify`, and the scaffold check `os init` and `os generate` run print the new `message` on the text face, and `os validate --json` and `os build --json` carry it in their `errors` and author-time `issues`. At the runtime publish gate (Studio, REST `/meta`, MCP): a `flow` write carries the five flow ids (all errors, refusing an active flow's publish), a `dataset` write the two dataset ids, and an `object` write the three validation-rule ids — the 422 issue's `message` and the refusal log line under `OS_ALLOW_UNLINTED_METADATA_WRITES` change. Each issue's `hint` is unchanged.
17+
- **Where the new text prints.** On the CLI, all 15 ids: `os validate`, `os build` (and `os compile`, which `os dev` runs on every compile), `os lint`, `os verify`, and the scaffold check `os init` runs print the new `message` on the text face, and `os validate --json` and `os build --json` carry it in their `errors` and author-time `issues`. At the runtime publish gate (Studio, REST `/meta`, MCP): a `flow` write carries the five flow ids (all errors, refusing an active flow's publish), a `dataset` write the two dataset ids, and an `object` write the three validation-rule ids — the 422 issue's `message` and the refusal log line under `OS_ALLOW_UNLINTED_METADATA_WRITES` change. Each issue's `hint` is unchanged.
1818
- **Never at the runtime gate:** the five `react-*` ids. Their rule runs at that door only for a flow write, whose snapshot carries no pages, so they speak only on the CLI doors above.

‎.changeset/22161-lint-slice-6-one-line.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ Clause-②: no
1616

1717
A verdict no longer repeats what the finding's `where` already names: the view that wires an action (`action-dispatch-contract-mismatch`). The unprovisioned-anchor ids print the same one-clause cause the other converted anchor rules print (`'owner_id' is an injected column with no storage on external object 'x'`), and the two virtual-entry ids (`sort-field-unsortable`, `searchable-field-unsearchable`) state the storage fact in one shared wording. `searchable-field-unsearchable` quotes at most three names of an object's declared set, then `(and N more)`. A retired component type (`user:profile`, `element:filter`, `element:form`, `ai:chat_window`) is quoted to the head of its prescription in `RETIRED_PAGE_COMPONENT_TYPES` (`` `element:filter` was removed in @objectstack/spec 17 (ADR-0049) ``), and the verdict says the parse refuses the node by name; the whole prescription is the parse door's refusal of the same name, which `os validate` and `os build` print. `filter-preset-comparand` opens with the first sentence of the refusal the schema door shares (`"last_30_days" is a dashboard date-range PRESET name, not a filter value`), then names the operator and the preset's `{date-macro}` window. The `fix` (the CLI's `fix:` line, the runtime issue's `hint`), every rule id, severity and `path`, and what each rule accepts or refuses are unchanged. A tool that matched the old message text should match on `rule` and `path` instead.
1818
- **`os explain <rule-id>` takes these 12 ids**, for example `os explain sort-field-unknown`. It prints the reasoning the verdicts no longer carry: why an unknown sort field breaks a view's first fetch and every load after it, and which list-view surfaces the sort and search rules walk and skip; what a `formula` field's lack of storage does to an ORDER BY and to a search; what an unprovisioned anchor is and what sorting or searching one measured; how a stale `searchableFields` entry narrows a search or falls through to the auto-default set, and how a list view's narrowing reaches the runtime as the `$searchFields` override; how a metadata-form predicate path is resolved against the edited schema, why a dead predicate fails open, and why the right side of `==` / `!=` is a literal; how the two bulk wirings call an action and why nothing refuses a mismatch at run time; which component types the vocabulary closes and where a retired type's prescription lives; and where a date-range preset name is understood and what each layer does with a bare one. Paragraphs shared across ids are one text, printed under every id they explain. The `rule:` line under each of these findings now ends with `` — `os explain <rule-id>` for … ``. The no-argument listing and its `--json` `rules` array list the 12 ids, and so does the unknown-id error's `Rules with an explanation:` line. `RULE_EXPLANATIONS` in `@objectstack/lint` gains the 12 entries.
19-
- **Where the new text prints.** On the CLI, all 12 ids: `os validate`, `os build` (and `os compile`, which `os dev` runs on every compile), `os lint`, `os verify`, and the scaffold check `os init` and `os generate` run print the new `message` on the text face, and `os validate --json` and `os build --json` carry it in their `errors` and author-time `issues`. At the runtime publish gate (Studio, REST `/meta`, MCP), the 422 issue's `message` and the refusal log line under `OS_ALLOW_UNLINTED_METADATA_WRITES` change for: the three `searchable-field-*` ids on a `view`, `object` or `flow` write; the three `sort-field-*` ids on a `view` or `flow` write; the three `predicate-*` ids on a `view` write; and `filter-preset-comparand` on a `dashboard`, `view`, `object`, `page`, `flow` or `report` write. Each issue's `hint` is unchanged.
19+
- **Where the new text prints.** On the CLI, all 12 ids: `os validate`, `os build` (and `os compile`, which `os dev` runs on every compile), `os lint`, `os verify`, and the scaffold check `os init` runs print the new `message` on the text face, and `os validate --json` and `os build --json` carry it in their `errors` and author-time `issues`. At the runtime publish gate (Studio, REST `/meta`, MCP), the 422 issue's `message` and the refusal log line under `OS_ALLOW_UNLINTED_METADATA_WRITES` change for: the three `searchable-field-*` ids on a `view`, `object` or `flow` write; the three `sort-field-*` ids on a `view` or `flow` write; the three `predicate-*` ids on a `view` write; and `filter-preset-comparand` on a `dashboard`, `view`, `object`, `page`, `flow` or `report` write. Each issue's `hint` is unchanged.
2020
- **Never at the runtime gate:** `component-type-unknown`, which runs on the CLI doors only, and `action-dispatch-contract-mismatch`, whose rule runs at that door only for a `flow` write, whose snapshot carries no actions to judge, so both speak only on the CLI doors above.

‎.changeset/22161-lint-slice-7-one-line.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ Clause-②: yes (widening: three rule id constants exported from the barrel)
1717
The `manager` arm of `approval-approvers-may-resolve-empty` stays conditional — it says what happens where `sys_user.manager_id` is unset and asserts nothing about the column, which the rule cannot read. `view-ref-nav-view-missing` still lists the object's list views, at most three and then `(and N more)`, and `chart-axis-not-selected` quotes at most three names of the chart's selection. The `fix` (the CLI's `fix:` line, the runtime issue's `hint`), every rule id, severity and `path`, and what each rule accepts or refuses are unchanged. A tool that matched the old message text should match on `rule` and `path` instead.
1818
- **Three rule id constants are published** for ids `os lint`'s data-model sweep already emitted without one: `RELATIONSHIP_MASTER_DETAIL_REQUIRED` (`relationship/master-detail-required`), `RELATIONSHIP_DELETE_BEHAVIOR` (`relationship/delete-behavior`) and `ROLLUP_NON_NUMERIC_AGGREGAND` (`rollup/non-numeric-aggregand`), exported from `@objectstack/lint` beside the `UNIQUE_*` constants. The id strings themselves are unchanged.
1919
- **`os explain <rule-id>` takes these 15 ids**, for example `os explain unique/double-declaration`. It prints the reasoning the verdicts no longer carry: why an all-group or all-`manager` approval slate can stall, what silences the `manager` arm and what does not, and what an `org_membership_level` approver resolves against; the ADR-0120 scope words, the two channels that refuse a bare `unique: true` on a declared index, how a field `unique` and an index `unique` contradict or repeat, and why a hand-written organization composite enforces nothing on NULL rows; why a `master_detail` reference must be required under `controlled_by_parent`, what deleting a master does to its details, and why a `min` / `max` roll-up needs a numeric child; the platform agent roster and its retired aliases, how the runtime enforces the agent withdrawal, and what `defaultAgent` resolves against; how a navigation `viewName` is matched and what a renamed view key does to its references; and which chart positions bind a measure on each surface and which selection each is measured against. Paragraphs shared across ids are one text, printed under every id they explain. The `rule:` line under each of these findings now ends with `` — `os explain <rule-id>` for … ``. The no-argument listing and its `--json` `rules` array list the 15 ids, and so does the unknown-id error's `Rules with an explanation:` line. `RULE_EXPLANATIONS` in `@objectstack/lint` gains the 15 entries.
20-
- **Where the new text prints.** On the CLI: `os validate`, `os build` (and `os compile`, which `os dev` runs on every compile), `os lint`, `os verify`, and the scaffold check `os init` and `os generate` run print the new `message` on the text face for the approval, agent, view-reference and chart ids and for the three `unique/*` ids, and `os validate --json` and `os build --json` carry it in their `errors` and author-time `issues`. `relationship/master-detail-required`, `relationship/delete-behavior` and `rollup/non-numeric-aggregand` print on `os lint` only (and in the metadata-generation rubric's lint half), as before. At the runtime publish gate (Studio, REST `/meta`, MCP): on a `flow` write, the two approval ids ride the 2xx `advisories` and the `[Protocol] authoring advisory` log line; on a `report` write, `chart-measure-unknown` at a report's `values` or chart `yAxis` changes the 422 issue's `message` and the refusal log line under `OS_ALLOW_UNLINTED_METADATA_WRITES`, and `chart-measure-unknown` at a report chart's `series[]` and `chart-axis-not-selected` ride the 2xx `advisories` and the advisory log line. Each issue's `hint` is unchanged.
20+
- **Where the new text prints.** On the CLI: `os validate`, `os build` (and `os compile`, which `os dev` runs on every compile), `os lint`, `os verify`, and the scaffold check `os init` runs print the new `message` on the text face for the approval, agent, view-reference and chart ids and for the three `unique/*` ids, and `os validate --json` and `os build --json` carry it in their `errors` and author-time `issues`. `relationship/master-detail-required`, `relationship/delete-behavior` and `rollup/non-numeric-aggregand` print on `os lint` only (and in the metadata-generation rubric's lint half), as before. At the runtime publish gate (Studio, REST `/meta`, MCP): on a `flow` write, the two approval ids ride the 2xx `advisories` and the `[Protocol] authoring advisory` log line; on a `report` write, `chart-measure-unknown` at a report's `values` or chart `yAxis` changes the 422 issue's `message` and the refusal log line under `OS_ALLOW_UNLINTED_METADATA_WRITES`, and `chart-measure-unknown` at a report chart's `series[]` and `chart-axis-not-selected` ride the 2xx `advisories` and the advisory log line. Each issue's `hint` is unchanged.
2121
- **Never at the runtime gate:** the three agent ids (the rule runs there only on a `flow` write, whose snapshot carries no agents or apps), the two view-reference ids and the three `unique/*` ids (CLI-only rules), the three data-model sweep ids (`os lint` only), and the chart ids at a list-view or page chart (a report write carries neither surface).
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): the navigation, object-reference, capability, action-name, import-mapping, view-container and interface-page findings print one verdict line, and `os explain <rule-id>` carries their reasoning
6+
7+
Clause-②: no
8+
9+
- **Shorter verdicts.** Each finding of these 10 rule ids now prints a `message` of one verdict sentence. Every finding the rules' own test suites fire is at most 199 characters, and the runtime publish gate's suite at most 95; before, the longest of each ran from 227 to 629 characters. The ids:
10+
- app navigation (`apps[].navigation`, `areas[]`): `nav-object-ungranted`, `nav-object-unservable`, `nav-target-unresolved`;
11+
- object field-name lists (`highlightFields`, `publicSharing.redactFields`, `indexes[].fields` and the field-level `relatedListColumns`, `lookupColumns`, `lookupFilters`, `dependsOn`): `object-field-ref-unknown`;
12+
- object-name references: `object-reference-unregistered-platform` (its sibling `object-reference-unknown` already printed one sentence of at most 175 characters and is unchanged);
13+
- `requiredPermissions`: `capability-reference-unknown`;
14+
- name-bound actions (list views, page components, navigation): `action-name-undefined`;
15+
- import mappings (`mappings[].fieldMapping[].target`): `mapping-target-field-unknown`;
16+
- the stack's `views:` collection: `view-container-shape`;
17+
- interface list pages (`appearance.allowedVisualizations`): `page/visualization-without-binding`.
18+
19+
A verdict no longer repeats what the finding's `where` already names: the import mapping and its object, and the interface page. `object-field-ref-unknown` prints the shared field-path sentence the list-view rules print (`highlightFields[1] "field_10" is not a field on object "proj_task".`); what a miss costs at each position is its explanation's. `action-name-undefined` names at most three places a related-list action is defined, then `(and N more)`. `nav-object-unservable` names the object's `enable` key and the status the list answers (404 or 405). The flat-list-view arm of `view-container-shape` no longer says `ViewSchema` strips the keys to an empty container — the schema is strict and refuses that shape, and so does the registration loop; it now says so. The `fix` (the CLI's `fix:` line, the runtime issue's `hint`), every rule id, severity and `path`, and what each rule accepts or refuses are unchanged. A tool that matched the old message text should match on `rule` and `path` instead.
20+
- **`os explain <rule-id>` takes these 10 ids**, for example `os explain nav-object-unservable`. It prints the reasoning the verdicts no longer carry: which grants count toward a navigation entry's read access, why an object's `enable` block makes an entry dead for every user and what the served `/meta` does with it, and why a grant and a list are two independent findings; which `defineStack` check `nav-target-unresolved` stands in for; which object each field-name list is judged against and what a miss costs at each position; the object-name resolution ladder and the platform object registry; where a capability may come from; which surfaces bind an action by name and how a related list resolves and places one; what an import mapping target may name, compound-field parts included; the container-only contract of `views:` and the doors that hold it; and the renderer's binding derivation for each interface-page visualization. Paragraphs shared across ids are one text, printed under every id they explain. The `rule:` line under each of these findings now ends with `` — `os explain <rule-id>` for … ``. The no-argument listing and its `--json` `rules` array list the 10 ids, and so does the unknown-id error's `Rules with an explanation:` line. `RULE_EXPLANATIONS` in `@objectstack/lint` gains the 10 entries.
21+
- **Where the new text prints.** On the CLI, all 10 ids: `os validate`, `os build` (and `os compile`, which `os dev` runs on every compile), `os lint`, `os verify`, and the scaffold check `os init` runs print the new `message` on the text face, and `os validate --json` and `os build --json` carry it in their `errors` and author-time `issues`. At the runtime publish gate (Studio, REST `/meta`, MCP): an `object` write's `object-field-ref-unknown` changes the 422 issue's `message` and the refusal log line under `OS_ALLOW_UNLINTED_METADATA_WRITES`, and a `dataset` write's `object-reference-unregistered-platform` (its base `object`) changes the 2xx `advisories` entry and the `[Protocol] authoring advisory` log line. Each issue's `hint` is unchanged.
22+
- **Never at the runtime gate:** `capability-reference-unknown` and `view-container-shape` (CLI-only rules), and the three navigation ids, `action-name-undefined`, `mapping-target-field-unknown` and `page/visualization-without-binding`, whose rules run at that door only for a `flow` write, whose snapshot carries no apps, pages, views or mappings of its own.

0 commit comments

Comments
 (0)