Skip to content

Commit d515b5e

Browse files
fix(lint): one-line fix lines for 18 more author-time rule ids; os explain RULE_ID carries what they explained (#22936)
Part of #22161 Clause-②: no Stage 2 of the card, slice 13: the over-long `fix:` lines, continued in slices 11 and 12's shape. Each finding's `hint` (the CLI's `fix:` line, the runtime issue's `hint`) of 18 more `packages/lint` rule ids, in eight whole 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: 73 author-time ids still carry a hint over 200 characters (the last section). ## What changes - **One remedy line per finding.** The 18 ids, every hint the lint suite fires, before → after (count of distinct hints · length range): | rule id | file | lint suite, before → after | runtime door | |---|---|---|---| | `flow-credential-literal` | `lint-flow-credential-literals.ts` | 3 · 488–646 → 3 · 141–177 | `flow` write, 2xx advisory | | `flow-update-readonly-when-field` | `validate-readonly-flow-writes.ts` | 2 · 531–549 → 2 · 165–174 | `flow` write, 2xx advisory | | `flow-update-readonly-field` | same | 10 · 230–400 → 10 · 107–143 | `flow` write, 422 | | `action-dispatch-contract-mismatch` | `validate-action-dispatch-contract.ts` | 3 · 491–514 → 3 · 124–157 | CLI only | | `security-controlled-by-parent-ambiguous-relation` | `validate-security-posture.ts` | 1 · 496 → 3 · 167–174 | `object` write, 422 | | `security-master-detail-ungranted` | same | 8 · 271–314 → 8 · 124–153 | `object` / `permission` write, 2xx advisory | | `security-controlled-by-parent-no-relation` | same | 1 · 294 → 1 · 194 | `object` write, 422 | | `security-fls-unknown-field` | same | 3 · 276–293 → 4 · 89–132 | `permission` write, 422 | | `security-wildcard-vama` | same | 1 · 218 → 1 · 103 | `permission` write, 422 | | `sharing-rule-object-not-shareable` | `validate-sharing-rule-enforceability.ts` | 3 · 471–482 → 3 · 133–144 | CLI only | | `sharing-rule-object-controlled-by-parent` | same | 2 · 313–395 → 2 · 155–169 | CLI only | | `page/visualization-without-binding` | `validate-page-visualization-bindings.ts` | 14 · 395–480 → 14 · 148–183 | CLI only | | `action-api-update-readonly-when-field` | `validate-readonly-action-writes.ts` | 4 · 458–470 → 4 · 182–188 | CLI only | | `flow-api-trigger-secret-missing` | `validate-flow-trigger-readiness.ts` | 1 · 461 → 1 · 171 | `flow` write, 422 | | `flow-time-relative-descriptor-unroutable` | same | 1 · 441 → 1 · 166 | `flow` write, 422 | | `flow-trigger-unroutable` | same | 1 · 295 → 1 · 163 | `flow` write, 422 | | `flow-trigger-unknown-event` | same | 2 · 191–268 → 2 · 96–101 | `flow` write, 422 | | `flow-trigger-unknown-object` | same | 2 · 179–210 → 2 · 125–156 | `flow` write, 2xx advisory | A count that rises is a line that now names something per finding: the ambiguous-master line names the tier that tied (three tiers fire), and the FLS line names the nearest column (with and without one). - **The reasoning moves, the remedy stays.** - `flow-credential-literal`: each position names the auth variant that holds the credential and the `connector_action` node; the connector's `provider` ('rest'), the Basic variant, where `credentialRef` resolves, that a `{variable}` template draws nothing, that nothing is refused and the connector guide are the explanation's. The variants are held to `ConnectorInstanceAuthSchema` by the rule's test. - `flow-update-readonly-field`: one line per verb (`runAs:'system'`, the create-time `beforeInsert` stamp, removing the field); why the system channel is intended and why a hook-assigned key survives the strip are the explanation's. - `flow-update-readonly-when-field` and `action-api-update-readonly-when-field`: the two routes and the refusal of elevation in one line, the same shape `hook-api-update-readonly-when-field` took in slice 12; why a hook-derived value lands on a locked record, the drop-the-field alternative and "never blocks a build" are the explanations'. - `action-dispatch-contract-mismatch`: both ends in one line (declare the wiring's contract, or rewire to the declaration's); how to pick, and why two wanted wirings are two actions, are the explanation's. - The five security ids: the tier precedence, what each sharing baseline means, the `sys_*` / `isSystem` alternative, what a field rename did to an FLS mask and where an app-level admin set belongs (ADR-0090 D9) are the explanations'. - The two sharing anchor ids: both honest fixes in one line; how to tell which half is wrong (ADR-0049 enforce-or-remove), why re-homing onto another public object fixes nothing, and that the detail rows follow the master are the explanations'. - `page/visualization-without-binding`: the three routes in one line, naming the field TYPE the derivation looks for; why `sourceView` is the one channel on this door and the name fallback are the explanation's (which already writes out the whole derivation, held to `OBJECTUI_DERIVATION_PREDICATES`). - The flow trigger ids: the insert synonym and the missing "any change" token, why multi-event arrays are deferred, the descriptor's optional keys, the schedule's default and its docs page, the signature format and its 300-second window, and the autolaunched alternative are the explanations'. The descriptor keys the line names are held to `TimeRelativeTriggerSchema` by the rule's test. - **A list becomes a nearest name.** `security-fls-unknown-field` listed up to twelve of the object's fields; it now names the nearest declared or registry-injected column when one is close (`nearestName`, the package's one "Did you mean" helper, over the two sets the rule's own resolution accepts) and none otherwise. At the runtime door the old list made the line longer than anywhere in the lint suite (324). - **Two ids gain an explanation entry**, because their reasoning had no entry to move into: `security-wildcard-vama` ("why the '*' entry cannot carry View All / Modify All") and `flow-trigger-unknown-object` ("why an unknown trigger object only warns"). Their `rule:` line now ends with the `os explain` pointer, which `explainPointer` prints for every id the table holds. Both constants were already exported, so the barrel is unchanged. The other 16 entries gain paragraphs. - **Nothing else moves.** Every `message`, rule id, severity and `path`, and what each rule accepts or refuses, is unchanged: every hunk in the eight rule files is a `hint` expression, a comment, a deleted hint-only constant (`RESOLVES`, `NOT_A_GATE`, and the FLS `roster`), the nearest-name computation that feeds the FLS hint, or `describeSearch` dropping its name-pattern clause (its only caller is the hint). No condition, branch, skip or dedupe moved. - `.changeset/22161-lint-slice-13-one-line-fix.md`: `@objectstack/lint` `patch`, `Clause-②: no`, with the runtime `hint` FROM → TO for every id that fires at the runtime gate. ## The runtime wire (Zone 2 item 3) Measured at the door with `runRuntimeAuthoringRules` on the base dist and on the rebuilt one (a scratch probe, 22 writes): - a `flow` write: `flow-credential-literal` (2xx `advisories[].hint`, header / query / connector input 646 / 528 / 488 → 173 / 177 / 141), `flow-update-readonly-field` (422 `issues[].hint`, update / create 239 / 396 → 116 / 139), `flow-update-readonly-when-field` (2xx advisory, 527 → 161, under `runAs: 'user'` and `'system'`), `flow-trigger-unknown-object` (2xx advisory, record / time-relative 210 / 179 → 156 / 125), `flow-trigger-unknown-event` (422, token / array 191 / 268 → 101 / 96), `flow-time-relative-descriptor-unroutable` (422, 441 → 166, alone and beside a schedule), `flow-trigger-unroutable` (422, 295 → 163, an off-chain token and none) and `flow-api-trigger-secret-missing` (422, 461 → 171); - an `object` write: `security-controlled-by-parent-no-relation` (422, 294 → 194), `security-controlled-by-parent-ambiguous-relation` (422, 496 → 167) and `security-master-detail-ungranted` (2xx advisory, 304 → 147); - a `permission` write: `security-fls-unknown-field` (422, 324 → 132), `security-wildcard-vama` (422, 218 → 103) and `security-master-detail-ungranted` (2xx advisory, 304 → 147); - no write reaches `action-dispatch-contract-mismatch`, `page/visualization-without-binding`, `action-api-update-readonly-when-field` or the two `sharing-rule-object-*` ids (a `view` bulk wiring, a list `page`, a `sharing_rule` and an `action` body write were probed): `validateSharingRuleEnforceability` is CLI-only, and the other three are reference-suite members on the frozen `flow` default whose collections a flow snapshot does not carry. Readers of `hint`, read-only against objectui `main` at `2498a075` (fetched into a ref of its own, `git grep`, no edit; the ref deleted after): Studio's `saveAdvisoryToast.ts` `formatFinding` renders `[rule] where — message hint` for every 2xx advisory, so the credential, readonlyWhen, unknown-object and ungranted-detail toasts shorten. `DraftChangesPanel.tsx` renders `p.hint || p.message` for the in-browser security-posture pass (`securityPostureLint.ts`, which runs `validateSecurityPosture` from the installed `@objectstack/lint` over object drafts and keeps `error` findings), so for `security-controlled-by-parent-no-relation` and `-ambiguous-relation` the panel shows the new line in place of the message once objectui takes this release. `metadata-client.ts` checks only that `hint` is a string, and the 422 `issues` renderers read `path` and `message`. REST (`PUT /api/v1/meta`) forwards `issues` / `advisories` as JSON; `packages/mcp` has no metadata write tool, so an MCP or AI author reaches the gate through REST. In this repository `toIssue` forwards `hint` verbatim and the advisory log line prints it; the one test outside `packages/lint` that reads one of these hints (`runtime-authoring-gate.flow-credential-literal.test.ts`, `toContain('credentialRef')`) holds. **No reader in either repository matches on `hint` text.** `check:watch-hint-literal` guards something else of the same name (every `*_WATCH_HINTS` population declaration in a gate script is a literal array): exit 0 standalone (72 declarations across 4 rostered names) and inside the gate set. ## Census (taken first, before any edit, at the base `7dbb89dc5d`) Method: the earlier slices' scratch preload (`NODE_OPTIONS=--import`, never committed) patching `Array.prototype.push` to record every finding-shaped object, deduped by (rule, hint) per process, with its push site, appended per new key so a worker killed at teardown loses nothing. Lengths are the `hint` alone. Runs, each before and after: - **`packages/lint` suite** (137 files; 6,529 tests at the base): the 18 ids re-measured at the base equal the claim's lengths exactly (646, 549, 400, 514, 496, 314, 294, 293, 218, 482, 395, 480, 470, 461, 441, 295, 268, 210). After, at `ec4f1802f`: **97 ids over 200** = 21 of the 26 fenced ids, the 3 repo-gate ids (`lint-startup-registry-verdict.ts`) and **73 author-time ids** (slice 12's 91 less these 18; every one at slice 12's length). The first "before" run recorded only `hint`-shaped findings; the after run also records the `fix`-shaped findings `data-model-rules.ts` builds (`object/missing-name-field` 327, `rollup/non-numeric-aggregand` 282, `field/select-missing-options` 207), files this slice does not touch, so the comparable before is 115. - **Runtime publish gate**, the whole `@objectstack/metadata-protocol` suite reading `@objectstack/lint` from its built dist (base dist before, rebuilt after): `flow-credential-literal` (3 · 488–646 → 3 · 141–177), `security-master-detail-ungranted` (299 → 146), `flow-api-trigger-secret-missing` (461 → 171) and `flow-trigger-unknown-object` (210 → 156) of the 18. - **`packages/cli` unit tier** (282 files): none of the 18 fires, before or after. - **`os validate`** (the built CLI) on `app-crm`, `app-todo`, `app-showcase`, `app-multi-package`, exit 0 each, before and after: none of the 18 fires; each prints the same 9 / 7 / 71 / 3 `rule:` lines, and the output is byte-identical once the two millisecond timings (`Load time`, `Validation passed (…ms)`) are normalised — those four lines per app are the only difference. ## Tests Every heavy run went through `scripts/pm/os-verify-lock.sh` (slot `issue-22161-s13`); its VERDICT lines are in the report. - **Each rule's own suite pins the new shape.** Every touched suite already records what its cases fire; a new last block per file holds every recorded `fix:` line of its converted ids to one line of at most 200 characters behind a coverage control (each arm fired: the three credential positions; both readonly verbs; both dispatch directions; all five security ids, the FLS id with and without a nearest column; both sharing anchor ids, the detail id with and without a resolved master; every visualization's type half; every flow trigger arm), plus a check per id that `explainRule(id)` holds what the line stopped saying. Pins that read the old hint text moved to the explanation, text-only (the readonlyWhen flow pins' "Elevation is not a workaround here" / "does land, even on a locked record", the array-trigger pin's "multi-event arrays are deferred"). Three new pins hold an explanation's spelled-out list to its source of truth: the auth variants to `ConnectorInstanceAuthSchema`, the descriptor keys to `TimeRelativeTriggerSchema`, and the FLS nearest name across declared and injected columns. - **Lint suite** at `ec4f1802f`: Test Files 137 passed (137), Tests 6,561 passed (6,561); VERDICT command-exit 0 (6,529 at the base). - **Lint build + typecheck:** `pnpm --filter @objectstack/lint build` (`check-dts-emitted` 6/6) and `run typecheck` (`check:test-typecheck` OK, 2 files / 6 errors / 2 pinned signatures held), VERDICT command-exit 0 each. The rebuilt dist carries the new text (`rewire it as` and `is the nearest)` 1 hit each in `index.js`, `index.cjs`, `runtime.js`, `runtime.cjs`; the old `Pick the contract the body` and `Object names match exactly. Check` 0). - **The built entries' export closure is unchanged** (Clause-② check): the runtime export keys and the declaration exports of `index`, `runtime` and `rule-explanations`, read from the base dist and from the rebuilt one, are identical — 370 / 552, 6 / 11 and 2 / 3 names, 0 added and 0 removed. - **CLI unit tier** on the rebuilt dist: Test Files 282 passed (282), Tests 4,262 passed (4,262), VERDICT command-exit 0 (the same before). It includes `explain-rule-id.test.ts`, which runs every `RULE_EXPLANATIONS` entry through `os explain`, the two new ones included. No `packages/cli` or `packages/metadata-protocol` test asserts these ids' hint text beyond the `credentialRef` pin above, so the declared rider went unused. - **Runtime gate:** the whole `@objectstack/metadata-protocol` suite, after: Test Files 226 passed, 3 skipped (229), Tests 28,171 passed, 19 skipped; VERDICT command-exit 0 (the same before). - **Ablation** (one-shot, from the committed head `ec4f1802f`, 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. `security-wildcard-vama`'s pre-slice 218-character hint was restored as one literal (anchor x1 → x0, blob `ee3feebdbbe9` → `0db2ab189761`). Predicted before the run: exactly the new bound pin goes red, every other case stays green. Observed: `src/validate-security-posture.test.ts` Tests 1 failed, 154 passed (155), exactly that pin, reading 218. Restored: blob `ee3feebdbbe9` == HEAD, `git diff HEAD` empty, `git status --porcelain` empty. - **ESLint, narrowed:** `npx eslint --no-inline-config --format json` over the 17 changed `.ts` files: 17 files in the report, 0 errors, 0 warnings. The population is `eslint.config.mjs`'s `**/*.{ts,…}` 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:** one merge of `origin/main` through `scripts/pm/os-regen-merge.sh` (`407cee32b` → `ec4f1802f`, onto `018b077fb`; no conflict, and the branch edits no generated artifact). `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` with no paths at the final head `ec4f1802f` (18 paths vs the merge base) derived 62 commands; 60 ran, each exit code captured before any pipe, all 0 (longest: `check:query-options-erasure` 336s, `check:slot-lookup` 182s, `check-comment-mask-corpus` 156s). `check-plugin-teardown-shape --self-test` first refused (exit 3: its positive-control fixture is pinned to commit `621a48760788`, unreachable in this shallow clone); after fetching that one commit it ran, exit 0. NOT MEASURED, as dispatched for slice 12: `check:dual-build-cjs-loads` (needs every package's dist) and `check:type-check-debt` (its script is `--re-measure`; `check:type-check-coverage` ran). `--ran`: `Run reconciliation — 62 derived, 60 run, 2 NOT-MEASURED, 0 UNRUN.` The artifact-roster block (51 families): the 48 that need no pull request exited 0 at `ec4f1802f`, the 4 rostered under a touched directory among them (`check-changeset-fixed`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`); `check:published-readme-exports` after building its four missing dists (`client-react`, `embedder-openai`, `knowledge-ragflow`, `organizations`, 54 tasks) under the lock (its first answer was the exit-3 prerequisite refusal, not a verdict); the three PR-context guards run against this PR and report on the card. `origin/main` moved three commits after the merge (`e4ad6776d`, `06cc45dc3`, `85b4ac634`); none touches a file of this slice, and the branch was not merged again. ## 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; the names in a hint (the field and object of the readonly ids, the action name, the master and detail objects, the FLS object and nearest column, the page's object) are the author's. The closest measured cases are `security-controlled-by-parent-no-relation` at 194 (no author text: a constant line) and `action-api-update-readonly-when-field` at 188. - **The FLS field list now prints nowhere.** A near miss gets the nearest column; a far miss gets none, where it used to get up to twelve of the object's fields. The card's "Asked for" names no list, and slices 11 and 12 made the same call for their lists. - **Two `rule:` lines gain the `os explain` pointer** (`security-wildcard-vama`, `flow-trigger-unknown-object`), through the new entries. That is the printer's own rule for an id the table holds, not a printer change. - **Studio's draft panel is a second reader of two security hints.** It shows `hint` in place of `message` for the two controlled-by-parent errors; the new lines read as remedies there, as the old ones did, only shorter. ## Remaining for this card **73 author-time `packages/lint` ids still carry a hint over 200 characters** in the final lint census (at `ec4f1802f`: 97 over 200 = 73 + 21 fenced + 3 repo-gate), by file, longest first: - `lint-flow-patterns.ts` (2): `flow-double-brace-interpolation` 1339, `flow-bare-dollar-reference` 1231 - `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-searchable-fields.ts` (3): `searchable-field-unknown` 448, `searchable-field-unprovisioned` 362, `searchable-field-unsearchable` 328 - `validate-flow-filter-tokens.ts` (1): `flow-filter-token-unknown` 427 - `validate-action-locations.ts` (1): `action-no-placement` 424 - `validate-mapping-target-fields.ts` (1): `mapping-target-field-unknown` 405 - `validate-dashboard-widget-options.ts` (1): `unconsumed-widget-option` 396 - `validate-sortable-fields.ts` (3): `sort-field-unprovisioned` 378, `sort-field-unsortable` 332, `sort-field-unknown` 301 - `validate-visibility-predicates.ts` (3): `visibility-predicate-unknown-function` 375, `visibility-bare-identifier` 221, `visibility-predicate-syntax` 214 - `validate-nav-access.ts` (1): `nav-object-ungranted` 370 - `validate-hook-body-writes.ts` (3): `hook-body-write-unprovisioned-anchor` 365, `hook-body-write-unknown-field` 275, `hook-body-source-unparseable` 254 - `validate-ai-agent-authoring.ts` (2): `agent-authoring-withdrawn` 363, `default-agent-outside-roster` 237 - `validate-react-page-props.ts` (4): `react-chart-field-unprovisioned` 362, `react-prop-missing-required` 277, `react-page-source-unparseable` 254, `react-block-needs-record-context` 228 - `validate-translatable-sections.ts` (1): `translation-section-name-missing` 359 - `validate-action-body-writes.ts` (4): `action-body-write-unprovisioned-anchor` 358, `action-body-write-unknown-field` 323, `action-record-write-discarded` 309, `action-body-source-unparseable` 254 - `validate-flow-node-writes.ts` (2): `flow-node-write-unprovisioned-anchor` 358, `flow-node-write-unknown-field` 289 - `validate-managed-api-methods.ts` (1): `object/managed-api-method-unaffordable` 357 - `validate-nav-object-servability.ts` (1): `nav-object-unservable` 348 - `validate-rule-compilability.ts` (2): `validation-rule-json-schema-uncompilable` 341, `validation-rule-regex-uncompilable` 300 - `validate-dataset-references.ts` (2): `dataset-include-unknown` 336, `dataset-field-unknown` 206 - `validate-translation-references.ts` (1): `translation-target-unknown` 317 - `validate-approval-approvers.ts` (3): `approval-approver-not-membership-tier` 313, `approval-expression-no-empty-policy` 294, `approval-expression-invalid` 292 - `validate-seed-replay-safety.ts` (1): `seed-insert-mode-duplicates-on-replay` 309 - `validate-chart-bindings.ts` (1): `chart-measure-unknown` 285 - `validate-object-field-refs.ts` (1): `object-field-ref-unknown` 284 - `validate-capability-references.ts` (1): `capability-reference-unknown` 273 - `validate-record-title.ts` (2): `title-format-retired` 267, `title-unresolvable` 237 - `validate-list-view-field-refs.ts` (2): `list-view-field-unknown` 258, `list-view-field-dotted` 233 - `validate-view-containers.ts` (1): `view-container-shape` 256 - `validate-seed-state-machine.ts` (1): `seed-value-outside-state-machine` 251 - `validate-ai-surface-affinity.ts` (1): `ai-skill-surface-mismatch` 235 - `validate-page-source-styling.ts` (1): `page-source-className-tailwind` 231 - `lint-liveness-properties.ts` (1): `liveness-planned-property` 230 - `validate-list-view-mode.ts` (1): `list-view-filters-in-views-mode` 219 Not in the 73: the 26 fenced ids as PR #22448 lists them (21 fire a hint over 200), the 3 repo-gate ids (`startup-open-vocabulary-verdict`, `startup-source-unparseable`, `startup-verdict-assertive-wording`), and the `packages/cli` ids on #22841. --- _Generated by [Claude Code](https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 475d2b1 commit d515b5e

18 files changed

Lines changed: 667 additions & 141 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): 18 more author-time rule ids print a `fix:` line of one remedy sentence, and `os explain RULE_ID` carries what those lines used to explain
6+
7+
Clause-②: no
8+
9+
- **Shorter fix lines.** Each finding of these 18 rule ids now carries a `hint` (the CLI's `fix:` line, the runtime issue's `hint`) of one remedy sentence of at most 200 characters on every variant the lint suite fires (the lengths here are the `hint` alone; the CLI prints it after `fix: `). The longest of each, before → after:
10+
- flow credentials: `flow-credential-literal` (646 → 177);
11+
- flow writes: `flow-update-readonly-when-field` (549 → 174) and `flow-update-readonly-field` (400 → 143);
12+
- list-view bulk wiring: `action-dispatch-contract-mismatch` (514 → 157);
13+
- security posture: `security-controlled-by-parent-ambiguous-relation` (496 → 174), `security-master-detail-ungranted` (314 → 153), `security-controlled-by-parent-no-relation` (294 → 194), `security-fls-unknown-field` (293 → 132) and `security-wildcard-vama` (218 → 103);
14+
- sharing rules: `sharing-rule-object-not-shareable` (482 → 144) and `sharing-rule-object-controlled-by-parent` (395 → 169);
15+
- interface pages: `page/visualization-without-binding` (480 → 183);
16+
- action bodies: `action-api-update-readonly-when-field` (470 → 188);
17+
- flow triggers: `flow-api-trigger-secret-missing` (461 → 171), `flow-time-relative-descriptor-unroutable` (441 → 166), `flow-trigger-unroutable` (295 → 163), `flow-trigger-unknown-event` (268 → 101) and `flow-trigger-unknown-object` (210 → 156).
18+
- **One line, one remedy.** Where a fix line explained its remedy, the explanation moved: why `runAs: 'system'` is the channel for a static readonly field and why a `beforeInsert` stamp survives the strip; why a hook-derived value lands on a `readonlyWhen`-locked record, the drop-the-field alternative and that the warning never blocks a build; how to pick between the two bulk contracts; the controlled-by-parent tier precedence and what each sharing baseline means; how to tell which half of a sharing rule on a public object is wrong; why `sourceView` is an interface page's one binding channel; the descriptor's optional keys and the schedule's default; the api trigger's signature format; the connector's `provider`, the Basic auth variant, where `credentialRef` resolves and that a `{variable}` template draws nothing.
19+
- **A list becomes a nearest name.** `security-fls-unknown-field` no longer lists up to twelve of the object's fields; it names the nearest declared or registry-injected column when one is close (`'crm_account.description' is the nearest`), and none otherwise. `page/visualization-without-binding` names the field TYPE the derivation looks for and no longer repeats the name pattern it also tries.
20+
- **`os explain RULE_ID` carries the rest.** What the fix lines stopped saying is each id's `RULE_EXPLANATIONS` entry in `@objectstack/lint`, which `os explain RULE_ID` prints. Two ids had no entry and gain one, so their `rule:` line now ends with the `os explain` pointer: `security-wildcard-vama` ("why the '*' entry cannot carry View All / Modify All") and `flow-trigger-unknown-object` ("why an unknown trigger object only warns"). The other 16 entries gain paragraphs. The auth variants and the descriptor keys the lines and entries spell out are held to the spec's `ConnectorInstanceAuthSchema` and `TimeRelativeTriggerSchema` by the rules' tests.
21+
- **At the runtime publish gate** (Studio, REST `PUT /api/v1/meta`, and the MCP and AI authors that write through that door), 13 of the 18 fire; the `hint` FROM → TO, measured at the door:
22+
- a `flow` write: `flow-credential-literal` (2xx `advisories[].hint`, header / query / connector input 646 / 528 / 488 → 173 / 177 / 141), `flow-update-readonly-field` (422 `issues[].hint`, update / create 239 / 396 → 116 / 139), `flow-update-readonly-when-field` (2xx advisory, 527 → 161, under `runAs: 'user'` and `'system'` alike), `flow-trigger-unknown-object` (2xx advisory, record / time-relative 210 / 179 → 156 / 125), `flow-trigger-unknown-event` (422, token / array 191 / 268 → 101 / 96), `flow-time-relative-descriptor-unroutable` (422, 441 → 166), `flow-trigger-unroutable` (422, 295 → 163) and `flow-api-trigger-secret-missing` (422, 461 → 171);
23+
- an `object` write: `security-controlled-by-parent-no-relation` (422, 294 → 194), `security-controlled-by-parent-ambiguous-relation` (422, 496 → 167) and `security-master-detail-ungranted` (2xx advisory, 304 → 147);
24+
- a `permission` write: `security-fls-unknown-field` (422, 324 → 132), `security-wildcard-vama` (422, 218 → 103) and `security-master-detail-ungranted` (2xx advisory, 304 → 147).
25+
26+
Studio's save-advisory toast prints an advisory's `hint` after its message, so the credential, readonlyWhen, unknown-object and ungranted-detail toasts shorten, and Studio's draft-changes panel shows the `hint` in place of the message for the two controlled-by-parent errors its in-browser security check finds on object drafts. No Studio, REST or MCP reader in this repository or in objectui matches on `hint` text; a tool that did should match on `rule` and `path` instead.
27+
- **Never at the runtime gate:** `action-dispatch-contract-mismatch`, `page/visualization-without-binding`, `action-api-update-readonly-when-field` and the two `sharing-rule-object-*` ids run on the CLI only.
28+
- **Unchanged:** every `message` (the verdict), rule id, severity and `path`, and what each rule accepts or refuses.

‎packages/lint/src/lint-flow-credential-literals.test.ts‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import {
3131
FLOW_CREDENTIAL_LITERAL,
3232
lintFlowCredentialLiterals as lintFlowCredentialLiteralsUnrecorded,
3333
} from './lint-flow-credential-literals.js';
34+
import { ConnectorInstanceAuthSchema } from '@objectstack/spec/integration';
35+
3436
import { explainRule } from './rule-explanations.js';
3537
import { runRuntimeAuthoringRules, runtimeAuthoringRulesFor } from './runtime-gate.js';
3638
import * as barrel from './index.js';
@@ -41,7 +43,7 @@ import * as barrel from './index.js';
4143
// verdict to one line of at most 200 characters — every firing variant this
4244
// suite exercises, not a chosen few. Run the whole file: those cases read what
4345
// the cases above fired.
44-
const fired: Array<{ rule: string; message: string }> = [];
46+
const fired: Array<{ rule: string; message: string; hint: string }> = [];
4547
const lintFlowCredentialLiterals: typeof lintFlowCredentialLiteralsUnrecorded = (...args) => {
4648
const findings = lintFlowCredentialLiteralsUnrecorded(...args);
4749
fired.push(...findings);
@@ -474,3 +476,55 @@ describe('[#22161] one-line verdicts — flow-credential-literal', () => {
474476
}
475477
});
476478
});
479+
480+
// [#22161] Slice 13: the `fix:` line. Each position's hint is one remedy line:
481+
// the auth variant that holds the credential and the node that calls the
482+
// connector. Where `credentialRef` resolves, the connector's `provider`, the
483+
// Basic variant, that a template draws nothing and that nothing is refused are
484+
// the explanation's.
485+
describe('[#22161] one-line fixes — flow-credential-literal', () => {
486+
it('every `fix:` line the cases in this file fired is one line of at most 200 characters', () => {
487+
const converted = fired.filter((f) => f.rule === FLOW_CREDENTIAL_LITERAL);
488+
// Coverage control: all three positions fired, each with its own route.
489+
for (const opening of ['header ', 'url query parameter ', 'connector input ']) {
490+
expect(converted.some((f) => f.message.startsWith(opening)), opening).toBe(true);
491+
}
492+
expect(new Set(converted.map((f) => f.hint)).size).toBe(3);
493+
for (const f of converted) {
494+
expect(f.hint, f.rule).not.toContain('\n');
495+
expect(f.hint.length, `${f.rule}: ${f.hint}`).toBeLessThanOrEqual(200);
496+
}
497+
});
498+
499+
it('`os explain` carries what the `fix:` line no longer says', () => {
500+
const text = explainRule(FLOW_CREDENTIAL_LITERAL)!.paragraphs.join('\n');
501+
for (const fact of [
502+
'resolved at boot (an environment variable in the open tier)',
503+
"names a `provider` ('rest' for a plain HTTP API)",
504+
"`auth: { type: 'basic', username, credentialRef }`",
505+
'A `{variable}` template is not a literal and draws nothing',
506+
'nothing is refused',
507+
'content/docs/automation/connectors.mdx',
508+
]) {
509+
expect(text, `explanation names ${fact}`).toContain(fact);
510+
}
511+
});
512+
513+
it('every auth variant the `fix:` lines and the explanation name is one the connector auth schema accepts', () => {
514+
// The explanation module imports nothing, so the variants it and the
515+
// `fix:` lines spell out are held to the spec's schema here.
516+
for (const auth of [
517+
{ type: 'bearer', credentialRef: 'PROBE_REF' },
518+
{ type: 'api-key', headerName: 'X-Api-Key', credentialRef: 'PROBE_REF' },
519+
{ type: 'api-key', paramName: 'api_key', credentialRef: 'PROBE_REF' },
520+
{ type: 'basic', username: 'probe', credentialRef: 'PROBE_REF' },
521+
]) {
522+
expect(ConnectorInstanceAuthSchema.safeParse(auth).success, JSON.stringify(auth)).toBe(true);
523+
}
524+
// The connector-input line offers every secret-bearing variant by name.
525+
const input = lintFlowCredentialLiteralsUnrecorded(controlStack()).find((f) =>
526+
f.message.startsWith('connector input '),
527+
)!;
528+
expect(input.hint).toContain("type: 'bearer' | 'api-key' | 'basic', credentialRef");
529+
});
530+
});

‎packages/lint/src/lint-flow-credential-literals.ts‎

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -90,32 +90,24 @@ const SERVED = 'readable by every member who can read flows';
9090
const STEER_CONNECTOR = "move it to a connector's `auth.credentialRef`";
9191
const STEER_INPUT = 'drop it: the connector authenticates through `auth.credentialRef`';
9292

93-
/** Where `credentialRef` resolves — stated once, in every hint. */
94-
const RESOLVES =
95-
' `credentialRef` names a secrets-layer reference that is resolved at boot (an environment variable in ' +
96-
'the open tier), so the secret never lands in metadata.';
97-
93+
/**
94+
* [#22161] One remedy line per position: the auth variant that holds the
95+
* credential and the node that calls the connector. Where `credentialRef`
96+
* resolves, the connector's `provider`, the Basic variant, that a `{variable}`
97+
* template draws nothing, that nothing is refused and the connector guide are
98+
* `os explain flow-credential-literal`.
99+
*/
98100
const HEADER_ROUTE =
99-
"Declare a `connectors:` entry with a `provider` (`'rest'` for a plain HTTP API) and " +
100-
"`auth: { type: 'bearer', credentialRef }` for a bearer token, or " +
101-
"`auth: { type: 'api-key', headerName, credentialRef }` for a key in a named header (a Basic pair is " +
102-
"`auth: { type: 'basic', username, credentialRef }`), and call it from a `connector_action` node." +
103-
RESOLVES;
101+
"Declare a `connectors:` entry with `auth: { type: 'bearer', credentialRef }` (or " +
102+
"`{ type: 'api-key', headerName, credentialRef }`) and call it from a `connector_action` node";
104103

105104
const QUERY_ROUTE =
106-
"Declare a `connectors:` entry with a `provider` (`'rest'` for a plain HTTP API) and " +
107-
"`auth: { type: 'api-key', paramName, credentialRef }`, which sends the key as that query parameter, and " +
108-
'call it from a `connector_action` node.' +
109-
RESOLVES;
105+
"Declare a `connectors:` entry with `auth: { type: 'api-key', paramName, credentialRef }`, which sends the " +
106+
'key as that query parameter, and call it from a `connector_action` node';
110107

111108
const INPUT_ROUTE =
112-
'The connector authenticates through its own declaration: give its `connectors:` entry ' +
113-
"`auth: { type: 'bearer' | 'api-key' | 'basic', credentialRef }` and drop the credential from `input`." +
114-
RESOLVES;
115-
116-
const NOT_A_GATE =
117-
' A `{variable}` template is not a literal and draws nothing. This is an advisory: nothing is ' +
118-
'refused. See content/docs/automation/connectors.mdx.';
109+
"Give the connector's `connectors:` entry `auth: { type: 'bearer' | 'api-key' | 'basic', credentialRef }` " +
110+
'and drop the credential from `input`';
119111

120112
/** A config-path segment for a map key: `.key` when it is an identifier, `["x-key"]` otherwise. */
121113
function keySegment(key: string): string {
@@ -173,7 +165,7 @@ export function lintFlowCredentialLiterals(stack: AnyRec): FlowCredentialLiteral
173165
// auth variant and why it only warns are
174166
// `os explain flow-credential-literal`.
175167
message: `${what} holds a credential-shaped literal, ${SERVED}; ${steer}`,
176-
hint: `${route}${NOT_A_GATE}`,
168+
hint: route,
177169
});
178170
};
179171

0 commit comments

Comments
 (0)