Skip to content

Commit 2e11d90

Browse files
fix(lint): one-line fix lines for 15 author-time rule ids; os explain RULE_ID carries what they enumerated (#22878)
Part of #22161 Clause-②: no Stage 2 of the card, slice 11: the over-long `fix:` lines, item 3 of slice 10's landing record as triage routed it. Each finding's `hint` (the CLI's `fix:` line, the runtime issue's `hint`) of 15 `packages/lint` rule ids is now one remedy sentence of at most 200 characters, and what the lines used to enumerate is each id's `RULE_EXPLANATIONS` entry behind `os explain RULE_ID`. The card stays open: 107 author-time ids still carry a hint over 200 characters (below). ## What changes - **One remedy line per finding.** The 15 ids, the longest hint the lint suite fires, before → after: | rule id | file | lint suite: hints · range, before → after | runtime door | |---|---|---|---| | `sharing-rule-runtime-variable-condition` | `validate-sharing-rule-enforceability.ts` | 1 · 2,280 → 1 · 195 | CLI only | | `sharing-rule-unlowerable-condition` | same | 3 · 387–968 → 3 · 172–199 | CLI only | | `approval-approvers-may-resolve-empty` | `validate-approval-approvers.ts` | 2 · 440–2,243 → 2 · 198–200 | `flow` write, 2xx advisory | | `rls-predicate-unenforceable` | `validate-rls-predicate-enforceability.ts` | 27 · 191–1,056 → 27 · 62–191 | `permission` write, 422 | | `rls-predicate-over-budget` | same | 3 · 1,037–1,044 → 1 · 197 | `permission` write, 422 | | `rls-predicate-unknown-field` | same | 5 · 651–766 → 1 · 154 | `permission` write, 422 | | `filter-empty-combinator` | `validate-empty-combinators.ts` | 3 · 647–764 → 3 · 125–154 | `flow` / `report` write, 422 | | `permission-retired-lifecycle-residue` | `validate-retired-permission-residue.ts` | 2 · 573–681 → 1 · 14 | `permission` write, 2xx advisory | | `visibility-predicate-over-budget` | `validate-visibility-predicates.ts` | 2 · 645–651 → 2 · 177–179 | `view` write, 422 | | `hook-api-update-readonly-field` | `validate-readonly-hook-writes.ts` | 8 · 604–647 → 8 · 153–182 | CLI only | | `react-chart-aggregate-invalid` | `validate-react-page-props.ts` | 3 · 111–363 → 3 · 113–191 | CLI only | | `react-chart-drilldown-invalid` | same | 2 · 114–128 → 2 · 128–181 | CLI only | | `flow-time-relative-descriptor-invalid` | `validate-flow-trigger-readiness.ts` | 1 · 342 → 1 · 176 | `flow` write, 422 | | `component-type-unknown` | `validate-component-types.ts` | 16 · 32–229 → 16 · 32–158 | CLI only | | `field-no-consumers` | `validate-field-consumers.ts` | 15 · 68–189 → 16 · 68–175 (showcase `os validate`: 24 · 68–624 → 24 · 68–199) | CLI only | A count that falls is several old hints that differed only in what the new line no longer lists (the over-budget bound's name, the object's field list, the retired key's name). The drill-down hint grows: it carried a pointer, and now carries the remedy. - **The named five (slice 8's four, plus slice 10's).** - `sharing-rule-runtime-variable-condition` (2,280): the hint was the whole mechanism essay. It is now "keep only the record's own properties in `condition` and name the audience in `sharedWith`; what replaces `current_user` depends on the object's sharingModel". ⛔ The one-line hint recommends no RLS at all, because an RLS policy narrows only `public_read` / `public_read_write` and grants nothing on `private`. The model-qualified split, the AND-composition, the ADR-0057 D1 depth scopes, the `position` over-broad-grant trap and the record-relative gap are the explanation's, with the #14234 wording legs moved there (an RLS sentence must name the models it holds for) and a new leg that holds the hint to naming no RLS. - Slice 5's three "above" hints, which pointed at verdict text slice 5 cut: `react-chart-drilldown-invalid` and `react-chart-aggregate-invalid` ("the rejection above carries the fix") now say "use the rename it suggests, or delete / move a key real on another surface / one layer out"; `flow-time-relative-descriptor-invalid` ("satisfies each message above") now says to start with the key the verdict names and re-run until the schema accepts it. - `component-type-unknown`'s "Apply the prescription above": the retired arm now carries the prescription's own "Delete the …" sentence, cut (never rewritten) by `retiredTypeRemedy()` from `RETIRED_PAGE_COMPONENT_TYPES`, at its first dash only when the sentence would pass 200 (for `element:form`: "Delete the `element:form` component and use the object-bound `object-form` block instead"). - `approval-approvers-may-resolve-empty`'s `manager` arm (2,243 on `main` after #22850): "Set every submitter's sys_user.manager_id (Setup → Users → Set Manager, or the admin user import), or add an approver that cannot resolve empty". `MANAGER_ONLY_REMEDY` / `MANAGER_ONLY_ROUTES` move, verbatim and under the same names, into the id's explanation; the rule file keeps the measurement docblock behind each graded route. The group arm (463) is one line too. - `field-no-consumers`' carrier list (629 printed, 624 as the hint): the first carrier site and `(and N more)`, the shape its verdict's same-name clause already uses. - **Enumerations cut to the first item and a count, elsewhere.** `rls-predicate-unenforceable` rewrites the first faulty site and counts the rest; for a bare `current_user` it names the key slot instead of listing every kernel key. `rls-predicate-unknown-field` drops the object's field list (the verdict's "Did you mean" names the nearest column). `permission-retired-lifecycle-residue` carries the tombstone prescription's "Delete the key" sentence, cut by `retiredKeyRemedy()` (the clause after its dash, on what stays denied, goes to the explanation), never retyped. - **The explanations gain paragraphs; no entry is added or removed.** The lowerable subset and its traps, the comparison classes with their types (a new shared `CROSS_CLASS_LISTING` paragraph, held equal to `CROSS_FIELD_CLASS_LISTING` by both rules' cross-class tests), the rule each `current_user` holding breaks, the null-comparand rewrites, the four ways to shrink an RLS predicate (with the top-level `&&` warning kept on the one-line hint as well), the declared match-nothing filter spelling, the drop-the-field alternatives for a readonly hook write, and what deleting a retired permission key leaves. `CROSS_CLASS_REMEDY` (module export, not on the barrel) is gone; each rule's line names its own alternatives. - **Nothing else moves.** Every `message`, rule id, severity and `path`, and what each rule accepts or refuses, is unchanged: every hunk in the 11 rule files is a `hint` expression, a comment, a hint constant, or a string-in / string-out cut helper (`retiredTypeRemedy`, `retiredKeyRemedy`, `firstPathAndCount`, `firstAndCount`); `typeFaultRewrite` keeps one rewrite per site and `kernelKeysHolding` (which fed only the removed alternatives) is deleted. No condition, branch, skip or dedupe moved. - `.changeset/22161-lint-slice-11-one-line-fix.md`: `@objectstack/lint` `patch`, `Clause-②: no`, naming every door that prints `fix:` and the runtime `hint` FROM → TO. ## The runtime wire (Zone 2 item 4) Measured at the door with `runRuntimeAuthoringRules` on the base dist and on the rebuilt one (scratch probe): - a `flow` write: `flow-time-relative-descriptor-invalid` (422 `issues[].hint`, 342 → 176), `filter-empty-combinator` (422, 647 / 764 → 134 / 154), `approval-approvers-may-resolve-empty` (2xx `advisories[].hint` and the `[Protocol] authoring advisory` log line, 2,243 / 440 → 198 / 200); - a `permission` write: `rls-predicate-unenforceable` (422, 912 / 312 → 186 / 121), `rls-predicate-unknown-field` (422, 708 → 154), `permission-retired-lifecycle-residue` (2xx advisory, 573 / 681 → 14); `rls-predicate-over-budget` runs on that write but fires only once the platform CEL bounds refuse (the suite holds it with `atGa`), so the probe drew no finding either side; - a `view` write: `visibility-predicate-over-budget` (422, 651 → 179); - a `hook` write runs none of the 15; the sharing-rule, component-type, react-chart, `field-no-consumers` and `hook-api-update-readonly-field` ids are CLI only. Readers of `hint`, measured read-only against objectui `main` at `dca25af` (`git grep`, no edit): Studio's `saveAdvisoryToast.ts` `formatFinding` renders `[rule] where — message hint` for every 2xx advisory, and `DraftChangesPanel.tsx` renders `p.hint || p.message` for the in-browser security-posture lint (none of the 15). `metadata-client.ts` `readSaveAdvisories` checks only that `hint` is a string. The 422 `issues` renderers (`ResourceEditPage.tsx`) read `path` and `message`. REST (`PUT /api/v1/meta`, `SaveMetaItemResponseSchema.advisories`, the error envelope's `issues`) forwards the array as JSON; `packages/mcp` has no metadata write tool, so an MCP or AI author reaches the gate through REST. **No reader in this repository or in objectui matches on `hint` text**; the one text match in this repository's sources (`check-yaml-examples.ts`) reads an unrelated object. `check:watch-hint-literal` guards something else of the same name: that every `*_WATCH_HINTS` population declaration in a gate script is a literal array (a computed one drops out of `dispatch-gates`). It ran green before the gate set (72 declarations across 4 rostered names) and inside it. ## Census (taken first, before any edit, at the base `31b5a5f7f5`) Method: slices 4 to 10's scratch preload (`NODE_OPTIONS=--import`, never committed), patching `Array.prototype.push` to record every finding-shaped object, here deduped by (rule, hint) per process, with its push site. Lengths are the `hint` alone. Runs, each before and after: - **`packages/lint` suite** (137 files, 6,455 tests at the base): 240 ids fire; **144 carry a hint over 200**: 21 of the 26 fenced ids, 3 repo-gate ids (`lint-startup-registry-verdict.ts`), and **120 author-time ids**. The named five's 7 ids and the 8 longest others (excluding the two `lint-flow-patterns.ts` ids, below) are this slice's 15. - **Runtime publish gate**, the whole `@objectstack/metadata-protocol` suite reading `@objectstack/lint` from its built dist (base dist before, rebuilt after): fires `rls-predicate-unenforceable` (300 → 139) and `permission-retired-lifecycle-residue` (573–681 → 14) of the 15. - **`packages/cli` unit tier** (281 files): fires `hook-api-update-readonly-field` (622 → 175) and `field-no-consumers`' unchanged inert line (68). - **`os validate`** (the built CLI) on `app-crm`, `app-todo`, `app-showcase`, `app-multi-package`, exit 0 each, before and after: `field-no-consumers` crm 68–336 → 68–179, todo 263–339 → 168–196, showcase 68–624 → 68–199, multi-package 68 → 68; `approval-approvers-may-resolve-empty` fires on the showcase, 440–2,243 → 198–200 (FROM measured on `main` after #22850). Each app prints the same number of `rule:` lines before and after (9 / 7 / 70 / 3). ## Tests Every heavy run went through `scripts/pm/os-verify-lock.sh`; its VERDICT lines are in the report. - **Each rule's own suite pins the new shape.** Every touched suite records what its cases fire; a new last block per id holds every recorded `fix:` line of the converted ids to one line of at most 200 characters behind a coverage control (each arm fired: the three unlowerable arms across the main file and its two siblings; both empty-slate arms; the shape, type-fault and null arms; all three combinator shapes; both retired keys; both readonly-hook verbs; every retired component type and the own-namespace arm; the counted carrier arm), plus exact pins per arm and a pin 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 four sibling files' bounds are scoped to the converted ids (the anchor and `unknown-user-variable` / `unparseable` ids keep longer hints). - **Lint suite** at the final head: Test Files 137 passed (137), Tests 6,496 passed (6,496); VERDICT command-exit 0. - **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. The rebuilt dist carries the new text (`Staff at least one target` 1 hit in each of `index.js`, `index.cjs`, `runtime.js`, `runtime.cjs`; `Apply the prescription above` 0). - **CLI unit tier** on the rebuilt dist: Test Files 281 passed (281), Tests 4,196 passed (4,196); VERDICT command-exit 0. No `packages/cli` or `packages/metadata-protocol` test asserts these ids' hint text, so the declared rider went unused; the runtime-gate test that reads the residue hint asserts its length over 10 (`Delete the key` is 14). - **Runtime gate:** the whole `@objectstack/metadata-protocol` suite, after: Test Files 225 passed, 3 skipped (228), Tests 28,131 passed, 19 skipped; VERDICT command-exit 0. - **Corpus pins over `packages/lint/src`:** `pnpm --filter @objectstack/spec test:repo`: Test Files 55 passed (55), Tests 971 passed (971); VERDICT command-exit 0. No new text carries an `os migrate meta --from` sentence (the residue explanation names `os migrate meta` without one), and no tracker id is in a printed hint or explanation (citations stay in comments and test names). - **Ablation** (one-shot, from the committed head `b4cf329ada`, through `scripts/ablation-replace.mjs` under the lock, with a shell `trap` restoring `HEAD` by absolute path). The rule suites import the rule source, so there is no dist leg. `permission-retired-lifecycle-residue`'s pre-slice line was restored: `hint: retiredKeyRemedy(prescription),` → `hint: prescription,` (anchor x1 → x0, blob `52140529de29` → `c568d4a53e68`). Predicted before the run: exactly the exact-remedy case and the new bound pin go red. Observed: `src/validate-retired-permission-residue.test.ts` Tests 2 failed, 21 passed (23), exactly those two; the bound pin read 573. Restored: blob `52140529de29` == HEAD, `git diff HEAD` empty, `git status --porcelain` empty. - **ESLint, narrowed:** `npx eslint --no-inline-config --format json` over the 28 changed `.ts` files: 28 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:** two merges of `origin/main` through `scripts/pm/os-regen-merge.sh` (to `12b9daf749`, then `55382dc02a`; neither touched a line this slice edits, 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 `b4cf329ada` derived 62 commands; 60 ran, each exit code captured before any pipe, all 0. NOT MEASURED, as dispatched: `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 `b4cf329ada`, `check:published-readme-exports` after building its four missing dists (`client-react`, `embedder-openai`, `knowledge-ragflow`, `organizations`) under the lock; the three PR-context guards run against this PR and report on the card. - **Round 2** (seat order `6111995055`): - the conflict merge `bf78c173d3` with #22850 (`69d4218058`, the organization administrators' slate). #22850's messages and its first explanation paragraph win. The two one-line fixes and the moved paragraphs are restated against it, and a test block holds that no fix line or explanation describes a retired rescue path; - the merge `4ca9154ae6`; - head `6e2c2f068c`. - Readings: lint 137 files / 6,497 tests; the approval file 56 / 56; metadata-protocol 225 files + 3 skipped / 28,131 tests; spec `test:repo` 55 / 971; `check:generated` 14 up to date. The no-path union is 109 derived, 107 run, 2 NOT MEASURED (`dual-build-cjs-loads`, `type-check-debt`), 0 UNRUN, and the 3 PR-context guards are green. ## Remaining for this card - **107 author-time `packages/lint` ids still carry a hint over 200 characters** in the final lint census (at `b4cf329ada`: 131 over 200 = 107 + 21 fenced + 3 repo-gate). The longest, by file: `lint-flow-patterns.ts`'s `flow-double-brace-interpolation` 1,339 and `flow-bare-dollar-reference` 1,231 (not taken here: PR #22832, then open, edited `lint-flow-patterns.test.ts`, and the slice stayed off that file; #22832 has since landed); `validation-rule-json-schema-unknown-format` 646; `flow-credential-literal` 646; `measure-aggregate-field-type-refused` 645; `predicate-rhs-path-shaped` 643; `rls-predicate-unknown-user-variable` 618; `hook-api-update-readonly-when-field` 607; `unique/legacy-organization-composite` 578; `dashboard-filter-field-unprovisioned` 561; `action-name-undefined` 560; `flow-update-readonly-when-field` 549; `ai-skill-tool-unresolved` 548; `org-axis-permission-inheritance` 542; `rls-predicate-unparseable` 523; `object-reference-unknown` 522; `filter-empty-node` 515; then 90 more down to 201. The full list, by file, is in the report on the card. - **The 26 fenced ids**, as PR #22448 lists them (21 fire a hint over 200), and the 3 repo-gate ids. - **The `packages/cli` ids** live on #22841. ## Acceptance notes - **A long name can still lengthen a line.** The bound holds on every variant the suites and the four example apps fire; the names in a hint (object, field, carrier path, rewritten predicate) are the author's. The closest measured case is `field-no-consumers` on `showcase_project_membership.allocation_percent` at 199, whose first carrier path is 80 characters. - **`field-no-consumers`' carrier list beyond the first now prints nowhere on the CLI.** The finding object still holds every site in `carriers` for a programmatic caller, but `os validate --json` carries an advisory as one `where: message` string and `os lint --json` maps the finding to `fix` without `carriers`. The card's "Asked for" names no carrier list at all; the first site plus the count is the dispatch's route. - **The spec's pointer moved with the remedy (round 2, the seat-authorized rider, order `6111995055`).** `ApproverType`'s `.describe()` (`packages/spec/src/automation/approval.zod.ts`) now points at `os explain approval-approvers-may-resolve-empty`, and its comment names `rule-explanations.ts` as the carrier. The reference page `content/docs/references/automation/approval.mdx` is regenerated, and `.changeset/22161-spec-approver-type-describe.md` (`@objectstack/spec` `patch`, `Clause-②: no`) carries it. This PR now publishes `@objectstack/lint` `patch` and `@objectstack/spec` `patch`. - **`rls-predicate-unknown-field`'s old hint said a missing column is "an outage in one position and an open door in the other"**, which the explanation's own history note records as no longer true (the compiled-predicate guard fails closed everywhere). The sentence is not carried into the explanation. --- _Generated by [Claude Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent aa94566 commit 2e11d90

32 files changed

Lines changed: 1235 additions & 423 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): 15 author-time rule ids print a `fix:` line of one remedy sentence, and `os explain <rule-id>` carries what those lines used to enumerate
6+
7+
Clause-②: no
8+
9+
- **Shorter fix lines.** Each finding of these 15 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 (the lengths here are the `hint` alone; the CLI prints it after `fix: `). Before, the longest of each ran from 128 to 2,280 characters. The ids:
10+
- sharing rules: `sharing-rule-runtime-variable-condition` (2,280 → 195) and `sharing-rule-unlowerable-condition` (968 → 199);
11+
- approvals: `approval-approvers-may-resolve-empty` (its `manager` arm 2,243 → 198, its group arm 440 → 200);
12+
- RLS predicates: `rls-predicate-unenforceable` (1,056 → 191), `rls-predicate-over-budget` (1,044 → 197) and `rls-predicate-unknown-field` (766 → 154);
13+
- filters: `filter-empty-combinator` (764 → 154);
14+
- permission sets: `permission-retired-lifecycle-residue` (681 → 14: `Delete the key`);
15+
- view visibility: `visibility-predicate-over-budget` (651 → 179);
16+
- hook bodies: `hook-api-update-readonly-field` (647 → 182);
17+
- flows: `flow-time-relative-descriptor-invalid` (342 → 176);
18+
- pages: `component-type-unknown` (229 → 158), `react-chart-aggregate-invalid` (363 → 191) and `react-chart-drilldown-invalid` (128 → 181: longer, because it now carries the remedy instead of a pointer);
19+
- fields: `field-no-consumers` (on `os validate` of the showcase app, 624 → 199).
20+
- **Four fix lines pointed at text the verdict no longer carries.** `component-type-unknown` said "Apply the prescription above", `react-chart-drilldown-invalid` and `react-chart-aggregate-invalid` said "the rejection above carries the fix", and `flow-time-relative-descriptor-invalid` said "satisfies each message above", after earlier releases cut those verdicts to one sentence. Each now names the remedy itself: the retired component type's "Delete the …" sentence cut from `RETIRED_PAGE_COMPONENT_TYPES` (for example `Delete the \`element:form\` component and use the object-bound \`object-form\` block instead`), the schema refusal's rename or the layer a misplaced key belongs to, and the descriptor key the verdict names.
21+
- **Enumerations are cut to the first item and a count.** `field-no-consumers` names the first carrier site and counts the rest (`… together with its 16 carrier site(s): translations[0].en.objects.showcase_account.fields.website (and 15 more)`), where it listed every one. `rls-predicate-unenforceable` gives the first faulty site's rewrite and counts the rest; for a bare `current_user` it names the key slot (`current_user.<key>`) instead of listing every kernel key. `rls-predicate-unknown-field` no longer lists every field of the object: the verdict's "Did you mean" already names the nearest one. `permission-retired-lifecycle-residue` carries the retirement's own "Delete the key" sentence, cut from the tombstone's prescription, not the whole prescription.
22+
- **`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: the lowerable predicate subset and its traps, the comparison classes with the declared types each holds, the rule each `current_user` holding breaks, which mechanism replaces a `current_user` read on which sharing model (an RLS policy narrows only `public_read` / `public_read_write`; it grants nothing on `private`), the graded routes that populate `sys_user.manager_id`, the four ways to shrink an oversized RLS predicate (and why a top-level `&&` must never be split), and the declared "match nothing" filter spelling. The entries already existed; they gain paragraphs.
23+
- **Where the new fix lines print.** On the CLI: the `fix:` line under each finding of `os validate`, `os build` (and `os compile`, which `os dev` runs at startup and, under `--verbose`, on each watch recompile), the scaffold check `os init` runs, and `os verify`'s refusal list; `os lint` prints it as `→ fix:` under `--fix` and as `fix` in `--json`; `os validate --json` (`errors`) and `os build --json` (author-time `issues`) carry it on an error-severity finding; their `warnings` carry an advisory as one `where: message` string, without it. At the runtime publish gate (Studio, REST `PUT /api/v1/meta`, and the MCP and AI authors that write through that door), the `hint` FROM → TO, measured at the door:
24+
- a `flow` write: `flow-time-relative-descriptor-invalid` (422 `issues[].hint`, 342 → 176), `filter-empty-combinator` (422, 647 / 764 → 134 / 154) and `approval-approvers-may-resolve-empty` (2xx `advisories[].hint` and the `[Protocol] authoring advisory` log line, 2,243 / 440 → 198 / 200);
25+
- a `permission` write: `rls-predicate-unenforceable` (422, 912 / 312 → 186 / 121), `rls-predicate-unknown-field` (422, 708 → 154), `rls-predicate-over-budget` (422, once the platform CEL bounds refuse) and `permission-retired-lifecycle-residue` (2xx advisory, 573 / 681 → 14);
26+
- a `view` write: `visibility-predicate-over-budget` (422, 651 → 179).
27+
28+
Studio's save-advisory toast prints an advisory's `hint` after its message, so the approval and residue toasts shorten; 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.
29+
- **Never at the runtime gate:** the two sharing-rule ids, `field-no-consumers`, `component-type-unknown`, the two `react-chart-*` ids and `hook-api-update-readonly-field` run on the CLI only.
30+
- **Unchanged:** every `message` (the verdict), rule id, severity and `path`, and what each rule accepts or refuses.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): `ApproverType`'s description points at `os explain approval-approvers-may-resolve-empty` for the graded `manager` routes
6+
7+
Clause-②: no
8+
9+
- **What changes:** the `.describe()` text of `ApproverType` (`@objectstack/spec/automation`), which the generated reference page renders for every approver entry (`content/docs/references/automation/approval.mdx`). It said `os lint` "carries the graded population routes and the full remedy". Since `@objectstack/lint` shortened `approval-approvers-may-resolve-empty`'s fix line to one sentence, the finding names two routes in one line, and `os explain approval-approvers-may-resolve-empty` prints the graded routes and the full remedy. The description now says so. Its "the request waits" clause now says the request falls to the node's `onEmptyApprovers` policy; under the default policy that opens the request on the organization's administrators, so it no longer waits.
10+
- **Unchanged:** the `ApproverType` values, the schema's shape, and what it accepts or refuses. Only the description text moves.

‎content/docs/references/automation/approval.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const result = ApprovalDecision.parse(data);
5353

5454
| Property | Type | Required | Description |
5555
| :--- | :--- | :--- | :--- |
56-
| **type** | `Enum<'manager' \| 'position' \| 'department' \| 'team' \| 'field' \| 'expression' \| 'org_membership_level' \| 'role' \| 'user' \| 'queue'>` | ✅ | Approval step approver type. `manager` is resolved from the directory rather than configured here: it reads the submitter's `sys_user.manager_id` at runtime, and that column is not a profile field an author edits, so until an operator populates it through one of the routes `os lint` names, a manager step resolves to nobody and the request waits. `os lint` reports that at authoring time as `approval-approvers-may-resolve-empty` and carries the graded population routes and the full remedy, the node-level `onEmptyApprovers` fallback included; the Approvals guide states the same remedy in prose. |
56+
| **type** | `Enum<'manager' \| 'position' \| 'department' \| 'team' \| 'field' \| 'expression' \| 'org_membership_level' \| 'role' \| 'user' \| 'queue'>` | ✅ | Approval step approver type. `manager` is resolved from the directory rather than configured here: it reads the submitter's `sys_user.manager_id` at runtime, and that column is not a profile field an author edits, so until an operator populates it, a manager step resolves to nobody and the request falls to the node's `onEmptyApprovers` policy. `os lint` reports that at authoring time as `approval-approvers-may-resolve-empty`, and `os explain approval-approvers-may-resolve-empty` carries the graded population routes and the full remedy, the node-level `onEmptyApprovers` fallback included; the Approvals guide states the same remedy in prose. |
5757
| **value** | `string` | optional | User id / membership tier / position / team / department / field — per `type`; for `expression`, a CEL expression over `current.*` / `trigger.*` / `vars.*` |
5858
| **resolveAs** | `Enum<'user' \| 'department' \| 'position' \| 'team'>` | optional | How an `expression` result is expanded into approvers (default 'user') |
5959
| **group** | `string` | optional | Group label for per_group sign-off (e.g. "legal", "finance") |
@@ -83,7 +83,7 @@ const result = ApprovalDecision.parse(data);
8383

8484
| Property | Type | Required | Description |
8585
| :--- | :--- | :--- | :--- |
86-
| **type** | `Enum<'manager' \| 'position' \| 'department' \| 'team' \| 'field' \| 'expression' \| …>` | ✅ | Approval step approver type. `manager` is resolved from the directory rather than configured here: it reads the submitter's `sys_user.manager_id` at runtime, and that column is not a profile field an author edits, so until an operator populates it through one of the routes `os lint` names, a manager step resolves to nobody and the request waits. `os lint` reports that at authoring time as `approval-approvers-may-resolve-empty` and carries the graded population routes and the full remedy, the node-level `onEmptyApprovers` fallback included; the Approvals guide states the same remedy in prose. |
86+
| **type** | `Enum<'manager' \| 'position' \| 'department' \| 'team' \| 'field' \| 'expression' \| …>` | ✅ | Approval step approver type. `manager` is resolved from the directory rather than configured here: it reads the submitter's `sys_user.manager_id` at runtime, and that column is not a profile field an author edits, so until an operator populates it, a manager step resolves to nobody and the request falls to the node's `onEmptyApprovers` policy. `os lint` reports that at authoring time as `approval-approvers-may-resolve-empty`, and `os explain approval-approvers-may-resolve-empty` carries the graded population routes and the full remedy, the node-level `onEmptyApprovers` fallback included; the Approvals guide states the same remedy in prose. |
8787
| **value** | `string` | optional | User id / membership tier / position / team / department / field — per `type`; for `expression`, a CEL expression over `current.*` / `trigger.*` / `vars.*` |
8888
| **resolveAs** | `Enum<'user' \| 'department' \| 'position' \| 'team'>` | optional | How an `expression` result is expanded into approvers (default 'user') |
8989
| **group** | `string` | optional | Group label for per_group sign-off (e.g. "legal", "finance") |
@@ -93,7 +93,7 @@ const result = ApprovalDecision.parse(data);
9393

9494
| Property | Type | Required | Description |
9595
| :--- | :--- | :--- | :--- |
96-
| **type** | `Enum<'manager' \| 'position' \| 'department' \| 'team' \| 'field' \| 'expression' \| …>` | ✅ | Approval step approver type. `manager` is resolved from the directory rather than configured here: it reads the submitter's `sys_user.manager_id` at runtime, and that column is not a profile field an author edits, so until an operator populates it through one of the routes `os lint` names, a manager step resolves to nobody and the request waits. `os lint` reports that at authoring time as `approval-approvers-may-resolve-empty` and carries the graded population routes and the full remedy, the node-level `onEmptyApprovers` fallback included; the Approvals guide states the same remedy in prose. |
96+
| **type** | `Enum<'manager' \| 'position' \| 'department' \| 'team' \| 'field' \| 'expression' \| …>` | ✅ | Approval step approver type. `manager` is resolved from the directory rather than configured here: it reads the submitter's `sys_user.manager_id` at runtime, and that column is not a profile field an author edits, so until an operator populates it, a manager step resolves to nobody and the request falls to the node's `onEmptyApprovers` policy. `os lint` reports that at authoring time as `approval-approvers-may-resolve-empty`, and `os explain approval-approvers-may-resolve-empty` carries the graded population routes and the full remedy, the node-level `onEmptyApprovers` fallback included; the Approvals guide states the same remedy in prose. |
9797
| **value** | `string` | optional | User id / membership tier / position / team / department / field — per `type`; for `expression`, a CEL expression over `current.*` / `trigger.*` / `vars.*` |
9898
| **resolveAs** | `Enum<'user' \| 'department' \| 'position' \| 'team'>` | optional | How an `expression` result is expanded into approvers (default 'user') |
9999
| **group** | `string` | optional | Group label for per_group sign-off (e.g. "legal", "finance") |
@@ -124,7 +124,7 @@ const result = ApprovalDecision.parse(data);
124124

125125
## ApproverType
126126

127-
Approval step approver type. `manager` is resolved from the directory rather than configured here: it reads the submitter's `sys_user.manager_id` at runtime, and that column is not a profile field an author edits, so until an operator populates it through one of the routes `os lint` names, a manager step resolves to nobody and the request waits. `os lint` reports that at authoring time as `approval-approvers-may-resolve-empty` and carries the graded population routes and the full remedy, the node-level `onEmptyApprovers` fallback included; the Approvals guide states the same remedy in prose.
127+
Approval step approver type. `manager` is resolved from the directory rather than configured here: it reads the submitter's `sys_user.manager_id` at runtime, and that column is not a profile field an author edits, so until an operator populates it, a manager step resolves to nobody and the request falls to the node's `onEmptyApprovers` policy. `os lint` reports that at authoring time as `approval-approvers-may-resolve-empty`, and `os explain approval-approvers-may-resolve-empty` carries the graded population routes and the full remedy, the node-level `onEmptyApprovers` fallback included; the Approvals guide states the same remedy in prose.
128128

129129
### Allowed Values
130130

0 commit comments

Comments
 (0)