Skip to content

Commit b616c0a

Browse files
feat(lint): a ledger-dead or live-elsewhere key warns without an authorWarn opt-in, and never shows the ledger note (#16094) (#21092)
Fixes #16094 Clause-②: no Ruling-ref: 5560227939 ## What changes `shouldWarn` in `packages/lint/src/lint-liveness-properties.ts` now admits a ledger row whose status is `dead`, `live-elsewhere` or `experimental`, or that opts in with `authorWarn: true`. Before, only `experimental` warned without an opt-in. `describe()`'s mapping to `liveness-dead-property` / `liveness-live-elsewhere-property` is unchanged. Before the change, both of those rule ids were exported and could never be produced: across the shipped ledgers, not one `dead` or `live-elsewhere` row set `authorWarn`. `checkItem` changes only for rows the ruling newly admits. A row that warns only because of its `dead` / `live-elsewhere` verdict (no `authorWarn`) shows its `authorHint`, else the verdict's existing default hint, and never its ledger `note`. Rows that opt in with `authorWarn`, and `experimental` rows, keep their hint exactly as before. This is the seat's Q3 answer (card comment 5925292339). Other changes: - **Stale comments.** Comments the flip made false are corrected in the same file. The one stale line above the `lintLivenessProperties` registry entry in `authoring-rules.ts` is corrected too. - **Changeset.** One `minor` changeset for `@objectstack/lint`, carrying `Clause-②: no` in its body. ## Recount on `main` Measured at base `6073bb96b8`, all depths: | Rows | Count | |---|---| | `dead` | 109 | | `live-elsewhere` | 1 | | Of those 110, `retiredKey` tombstones (`check:liveness --json`) | 97 | | Authorable | 13 (12 `dead`, 1 `live-elsewhere`) | | Authorable and reachable through the rule's walk | 4 | The 4 reachable rows are `view.name`, `view.label`, `permission.rowLevelSecurity.label` and `permission.rowLevelSecurity.description`. The other 9 authorable rows sit in types the walk never visits: - `connector.metadata` - `manifest.runtime` and `manifest.integrity` - six `realtime_subscription` rows The warn map grows by 105 entries, at depth 1 or less. None of the 105 carries an `authorHint`, and 80 of their notes cite a tracker id. That is why the hint selection changed in this PR. ## The live-elsewhere end-to-end pin: measured impossible, replaced by an equivalent invariant plus a reach sentinel The ruling asks for an end-to-end pin proving `liveness-live-elsewhere-property` is produced against the shipped ledger from its one `live-elsewhere` row. That row is `manifest.runtime`. **Measured: no stack can produce that id through `lintLivenessProperties`.** - `manifest` is not in `TYPE_COLLECTIONS`, and it is not one of the bespoke walks (objects/fields, translation bundles). - `stack.manifest` is a single object, not a collection. - `authorWarnedProperties` has one caller outside its package, `packages/cli/src/utils/i18n-extract.ts`, and that caller asks only about `translation`. No translation row is newly admitted. **The invariant that replaces the literal pin is equivalent for the ruling's purpose, plus a reach sentinel.** Three pins on the shipped ledger hold it: 1. The shipped row is admitted: `authorWarnedProperties('manifest').has('runtime')`. 2. The shipped row maps to `LIVENESS_LIVE_ELSEWHERE_PROPERTY`: `checkItemAgainstWarnMap` over the row as read from the shipped `manifest.json`. 3. **No walk visits `manifest`.** `lintLivenessProperties({ manifest: { runtime, integrity } })` says nothing about either key. This pin goes red the day any walk visits `manifest`, and its comment names `manifest.integrity`. Pin 3 matters because `manifest.integrity` is `dead` in the ledger, yet `os plugin publish` reads its map and refuses on a digest mismatch (`packages/cli/src/commands/plugin/publish.ts:134`). A manifest walk added before that row is re-graded would tell authors a gate-read key is inert. Pin 3 makes that walk a deliberate, visible change. Because `manifest` is not walked, the flip does not surface `manifest.integrity` to any author. ## `--strict`, and why `Clause-②` stays `no` Nothing is refused, and nothing changes without `--strict`. `os lint --strict` and `os validate --strict` go from exit 0 to exit 1 on a stack that was otherwise warning-clean and authors a view container `label`/`name` or an RLS policy `label`/`description`. Measured with the CLI built from source on fixture stacks, before (base) and at this head: | fixture | `os lint` | `os lint --strict` | `os validate` | `os validate --strict` | |---|---|---|---|---| | clean | 0 → 0 | 0 → 0 | 0 → 0 | 0 → 0 | | view container `label` | 0 → 0 | **0 → 1** | 0 → 0 | **0 → 1** | | RLS policy `label` + `description` | 0 → 0 | **0 → 1** | 0 → 0 | **0 → 1** | | raw config (no `defineStack`) with tombstoned `list.striped` | 0 → 0 | **0 → 1** | 1 → 1 | 1 → 1 | The `Clause-②` criterion is "widens the accept set or enlarges the public surface". A new warning does neither, and both rule ids were already exported. The seat ruled on this as Q1 (card comment 5925292339), citing two precedents: `.changeset/20654-flow-credential-literal-advisory.md` and lint `d753744`. Other doors: - `os build` has no warning-promoting flag. - The runtime publish door runs this rule for `email_template` / `mapping` / `datasource` only, and none of those gains a row. - The `os lint --eval` corpus has no views and no RLS. - `check:i18n-coverage` counts only `i18n/` rules. - No repo gate asserts a zero-warning count on the examples. **Bump: minor.** The at-tier contract review (`5925918475`) set this, and it corrects the earlier `patch`. No export is added or removed, and the default-face accept set is unchanged. But two rule ids become producible, which a consumer sees as two new advisories, and a non-zero exit becomes reachable under `--strict`. The repository grades that shape `minor`: `.changeset/20654` is a new advisory with `Clause-②: no` at `minor`, and the cli 17.5.0 entry grades "the new advisories and the newly reachable non-zero exit" `minor`. `d753744` is a fix to an existing finding, not a precedent for this. The changeset carries `Clause-②: no` in its body. ## Who starts warning These are this repository's example apps, run with `os lint --json` and `os validate --json`, at base and at this head: | example | new findings | exit codes (4 modes) | |---|---|---| | `app-showcase` | 2 × `liveness-dead-property`: permission `showcase_contributor`, `rowLevelSecurity.label` and `.description` | unchanged | | `app-crm` | 0 | unchanged | | `app-todo` | 0 | unchanged | | `app-multi-package` | 0 | unchanged | All four examples already exit 1 under `--strict`. **The two showcase findings' printed hint.** At this head, both read: `Remove it — it is declared in the spec but not consumed at runtime.` That is 69 characters. Before the hint fix, both printed the ledger note instead (939 and 270 characters of maintainer prose). The 270-character one ended "Benign, not authorWarn'd." The showcase's two existing `liveness-planned-property` findings (`externalSharingModel`) are byte-identical, message and fix, between base and this head. ## Retired keys A `retiredKey` tombstone keeps its `dead` row. - **Commands that parse.** `os validate`, `os build` and the runtime gate refuse the key first, so it gets no second report. A `defineStack` config with a retired key fails `os lint` at load as before. - **`os lint` on a raw config.** `os lint` does not Zod-parse. A raw config without `defineStack` that `os lint` accepts, and that carries a retired key, now gets a `liveness-dead-property` warning with the default "Remove it" hint. Before, it got nothing. Ten existing pins asserted silence on such keys. That silence came from the opt-in, not from the tombstone. They are re-judged to assert the `dead` grade, with the parse control alongside: `ViewSchema` refuses `list.striped`. ## Tests All results below are at head `49fb6cfad3`, after merging `origin/main` at `9c8b65aa23` (which carries #21047). - **Scoped file.** `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/lint-liveness-properties.test.ts`: 93 passed, against 84 at base. There are 9 new pins, and the 10 silence pins are re-judged. - **Whole package.** `@objectstack/lint`: 118 files, 5486 tests passed. `pnpm --filter @objectstack/lint typecheck`: exit 0. - **CLI.** Unit tier: 240 files, 3419 tests passed. Integration tier: all 69 files, run as 4 lock-held runs, 591 passed and 1 skipped. `lint-per-package-authoring-parity` passes 5/5. - **Gates.** `dispatch-gates --commands`: 61 families, all exit 0. `--ran`: 61 derived, 61 run, 0 NOT-MEASURED. - **eslint.** A narrowed run over the 4 changed `.ts` files: 0 errors, 0 warnings. The new pins, end to end against the shipped ledger: - an authorable `dead` key, `rowLevelSecurity.label` / `.description`, produces `liveness-dead-property`; - the live keys beside it stay silent; - the dotted path fans out past index 0; - the fixture parses through `PermissionSetSchema`; - a tombstoned key is refused at parse; - the three `manifest` pins described above. **Hint pins, ledger-wide:** - All 105 verdict-triggered rows show a hint that is not their note and that carries no tracker id. Anti-vacuity: 80 of those notes do carry one. - **Control:** all 9 rows that warned before the ruling (`authorWarn` or `experimental`) keep `authorHint ?? note` byte for byte. Anti-vacuity: 6 of them show their note today. - Synthetic pins hold the precedence per verdict and opt-in. **Ablations**, through `scripts/ablation-replace.mjs`: the anchor hit and the blob moved each time. Every restore left the blob equal to HEAD and `git diff HEAD` empty, under a trap. | Mutation | Result | What it proves | |---|---|---| | Verdict set reduced to `{experimental}` (pre-ruling behaviour) | 12 red, including both new verdict pins | the verdict pins depend on the flip | | Hint selection reverted to `authorHint ?? note ?? default` | 3 red: the three Q3 pins | the Q3 pins catch the old precedence | | Hint selection widened to never show any note | 3 red, including the byte-for-byte control | the control can fail | The test subject resolves to `src` through the relative import, so no dist build was involved in any ablation. **Narrowed eslint** (the 4 changed `.ts` files, as in Tests above): - Population: `eslint.config.mjs`'s `**/*.{ts,…}` and `packages/**/*.{ts,…}` blocks select all four; the changeset is not linted. - Invariance: the config enables no type-aware linting (no `parserOptions.project` or `projectService`), so this diff cannot move any untouched file's verdict. **Control bytes.** A self-scan of the 5 changed files finds none. `check:nul-bytes` exits 0. ## The CLI parity fixture, re-judged The #18778 fixture in `packages/cli/test/lint-per-package-authoring-parity.test.ts` declared view containers as `{ name, label, object, list }`. `view.name` and `view.label` are `dead`, so after the flip the union run raised 4 warnings, which broke the fixture's premise that the union raises nothing. CI on `64e0275024` failed two tests (`expected 5 to be 1`, at `:241` and `:279`). The containers now bind by `object` alone (`list.label` is live and stays), with a comment saying why. Each test keeps its intent: the union is clean, there is one per-package survivor, and `--strict` fails with 1. ## Acceptance notes - **Pre-existing note leak on opted-in rows (not touched here, as ruled).** `object.externalSharingModel` is `planned` + `authorWarn` with no `authorHint`, so authoring it prints its ledger note as the fix. That note is 728 characters and cites `#2696`. Measured on `app-showcase` at this head: 2 findings (`showcase_account`, `showcase_announcement`). Across the shipped ledgers, 6 of the 9 rows that warned before the ruling show their note. AGENTS.md keeps tracker numbers out of anything an author is shown. - **A manifest walk, and the `manifest.integrity` re-grade it would need first.** These have zero measured pull. No example or plugin config in this repository authors `runtime` or `integrity`; `os plugin build` writes `integrity`. The note that deferred the row's status answers 404. No carrier. - **`connector` is not walked.** After the flip, `connector` (a real stack collection, `connectors`) carries a warn-worthy row, `metadata`. By its own note it is an uninterpreted extension bag whose specification is "no consumer", and `connector` is not in `TYPE_COLLECTIONS`. Registering it would warn on every authored `metadata` bag. No carrier. - **#21047 landed** (`88b484e00c`) and is merged here. The field-set pin is measured, not assumed, at `['conditionalRequired']`: `field.picklist` is `live` with no `authorWarn` after #21047, and `field.conditionalRequired` is a `dead` tombstone. - **Merge state.** `origin/main` was merged at `9c8b65aa23` (`b0705c1530`). The 6 later `main` commits touch neither `packages/lint`, `packages/spec/liveness` nor the CLI parity test. - **`mapping.connectorSource` crash on `main`, not this PR's.** `main` ships `mapping.connectorSource` as `live` + `authorWarn` (8368f1c), and that crashes `os lint` and `os validate` on any stack authoring it: `describe()` throws its integrity sentinel. This PR's hint control is scoped to rows `describe()` answers, and its COVERAGE pin keeps the sentinel loud. Filed as #21127. --- _Generated by [Claude Code](https://claude.ai/code/session_01JAhu8u8QfBvRjVZDox7CP9)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1bd14c9 commit b616c0a

5 files changed

Lines changed: 378 additions & 79 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
Authoring a key whose liveness-ledger verdict is `dead` now draws a `liveness-dead-property` warning, and a `live-elsewhere` key a `liveness-live-elsewhere-property` warning, with no per-row `authorWarn` opt-in: the verdict itself is the warning. Before this, both rule ids were exported and never produced, because no shipped `dead` or `live-elsewhere` row opted in.
6+
7+
Clause-②: no
8+
9+
**Which keys warn.** A ledger row warns when its status is `dead`, `live-elsewhere` or `experimental`, or when it sets `authorWarn: true` (still the only way a `planned` row warns). Among authorable keys in the metadata types the rule walks, four are `dead` today: a view container's own `name` and `label` (the `defineView` container, not `list.label`), and a permission set's `rowLevelSecurity[].label` and `rowLevelSecurity[].description`. No walk visits `manifest`, `connectors` or realtime subscriptions, so their rows still warn nobody. That includes `manifest.runtime`, the one `live-elsewhere` row.
10+
11+
**The hint.** A row that warns only because of its `dead` or `live-elsewhere` verdict shows its `authorHint`, else the verdict's default hint: "Remove it — it is declared in the spec but not consumed at runtime." for `dead`. It never shows the ledger's internal `note`. Rows that opt in with `authorWarn`, and `experimental` rows, show exactly the hint they showed before.
12+
13+
**Retired keys.** A `retiredKey` tombstone keeps its `dead` row. Every command that parses (`os validate`, `os build`, the runtime publish gate) still refuses the key first, so it gets no second report. `os lint` does not parse: a config it accepts without `defineStack` that carries a retired key now gets a `liveness-dead-property` warning where it got nothing.
14+
15+
**What changes for a project.** Nothing is refused, and nothing changes without `--strict`. `os lint --strict` and `os validate --strict` now exit 1 instead of 0 on a stack that was otherwise warning-clean and authors a view container `label` or `name`, or a row-level-security policy `label` or `description`. A view container that carries its own `name` and `label` beside its `object` binding is one such shape: it draws two warnings per container, and under `--strict` that flips the exit. Across this repository's example apps, only `app-showcase` gains warnings: two, on one permission set's policy `label` and `description`, and no example's exit code changes. To clear the warning, delete the key: nothing reads it.

‎packages/cli/test/lint-per-package-authoring-parity.test.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,18 @@ const ordersObjects = [{
151151
account: { name: 'account', type: 'lookup', label: 'Account', reference: 'pp_account' },
152152
},
153153
}];
154+
// The containers bind by \`object\` and carry no \`name\` / \`label\` of their own:
155+
// both keys are \`dead\` in the view ledger, and since #16094 each draws a
156+
// union-run \`liveness-dead-property\` warning, which would break the "union
157+
// raises NOTHING" premise this fixture exists to hold. The list's own \`label\`
158+
// is live and stays.
154159
const ordersViews = [
155160
{
156-
name: 'pp_account', label: 'Account List', object: 'pp_account',
161+
object: 'pp_account',
157162
list: { label: 'Account List', columns: ['name', 'industry'] },
158163
},
159164
{
160-
name: 'pp_order', label: 'Order List', object: 'pp_order',
165+
object: 'pp_order',
161166
list: { label: 'Order List', columns: ['name', 'account'] },
162167
},
163168
];

‎packages/lint/src/authoring-rules.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,9 +1494,9 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
14941494
})),
14951495
},
14961496
// The spec-liveness loop on the author side: a property the ledger marks
1497-
// dead-and-misleading or experimental is set hopefully and does nothing.
1498-
// Ledger-driven (entries opt in via `authorWarn`), so it is high-signal and
1499-
// never fatal.
1497+
// dead, live-elsewhere or experimental is set hopefully and does nothing
1498+
// here. Ledger-driven (those verdicts warn on their own; any other row only
1499+
// when it opts in via `authorWarn`), and never fatal.
15001500
{
15011501
name: 'lintLivenessProperties',
15021502
tier: 'advisory',

0 commit comments

Comments
 (0)