Repository navigation
feat(lint): a ledger-dead or live-elsewhere key warns without an authorWarn opt-in, and never shows the ledger note (#16094) - #21092
Conversation
…ithout a per-row opt-in shouldWarn admits a row whose status is dead, live-elsewhere or experimental, or which opts in with authorWarn. Before, only experimental warned on its own, so liveness-dead-property and liveness-live-elsewhere-property were unreachable: no shipped dead or live-elsewhere row set authorWarn. The pins that asserted silence on retired (tombstoned) keys are re-judged: their rows stay dead, a parse still refuses them first, and handed an unparsed stack the rule now grades them dead. New pins prove the dead id is produced end to end against the shipped ledger, that live keys beside it stay silent, that a tombstoned key is refused at parse, and that the shipped live-elsewhere row is admitted and mapped, while no walk visits the manifest. Claude-Session: https://claude.ai/code/session_01JAhu8u8QfBvRjVZDox7CP9 Co-authored-by: Claude <noreply@anthropic.com>
…arns Claude-Session: https://claude.ai/code/session_01JAhu8u8QfBvRjVZDox7CP9 Co-authored-by: Claude <noreply@anthropic.com>
…ever shows its ledger note A row admitted by the ruled verdicts alone, with no authorWarn opt-in, now shows its authorHint, else the verdict's default hint. Its note is maintainer evidence (tracker ids, commit shas, "deliberately not warned"), which an author must not be shown. Opted-in and experimental rows keep their hint byte for byte; ledger-wide pins hold both halves. The changeset drops its internal citation and states the --strict effect in one sentence; the stale registry comment in authoring-rules.ts is corrected. Claude-Session: https://claude.ai/code/session_01JAhu8u8QfBvRjVZDox7CP9 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1df43dac3cacbe7ec536a06d92d130a9b1a26a44 && git checkout 1df43dac3cacbe7ec536a06d92d130a9b1a26a44
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9c8b65aa23781f2c1d2aa0abe92fc11e0e71b3a1 49fb6cfad312b18cd24f397b8631454bdd375485 && git checkout -B drift-repro 9c8b65aa23781f2c1d2aa0abe92fc11e0e71b3a1 && git merge --no-ff 49fb6cfad312b18cd24f397b8631454bdd375485
node scripts/docs-audit/affected-docs.mjs --json 9c8b65aa23781f2c1d2aa0abe92fc11e0e71b3a1
|
Contract reviewServed-tier: Scope: PR #21092 on card #16094, net diff against ① Derived judgmentsAccept set and public surface, each named.
The changeset, sentence by sentence (
The pin requirement. The What the diff gets wrong or misses.
② Semver level
The changeset body carries no ③ Boundary flagsRound 1
Round 1 Round 1 Patch round Patch round Check-runs on the head, as read at 06:16Z: 30 success, 6 skipped, 2 failure ( Owed before a PASS on the next head: (1) re-judge Implemented-by: VERDICT: FAIL |
# Conflicts: # packages/lint/src/lint-liveness-properties.test.ts
…view-container shape; changeset is minor The #18778 parity fixture's two view containers carried their own name and label beside the object binding. Both keys are dead in the view ledger and now warn on the union run, which broke the fixture's "union raises nothing" premise (5 warnings where 1 per-package survivor was pinned, and failing 5 under --strict). The containers now bind by object alone, so the fixture again holds exactly one per-package survivor and each test keeps its intent. The #5010 comment in the liveness test now says a retired key shows the dead default hint, not its note. The changeset is minor, carries Clause-②: no, and names view containers with their own name/label as a shape that now warns. Claude-Session: https://claude.ai/code/session_01JAhu8u8QfBvRjVZDox7CP9 Co-authored-by: Claude <noreply@anthropic.com>
… answers A live row that opts in with authorWarn makes describe() throw before and after the ruling, so it has no hint to keep; the COVERAGE pin holds that it stays loud. main now ships one such row (mapping.connectorSource), which the control would otherwise trip over. Claude-Session: https://claude.ai/code/session_01JAhu8u8QfBvRjVZDox7CP9 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Scope: PR #21092 on card #16094 at head ① Derived judgmentsAccept set and public surface, each named (the source is unchanged since the prior record; re-read on this head).
The changeset, sentence by sentence (
The merge of
The CLI fixture re-judge (
The byte-for-byte hint control, re-scoped in
The
Still wrong or stale, none of it shipped prose. The PR body's Tests section carries two "Gates" paragraphs (61 families / 60 families) and two narrowed-eslint paragraphs (4 files / 3 files); the seat's edit added this head's and left the prior head's in place — the 61 / 4 pair is this head's. Pre-existing and untouched, as before: the translation-walk comment in ② Semver level
③ Boundary flagsThe prior FAIL record's four owed items (
Newest
Newest
One flag this review adds, outside the claim surface and outside the review faces: ESCALATED, not owed on this PR. Check-runs on Nothing is owed before landing. The ruled flip, the hint fix, the pins, the fixture re-judge and the changeset are right as they stand. Implemented-by: VERDICT: PASS |
…note, for any row class (objectstack-ai#21169) Fixes objectstack-ai#21096 Clause-②: no ## What `checkItem` in `packages/lint/src/lint-liveness-properties.ts` now builds the fix text as `entry.authorHint ?? defaultHint`. The row's ledger `note` is never author-facing, for every row class (opted-in `authorWarn`, `experimental`, `dead`, `live-elsewhere`, `planned`). The now-unused `isVerdictTriggered` helper and the comments describing the old split are removed. `describe()` already carries a default hint for all four verdicts a finding can carry (`experimental`, `planned`, `dead`, `live-elsewhere`), so no prose was invented. A `live` row with `authorWarn` still throws the integrity sentinel, unchanged. No ledger row under `packages/spec/liveness/**` is edited: this is the structural direction from triage, not per-row `authorHint` discipline. ## Measured end to end `os lint --json` on `examples/app-showcase` (built from this tree, before and after rebuilding `@objectstack/lint`), the two `liveness-planned-property` findings, `fix` field: - Before, both findings: "[ADR-0090 D11] P1 lands the SPEC SHAPE only (validated at authoring that the external dial is no wider than the internal one; ...) ... tracked on objectstack-ai#2696. `planned` + authorWarn per enforce-or-mark ... Re-verified 2026-07-30: ..." (about 700 characters of maintainer prose, one tracker id). - After, both findings: "Keep it — a consumer is being built against this property; it has no runtime effect yet." The two `liveness-dead-property` findings on `showcase_contributor` are unchanged ("Remove it — it is declared in the spec but not consumed at runtime."). Exit code 0 before and after. ## Tests (`lint-liveness-properties.test.ts`) - The real-ledger `object.externalSharingModel` pin now asserts the planned default hint, no `#` followed by digits, and that the row's note text is absent (with an anti-vacuity guard that the row is still opted-in, has no `authorHint` and a note with a tracker id). - New real-ledger pin over every shipped opted-in and `experimental` row: a row with an `authorHint` prints it exactly (control); a row without one prints neither its note nor a tracker id. - New synthetic pin: authorHint wins, otherwise the verdict default, never the note, for `dead`, `live-elsewhere`, `planned` and `experimental`, with and without `authorWarn`. - Two objectstack-ai#21092 pins that encoded the old "opted-in rows show the note" behavior are rewritten. Results: `pnpm --filter @objectstack/lint test` 118 files / 5486 tests pass; the liveness file alone 93 pass; `pnpm --filter @objectstack/lint typecheck` green. Ablation (one-line revert to `entry.authorHint ?? entry.note ?? defaultHint`, through `scripts/ablation-replace.mjs`, anchor hit 1 time, blob moved, restore verified byte-identical to HEAD): 6 tests go red (the two real-ledger pins, the two synthetic pins, the live-elsewhere precedence pin, the dead RLS end-to-end pin); 87 stay green. Restored; `git diff HEAD` empty. ## Other sites where a ledger note could reach an author None found in `packages/lint/src` or `packages/cli/src` besides this expression. The other `.note` reads in `validate-react-page-props.ts` are deprecation records, not ledger rows. Nothing else reads the ledger's `note` as author text. ## Gates `node scripts/pm/dispatch-gates.mjs --commands` derived 60 families; each was run and recorded with its exit code, then reconciled with `--ran`: 59 exit 0, 1 NOT MEASURED (`check:dual-build-cjs-loads`, exit 3, PREREQUISITE NOT MET: eight unrelated packages have no `dist/` in this worktree; nothing was measured, and the diff touches no `exports` or build output). CI owns the rest. ## Coordination objectstack-ai#21135 (the liveness README) states the same model. It has no open PR and no branch at this time; whichever lands second keeps the two consistent. This PR does not touch the README. --- _Generated by [Claude Code](https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…and lints again — the `live` ledger row carries no author warning (objectstack-ai#21127) (objectstack-ai#21176) Fixes objectstack-ai#21127 Clause-②: no ## What this changes `os validate --json` and `os lint --json` exited 1 on any stack whose `mappings[]` entry authors `connectorSource`. The whole answer was the liveness lint's integrity sentinel: `lintLivenessProperties: ledger entry has unrecognised status "live"`. The ledger row `mapping.connectorSource` had been re-graded `live` and kept `authorWarn: true`. `describe()` in `packages/lint/src/lint-liveness-properties.ts` throws on a warned `live` row by design, and that throw stays as it is. - **The row** (`packages/spec/liveness/mapping.json`): `connectorSource` stays `live` and drops `authorWarn` / `authorHint`. Its `note` records why. - **The caveat moves to where an author reads it** (`packages/spec/src/data/mapping.zod.ts`). The `connectorSource` `.describe()` now reads: "Pulled when a job drives it; nothing schedules it yet, so the binding alone moves no rows — schedule the pull with a `job` once a job can drive one". The docblock no longer says the ledger keeps `authorWarn`. - **The existing integrity check refuses the combination, inside its current gate** (`check:liveness`, `packages/spec/scripts/liveness/check-liveness.mts`). No new gate or script was added. The graded walk never reads a container row that drills into `children`, and `connectorSource` is such a row, so the new rule walks the raw ledger rows at every depth. It fails on `status: "live"` with `authorWarn: true`, prints a prescription, and prints a census line on every run: `author warnings: 2 ledger row(s) opt into authorWarn, at any depth (planned 2); 0 on a live row.` - **Regenerated:** `content/docs/references/data/mapping.mdx` and `integration/connector.mdx` (`gen:docs`), and `packages/spec/src/migrations/registry.ts` (`gen:migration-registry`). ## Files beyond the claim's listed surface (each one is text this change made false) The claim lists the ledger row, the `connectorSource` describe, the existing integrity check, the regenerated artefacts, the pins and one changeset. These files are outside that list. Each one said that authoring the binding warns, and that stopped being true with the row fix: - `packages/spec/src/integration/connector.zod.ts`: the retired `connector.syncConfig` prescription, which an author sees in the refusal. - `packages/spec/src/migrations/entries/semantic/18.connector-sync-keys-retired.ts`: the upgrade entry's acceptance text. `registry.ts` is regenerated from it. - `packages/spec/src/integration/connector-sync-retirement.test.ts`: a one-line pin on that prescription text. - `packages/spec/docs/SYNC_ARCHITECTURE.md`: "authoring `connectorSource` still warns". - `packages/spec/liveness/README.md`: the mapping row's note, plus a third `authorWarn` rule (a `live` row carries none). - `packages/lint/src/authoring-rules.ts`: comments only. - `packages/lint/src/runtime-gate.inert-type-writes.test.ts`: a comment, and a pin. A `mapping` write that authors `connectorSource` used to get an `authoring-rule-threw` advisory at the runtime door, and now gets none. - `packages/cli/test/validate-lint-mapping-connector-source.test.ts`: the door pin, a new file. - `.changeset/20919-spec-connector-source-live.md`: see the next section. ##⚠️ One deliberate correction of a pending release note (needs confirming) `.changeset/20919-spec-connector-source-live.md` belongs to PR objectstack-ai#21084. It has not been released yet, and it said "`connectorSource` rows are `live` and keep `authorWarn`". This PR makes that false, so the sentence now reads "rows are `live`, with no author warning: that nothing schedules a pull yet is said on the key's description". `node scripts/check-empty-changeset.mjs --base origin/main` is therefore red by design. It says: "This PR changes a changeset it did not add … DELIBERATE CORRECTION -- … do NOT restore it -- say so on the PR and get it confirmed". This section is that statement, and `skip-changeset` is not applied. `.changeset/20281-connector-sync-moved-to-mapping.md` carried a similar claim. PR objectstack-ai#21150 has since rewritten it on `main` without the warn claim, and this PR does not touch it. ## Premise check - **H1, the crash** (measured at the dispatch base `665cab338f`, which contains PR objectstack-ai#21092 `b616c0a63d`). The card's fixture is one object `fx_account` with `sharingModel: 'private'`, plus one mapping with `sourceFormat: 'json'`, `targetObject: 'fx_account'`, `mode: 'upsert'`, a `fieldMapping` and `connectorSource: { connector: 'crm_api', action: 'request' }`. - At base, `os validate --json` and `os lint --json` both exited 1, and the only error was the sentinel. - After the fix, on the merged head `b6d58dd04`: `validate` exits 0 with `"valid": true`. `lint` exits 0 with `"passed": true` and one unrelated warning (`protocol/missing-engines-range`). Neither output names `connectorSource` or contains the sentinel. - The crash had been hiding a real finding. Without `sharingModel`, the same object gets `security-owd-unset`, so the fixture declares it. - **H2, the integrity check.** Yes, inside `check:liveness`. Run against the unfixed ledger, the gate exits 1 with exactly one ✗ block: "1 `live` ledger row(s) opt into `authorWarn` — the author-side lint throws on them: mapping/connectorSource". - **H3, census** (re-run on the merged head). Across all 41 governed ledgers, 924 rows were walked at every depth. Two carry `authorWarn: true`, both `planned` (`object.externalSharingModel`, `translation.flows`). None is `live`. Before the fix, `mapping.connectorSource` was the only `live` one, so no other row has this defect. - **H4, describe text.** Done as above. Author-facing text cites no tracker number. ## Pins - `packages/spec/src/data/mapping-connector-source.test.ts`: every key of the binding is `live`, and no row of it, at any depth, carries `authorWarn` / `authorHint`. The description still says nothing schedules a pull. The old assertion `authorWarn === true` pinned the defect. - `packages/spec/scripts/liveness/check-liveness.test.ts` runs the real gate via `--ledger-root`: - green on the shipped ledgers, with the census line showing warned `planned` rows and none on a `live` row (the control); - red when a `live` container row with `children` opts in (the regression's shape); - red when a drilled `live` child opts in. - The carriers are picked from the shipped ledgers by shape, not by name. - `packages/lint/src/lint-liveness-properties.test.ts` runs against the real ledger. The card's fixture lints without throwing and draws no `connectorSource` finding. Control: `object.externalSharingModel` (`planned` + `authorWarn`) still warns as `liveness-planned-property`. - `packages/lint/src/runtime-gate.inert-type-writes.test.ts`: a `mapping` write that authors `connectorSource` gets no errors and no advisories at the runtime door. - `packages/cli/test/validate-lint-mapping-connector-source.test.ts` is in the integration tier and spawns the CLI: - the card's fixture: `os validate --json` exits 0 with `valid: true`, and `os lint --json` exits 0 with `passed: true`; - control: adding `externalSharingModel` produces a `liveness-planned-property` warning, still at exit 0. ## Reverse verification (one-time, not a permanent file) This was run twice on the committed fix, with identical results: once at `7bc7c5c26`, and again after the `main` merge at `b6d58dd04`. `node scripts/ablation-replace.mjs` put `"authorWarn": true` back on the `connectorSource` row. The anchor count went 1 → 0, and the blob changed `49cc72e394b8` → `65665b90b21d`. With the defect back, every leg went red: - door: `validate` exit 1 and `lint` exit 1, with the sentinel in both; - `check:liveness` exit 1 (`1 on a live row — FORBIDDEN`); - spec pins: 2 failed; - lint pins: 3 failed; - CLI door pin: 3 failed. The tool restored the file and proved it: the blob equals HEAD and `git diff HEAD` is empty. The ledger is read from `packages/spec/liveness/` at runtime, not from `dist/`, so no rebuild was involved. ## Verification (all at `b6d58dd04`, the PR head) `origin/main` `fde553c50` was merged in with `bash scripts/pm/os-regen-merge.sh`. The merge was clean. `registry.ts` auto-merged as text, and `check:migration-registry` then confirmed it. Main had not touched the two reference pages, so they kept the branch's bytes. No regeneration commit was needed: after a fresh spec build, `check:generated` reports all 15 generated artefacts up to date. The delta against `main` is unchanged: 19 files, +538 / −42. | what | command | result | |---|---|---| | spec build | `pnpm --filter @objectstack/spec build` | exit 0 | | generated artefacts | `pnpm --filter @objectstack/spec run check:generated` | exit 0, "All 15 generated artifacts are up to date" | | CLI closure build | `pnpm exec turbo run build --filter=@objectstack/cli... --concurrency=2` | exit 0, 59/59 tasks | | the card's fixture | `os validate --json` / `os lint --json` | exit 0 / exit 0, sentinel 0 times, `connectorSource` 0 times | | ledger integrity | `pnpm --filter @objectstack/spec run check:liveness` | exit 0, "0 on a `live` row" | | strictness ledger | `pnpm --filter @objectstack/spec run check:strictness-ledger` | exit 0 | | spec tests | `vitest run scripts/liveness src/data src/integration/connector-sync-retirement.test.ts src/migrations` | exit 0, 130 files, 4269 passed, 1 todo | | spec typecheck | `pnpm --filter @objectstack/spec typecheck` | exit 0 | | lint, whole package | `pnpm --filter @objectstack/lint exec vitest run` + `typecheck` | exit 0, 118 files, 5489 passed; typecheck exit 0 | | CLI unit tier | `vitest run --project unit test/lint test/validate src/commands/validate src/commands/lint` | exit 0, 11 files, 125 passed | | CLI door pin | `vitest run --project integration test/validate-lint-mapping-connector-source.test.ts` | exit 0, 3/3 | | eslint, narrowed | `pnpm exec eslint --no-inline-config --format json` on all 19 changed paths | exit 0. eslint linted 12 and reported 7 (`.md` / `.mdx` / `.json`) as "no matching configuration"; 0 errors, 0 warnings | | derived gates | `dispatch-gates.mjs --commands` (117), each run, then `--ran` | 117 run: 116 exit 0, and 1 exit 1. That one is `check-empty-changeset --base origin/main`, the deliberate correction above. `--ran`: "117 run, 0 NOT-MEASURED, 0 UNRUN". `check:pm-dispatch-gates` was killed by a timeout at 400s and then at 540s. It was then run to completion and exited 0; its battery took 1432s on this box | **The eslint narrowing is a measurement, not a skip:** - The population comes from eslint's own answer for each changed path: 12 linted, and 7 that `eslint.config.mjs` has no configuration for. - The file count comes from the `--format json` output. - The config enables no type-aware linting (no `parserOptions.project`), so this diff cannot move the verdict on any file it did not touch. ## Acceptance notes - **turbo 2.11.5 wrote into `AGENTS.md` before the merge.** It appended its managed `turborepo-agent-rules` block on every AI-attributed turbo run in this worktree: 3 builds, and once during `check:type-check-debt`. Each time the file was restored with `git checkout HEAD -- AGENTS.md`, and `git diff HEAD` was confirmed empty. Files were staged by path only, so the block was never staged and is not in this PR. PR objectstack-ai#21151 (`agentGuidance: false` in `turbo.json`) came in with the merge. Since then, `AGENTS.md` stayed unmodified across the 59-task CLI closure build and all 117 gates. - `@objectstack/lint` gets no changeset. Its source change is comments only. Measured: tsup keeps comments, so the comment text does reach `dist/*.js` bytes. The seat can override this. - objectstack-ai#21127 is cited only where agents and reviewers read: the `authoring-rules.ts` comment, and the ledger's `note` and README prose. Author-facing text (`.describe()`, the retired-key prescription, the upgrade entry) cites no tracker. --- _Generated by [Claude Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…erdict model the lint ships (objectstack-ai#21450) Fixes objectstack-ai#21135 Clause-②: no ## What this changes `packages/spec/liveness/README.md`, section "Author warnings — closing the loop", now describes the model `packages/lint/src/lint-liveness-properties.ts` ships on `main`: - A `dead`, `live-elsewhere` or `experimental` verdict warns on its own. `authorWarn` opts in a `planned` row, and nothing else. - Every warning shows the row's `authorHint`, or else the verdict's default hint. The ledger `note` is never shown. - Rule 1 is about the verdict, not the marker. Rule 2 (booleans) covers any materialized default. - The coverage paragraph states the walk's real reach. Two sentences outside the section restated the old opt-in model, so they are fixed too (dispatch Zone 2, item 4). There is also one `@objectstack/spec` `patch` changeset. ⛔ No ledger row changes. ⛔ No lint code change. The paragraph "And one the gate enforces for you: never on a `live` row" was re-checked against the code and is unchanged (see Premise check, item 1). ## Premise check (dispatch Zone 2, measured at `535d1d25ab`) 1. **The hint chain had moved again since the card's table. The section is written to the code, not to the card or the triage.** - `shouldWarn` (`lint-liveness-properties.ts:173-176`) admits a row whose status is `dead`, `live-elsewhere` or `experimental`, or whose `authorWarn` is `true`. - `checkItem` (`:407-410`) builds the hint as `entry.authorHint ?? defaultHint`, for every row. Commit `327391c3af` (PR objectstack-ai#21169, landed for objectstack-ai#21096) removed the `note` fallback that opted-in and `experimental` rows still had. So triage's chain ("`note` only for opted-in and `experimental` rows") no longer holds. The README now says the `note` is never shown, on any row. - `live` + `authorWarn`: `describe()` throws its integrity sentinel (`:283`). `check:liveness` refuses the combination at every depth (`scanAuthorWarnRows` in `check-liveness.mts`, from commit `9bdc6d3faa`, PR objectstack-ai#21176, landed for objectstack-ai#21127). The README paragraph objectstack-ai#21176 added says exactly that, so it stays as it is. - **Probe** (tsx over `src`, against the shipped ledgers): - A view container `name` / `label` → `liveness-dead-property`, hint "Remove it — it is declared in the spec but not consumed at runtime." - `rowLevelSecurity[].label` / `.description` → the same. - `object.externalSharingModel` (`planned` + `authorWarn`, no `authorHint`) → `liveness-planned-property`, default "Keep it — …" hint. - `action.outcomeMessages` (`planned` + `authorWarn` + `authorHint`) → its `authorHint`. - `agent.lifecycle` / `tool.outputSchema` (`experimental`) → the default hint, not the `note`. - `manifest.runtime` (`live-elsewhere`) is admitted to the warn map, but `lintLivenessProperties({ manifest })` gives 0 findings: no walk visits `manifest`. - `dashboard`'s warn map holds no depth-2 key (`widgets.chartConfig.*` are `dead`), so the lint reads one level of `children`. - Synthetic: a `dead` row gives byte-identical findings with and without `authorWarn`. A `live` + `authorWarn` row throws. 2. **Stale sentences re-located.** At `535d1d25ab` they sit at `README.md:560` ("opt-in per entry"), `:564` (the `experimental`-only default), `:565` ("falls back to `note`"), `:569-572` (rule 1), `:573-577` (rule 2) and `:587-591` (coverage). The same section also had two false statements the card did not list: - "(see `enable.searchable`'s `_authorWarnSkipped`)": no ledger row at any depth carries `_authorWarnSkipped` (census over all 41 ledgers, 940 rows), and `enable.searchable` is `live`. - "It covers **every governed type**": 11 governed types with warning rows are not walked (`manifest`, `connector`, `analytics_cube`, `query`, `realtime_subscription`, `api` and the five `RestServerConfig` families). 3. **No generated file lifts this section.** `pnpm --filter @objectstack/spec run check:generated` passes with "All 15 generated artifacts are up to date", `check:docs` included. The README's only programmatic readers are `check-liveness.mts`, `build-state-counts.mts` and `readme-table.mts`, and all three read the *Current state* table only. `git grep` for the section's phrases over `content/`, `apps/` and `packages/spec/scripts` gives 0 hits. 4. **The old model elsewhere.** `git grep -n "opt-in per entry\|authorWarn"` over `packages/spec/liveness/` and `content/docs/`: - `README.md:379` ("the honest status is `dead` + `authorWarn`") and `:966-969` ("every misleading entry carries `authorWarn`", plus "must also be registered in `TYPE_COLLECTIONS`") state the old model as current. Both are fixed here. - `:348` / `:360` (the dated 2026-07 tally), `:434`, and the *Current state* Notes cells are dated history, not the current model. They are unchanged. - `content/docs/`: the only hits are `releases/v14.mdx:245` and `releases/v17/index.mdx:175`. Both are release-owned history, so there is no owner to notify. `permissions/authorization.mdx:524` and `ui/translations.mdx:390` describe warnings in terms that still hold. ## Every sentence changed, before → after **Section heading** - Before: `## Author warnings — closing the loop (`authorWarn`)` - After: `## Author warnings — closing the loop (verdicts, and `authorWarn`)` **Intro paragraph** - Before: "Classification is also fed back to the *author* at build time. The CLI `compile` lint (`packages/lint/src/lint-liveness-properties.ts`) reads these ledgers and emits an advisory **warning** when an authored object/field sets a property that is misleading — "you set this expecting it to do something; at runtime it does nothing / isn't enforced" — with a corrective hint. Never fails the build." - After: "Classification is also fed back to the *author*. The liveness lint (`packages/lint/src/lint-liveness-properties.ts`) reads these ledgers and emits a **warning** when an authored item sets a property whose row warns — "you set this expecting it to do something; at runtime it does nothing / isn't enforced / isn't read yet" — under a rule id per verdict, with a corrective hint. `os lint`, `os validate` and `os build` / `os compile` run it, and so does the runtime metadata write door for `email_template`, `mapping` and `datasource` items. It is never an error and never fails `os build`, but `os lint --strict` and `os validate --strict` turn every warning into exit 1." - Sources: `AUTHORING_COMMANDS` and `runtimeTypes` in `authoring-rules.ts`, and `failing = errors.length + (strict ? warnings.length : 0)` in `lint.ts`. objectstack-ai#21092 measured the `--strict` 0 → 1 flip at both doors. **The opt-in line** - Before: "Signal over noise is the whole point, so warnings are **opt-in per entry**:" - After: "**The verdict decides which rows warn.** `authorWarn` only opts in a row whose verdict does not warn on its own:" It is followed by a new five-row table: `dead`, `live-elsewhere` and `experimental` always warn, `planned` only with `authorWarn`, and `live` never. Each row names its rule id. **`authorWarn` field row** - Before: "warn when this property is authored (in addition, any `experimental` entry warns by default — it's a declared-but-unenforced guarantee)." - After: "makes a `planned` row warn. On a `dead`, `live-elsewhere` or `experimental` row it changes nothing, because the verdict already warns. So leaving it off never keeps such a row quiet." **`authorHint` field row** - Before: "the corrective one-liner shown under the warning (falls back to `note`)." - After: "the corrective one-liner shown under the warning, on every row that warns. Without one, the verdict's default hint is shown: `dead`'s says "Remove it", `planned`'s and `live-elsewhere`'s say "Keep it", and `experimental`'s says the guarantee is not yet enforced. The row's `note` is never shown to an author, on any row: it is written for this ledger's maintainers." **Rules intro** - Before: "Two rules keep it false-positive-free, **both of which the marker author must respect**:" - After: "Two rules keep the warnings truthful. **Both bind whoever grades a row, not only whoever marks one**:" **Rule 1** - Before: "**Only mark genuinely *misleading* dead props** — ones that imply a capability/behavior that doesn't exist (`versioning`, `field.columnName`, `softDelete`). Benign display/doc metadata that's "dead" (no runtime reader) — `description`, `tags`, `icon` — must NOT be marked; an author isn't misled by them." - After: "**Grading a row `dead`, `live-elsewhere` or `experimental` is an author-facing act.** The verdict warns every author who sets the key, and fails their `--strict` run. No marker keeps it quiet. That includes benign display or doc metadata with no runtime reader (`description`, `tags`, `icon`): graded `dead`, it warns "Remove it" like any other dead key." It is followed by three bullets: - Measure a display key against *Designer previews count as consumers* before writing `dead`. A key that something shows to a person is `live`, and that measurement, not a missing marker, is what keeps it quiet. - A display key still `dead` after that warns, and the warning is the measurement speaking. ⛔ Never grade a row `live` or `planned` to silence it. - When the verdict's default hint is the wrong corrective, give the row an `authorHint`. The `note` cannot do that job. **Rule 2** - Before: "**Booleans: only mark `default(false)` flags.** The lint warns on a boolean only when set `true`, and it can't tell author-set-`true` from a schema default. A `default(true)` flag (`enable.searchable`) would then warn on *every* object that has an `enable` block — so leave those unmarked (see `enable.searchable`'s `_authorWarnSkipped`). Object/string/array props warn when merely present, so this caveat is boolean-only." - After: "**Booleans and materialized defaults.** The lint warns on a boolean only when it is `true`, and on any other value when it is present at all. Where the stack it reads has been parsed (`os validate`, `os build`, any `defineStack` config), schema defaults have already filled in, so it cannot tell an authored value from a default. A `default(true)` flag, or any key whose default materializes, would warn on every item that carries the default, whoever wrote it. So `authorWarn` stays off such a row, and a warning verdict on one warns every such item. `enable.searchable` (`default(true)`) is the shape; it is graded `live`, so the question does not arise for it. `mapping.errorPolicy` and `batchSize` were dead keys of this kind, and no warning could reach their authors truthfully, so they were retired instead (the `mapping` row below)." - The old "boolean-only" sentence contradicted the README's own `mapping` row, which calls those two keys "the non-boolean instance of the default(true) rule". On parsing: the rule's input tier is `parsed`, and `defineStack` runs `ObjectStackDefinitionSchema.safeParse` by default. `os lint`'s own comment says its `parsed` tier fills no defaults for a raw config, which is why the sentence names the doors where defaults do fill. **Coverage paragraph** - Before: "The lint is ledger-driven: coverage grows by marking more entries `authorWarn`, not by touching the lint code. It covers **every governed type**: objects (incl. `enable.*`) and their fields walk bespoke nesting; flows/actions/agents/tools/skills/datasets/permissions/hooks/pages are checked as flat stack collections, and container properties fan out over arrays (each flow node, each dataset measure)." - After: "The lint is ledger-driven: coverage grows with the ledger's verdicts (grading a row `dead`, `live-elsewhere` or `experimental`) and with `authorWarn` opt-ins on `planned` rows, not by touching the lint code. That holds only inside the types its walk visits: objects (incl. `enable.*`) and their fields walk bespoke nesting, translation bundles walk their locale entries, and every type listed in the lint's `TYPE_COLLECTIONS` is checked as a flat stack collection, with container properties fanning out over arrays (each flow node, each dataset measure). It reads a row and its direct `children`, no deeper. A governed type the walk does not visit (`manifest`, `connector` and `realtime_subscription` are three) warns no author through this lint, whatever its rows say." **Outside the section, `README.md:379`** - Before: "When in doubt, the honest status is `dead` + `authorWarn`: an author who gets a" - After: "When in doubt, the honest status is `dead`, which warns its authors with no `authorWarn` marker (see *Author warnings* below): an author who gets a" **Outside the section, below the *Current state* table** - Before: "The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every misleading entry carries `authorWarn` so authors hear about it at compile time (governed types with warn entries must also be registered in the CLI lint's `TYPE_COLLECTIONS` — see lint-liveness-properties.ts)." - After: "The `dead` set across types is the enforce-or-remove worklist (ADR-0049); a `dead` row warns its authors at compile time by its verdict alone, with no `authorWarn` marker (see *Author warnings* above). That reaches an author only in a type the lint walks: a governed type's warning rows are heard only once the lint visits the type, which for a flat stack collection means registering it in the lint's `TYPE_COLLECTIONS` (see lint-liveness-properties.ts)." - Measured: 0 `dead` rows carry `authorWarn` today. The only `authorWarn` rows are 3 `planned` ones: `action.outcomeMessages`, `object.externalSharingModel` and `translation.flows`. The old "must also be registered" did not hold either: `manifest` is deliberately unwalked, and objectstack-ai#21092 pinned that. ## Changeset: measured, not assumed - `npm pack --dry-run --json --ignore-scripts` in `packages/spec` lists 2077 files, and `liveness/README.md` is one of them (control: `liveness/view.json`, also listed). The README ships, so it gets a `patch` changeset for `@objectstack/spec`: `.changeset/21135-liveness-readme-author-warnings.md`, carrying `Clause-②: no`. - The `Check Changeset` job (`pr-automation.yml`) has no path exemption. It is skipped only by the `skip-changeset` label or the release PR. ## Verification (all at head `a3aed7da52`) | What | Command | Result | |---|---|---| | spec build (empty closure) | `pnpm --filter @objectstack/spec build` (under the verify lock) | exit 0 | | generated artifacts | `pnpm --filter @objectstack/spec run check:generated` | exit 0, "All 15 generated artifacts are up to date" | | spec tests (the card's done-when) | `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2` (lock) | exit 0: 600 files, 17684 passed, 1 todo | | the lint the section documents (not touched; evidence that the model described is the pinned one) | `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/lint-liveness-properties.test.ts` (lock) | exit 0, 95 passed | | derived gates | `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths; 2 paths vs merge base `535d1d25a`), each command run, exit captured before any pipe | 60 derived: 58 exit 0, 2 exit 3 (below) | | reconciliation | `dispatch-gates.mjs --ran ran.list` | "60 derived, 58 run, 2 NOT-MEASURED, 0 UNRUN" | | roster gates whose roster sits in a directory this diff touches | `check-changeset-fixed`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity` | all exit 0 | | liveness gates | `check:liveness`, `check:empty-state`, `check:strictness-ledger`, `check:variant-docs` (in the derived set) | all exit 0 | | nul bytes, plus a hand scan for control bytes | `pnpm check:nul-bytes`, and a `grep -P` control-byte scan over both files | exit 0; 0 hits | **NOT MEASURED (declared to CI).** Both report `PREREQUISITE NOT MET`: they load built `dist/` entry points this worktree does not have. Neither reads anything this diff changes, since a markdown file and a changeset are emitted into no `dist/`. The `Lint & Repo Gates` job measures both on a fresh full build. - `pnpm check:dual-build-cjs-loads` needs every package's `dist`. - `pnpm check:lean-entry-closure` needs the `@objectstack/objectql` closure's `dist`. **Typecheck not run (declared).** The diff has no TypeScript, and neither `.md` file is in any tsc program. **eslint, narrowed and measured.** `pnpm exec eslint --no-inline-config --format json` was run over both changed paths. - Population, from eslint's own answer: 2 files, and both report "File ignored because no matching configuration was supplied". The config's file globs are code extensions only (`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`). - Count, from the JSON output: 2 results, 0 errors, 0 lint messages. - Invariance: `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project` / `projectService`), so this diff cannot move the verdict on any file it does not touch. ## Acceptance notes These are observations, not filed. - **Stale `_authorWarnSkipped` mentions elsewhere.** No ledger carries the field any more. It is still named in spec source comments (`data/mapping.zod.ts:30`, `ui/app.zod.ts:1153`, `conversions/registry.ts:3013`) and in two dated *Current state* Notes cells (`mapping`, `qa`). These are comments and history, with no runtime or author-facing effect. Carrier: none. - **Ledger notes on `view.label`, `rowLevelSecurity.label` and `.description`** end with "Not authorWarn'd" / "Benign, not authorWarn'd". That is true of the marker, but no longer means silence: the verdict warns. This card touches no ledger JSON, so they are unchanged. The rows themselves were measured under the designer-previews ruling (objectui `db11afd4967`), so the restated rule 1 implies no re-grade. Carrier: none. - **Stale reasoning in code comments.** `kernel/metadata-plugin.zod.ts:947` reasons from "`datasource.json` carries 0 `authorWarn` rows". Its conclusion still holds, because `datasource` has no warning-verdict row, but the reason is the old model. The lint's own docblock (`lint-liveness-properties.ts:720`) says "Covers every governed metadata type", which the coverage measurement above contradicts. Both are comments, and lint code is out of this card's scope. Carrier: none. - **Upstream since `535d1d25ab`.** `origin/main` moved 2 commits, touching `liveness/analytics_cube.json` and `liveness/dataset.json` only. Neither touches a sentence here or a file in this diff. --- _Generated by [Claude Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #16094
Clause-②: no
Ruling-ref: 5560227939
What changes
shouldWarninpackages/lint/src/lint-liveness-properties.tsnow admits a ledger row whose status isdead,live-elsewhereorexperimental, or that opts in withauthorWarn: true. Before, onlyexperimentalwarned without an opt-in.describe()'s mapping toliveness-dead-property/liveness-live-elsewhere-propertyis unchanged. Before the change, both of those rule ids were exported and could never be produced: across the shipped ledgers, not onedeadorlive-elsewhererow setauthorWarn.checkItemchanges only for rows the ruling newly admits. A row that warns only because of itsdead/live-elsewhereverdict (noauthorWarn) shows itsauthorHint, else the verdict's existing default hint, and never its ledgernote. Rows that opt in withauthorWarn, andexperimentalrows, keep their hint exactly as before. This is the seat's Q3 answer (card comment 5925292339).Other changes:
lintLivenessPropertiesregistry entry inauthoring-rules.tsis corrected too.minorchangeset for@objectstack/lint, carryingClause-②: noin its body.Recount on
mainMeasured at base
6073bb96b8, all depths:deadlive-elsewhereretiredKeytombstones (check:liveness --json)dead, 1live-elsewhere)The 4 reachable rows are
view.name,view.label,permission.rowLevelSecurity.labelandpermission.rowLevelSecurity.description.The other 9 authorable rows sit in types the walk never visits:
connector.metadatamanifest.runtimeandmanifest.integrityrealtime_subscriptionrowsThe 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-propertyis produced against the shipped ledger from its onelive-elsewhererow. That row ismanifest.runtime.Measured: no stack can produce that id through
lintLivenessProperties.manifestis not inTYPE_COLLECTIONS, and it is not one of the bespoke walks (objects/fields, translation bundles).stack.manifestis a single object, not a collection.authorWarnedPropertieshas one caller outside its package,packages/cli/src/utils/i18n-extract.ts, and that caller asks only abouttranslation. 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:
authorWarnedProperties('manifest').has('runtime').LIVENESS_LIVE_ELSEWHERE_PROPERTY:checkItemAgainstWarnMapover the row as read from the shippedmanifest.json.manifest.lintLivenessProperties({ manifest: { runtime, integrity } })says nothing about either key. This pin goes red the day any walk visitsmanifest, and its comment namesmanifest.integrity.Pin 3 matters because
manifest.integrityisdeadin the ledger, yetos plugin publishreads 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
manifestis not walked, the flip does not surfacemanifest.integrityto any author.--strict, and whyClause-②staysnoNothing is refused, and nothing changes without
--strict.os lint --strictandos validate --strictgo from exit 0 to exit 1 on a stack that was otherwise warning-clean and authors a view containerlabel/nameor an RLS policylabel/description.Measured with the CLI built from source on fixture stacks, before (base) and at this head:
os lintos lint --strictos validateos validate --strictlabellabel+descriptiondefineStack) with tombstonedlist.stripedThe
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.mdand lintd753744.Other doors:
os buildhas no warning-promoting flag.email_template/mapping/datasourceonly, and none of those gains a row.os lint --evalcorpus has no views and no RLS.check:i18n-coveragecounts onlyi18n/rules.Bump: minor. The at-tier contract review (
5925918475) set this, and it corrects the earlierpatch. 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 shapeminor:.changeset/20654is a new advisory withClause-②: noatminor, and the cli 17.5.0 entry grades "the new advisories and the newly reachable non-zero exit"minor.d753744is a fix to an existing finding, not a precedent for this. The changeset carriesClause-②: noin its body.Who starts warning
These are this repository's example apps, run with
os lint --jsonandos validate --json, at base and at this head:app-showcaseliveness-dead-property: permissionshowcase_contributor,rowLevelSecurity.labeland.descriptionapp-crmapp-todoapp-multi-packageAll 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-propertyfindings (externalSharingModel) are byte-identical, message and fix, between base and this head.Retired keys
A
retiredKeytombstone keeps itsdeadrow.os validate,os buildand the runtime gate refuse the key first, so it gets no second report. AdefineStackconfig with a retired key failsos lintat load as before.os linton a raw config.os lintdoes not Zod-parse. A raw config withoutdefineStackthatos lintaccepts, and that carries a retired key, now gets aliveness-dead-propertywarning 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
deadgrade, with the parse control alongside:ViewSchemarefuseslist.striped.Tests
All results below are at head
49fb6cfad3, after mergingorigin/mainat9c8b65aa23(which carries #21047).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.@objectstack/lint: 118 files, 5486 tests passed.pnpm --filter @objectstack/lint typecheck: exit 0.lint-per-package-authoring-paritypasses 5/5.dispatch-gates --commands: 61 families, all exit 0.--ran: 61 derived, 61 run, 0 NOT-MEASURED..tsfiles: 0 errors, 0 warnings.The new pins, end to end against the shipped ledger:
deadkey,rowLevelSecurity.label/.description, producesliveness-dead-property;PermissionSetSchema;manifestpins described above.Hint pins, ledger-wide:
authorWarnorexperimental) keepauthorHint ?? notebyte for byte. Anti-vacuity: 6 of them show their note today.Ablations, through
scripts/ablation-replace.mjs: the anchor hit and the blob moved each time. Every restore left the blob equal to HEAD andgit diff HEADempty, under a trap.{experimental}(pre-ruling behaviour)authorHint ?? note ?? defaultThe test subject resolves to
srcthrough the relative import, so no dist build was involved in any ablation.Narrowed eslint (the 4 changed
.tsfiles, as in Tests above):eslint.config.mjs's**/*.{ts,…}andpackages/**/*.{ts,…}blocks select all four; the changeset is not linted.parserOptions.projectorprojectService), so this diff cannot move any untouched file's verdict.Control bytes. A self-scan of the 5 changed files finds none.
check:nul-bytesexits 0.The CLI parity fixture, re-judged
The #18778 fixture in
packages/cli/test/lint-per-package-authoring-parity.test.tsdeclared view containers as{ name, label, object, list }.view.nameandview.labelaredead, so after the flip the union run raised 4 warnings, which broke the fixture's premise that the union raises nothing. CI on64e0275024failed two tests (expected 5 to be 1, at:241and:279). The containers now bind byobjectalone (list.labelis live and stays), with a comment saying why. Each test keeps its intent: the union is clean, there is one per-package survivor, and--strictfails with 1.Acceptance notes
object.externalSharingModelisplanned+authorWarnwith noauthorHint, so authoring it prints its ledger note as the fix. That note is 728 characters and cites#2696. Measured onapp-showcaseat 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.manifest.integrityre-grade it would need first. These have zero measured pull. No example or plugin config in this repository authorsruntimeorintegrity;os plugin buildwritesintegrity. The note that deferred the row's status answers 404. No carrier.connectoris 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", andconnectoris not inTYPE_COLLECTIONS. Registering it would warn on every authoredmetadatabag. No carrier.88b484e00c) and is merged here. The field-set pin is measured, not assumed, at['conditionalRequired']:field.picklistislivewith noauthorWarnafter feat(objectql): resolve picklist references at runtime — served options, additive extensions, write validation, load-time refusal #21047, andfield.conditionalRequiredis adeadtombstone.origin/mainwas merged at9c8b65aa23(b0705c1530). The 6 latermaincommits touch neitherpackages/lint,packages/spec/livenessnor the CLI parity test.mapping.connectorSourcecrash onmain, not this PR's.mainshipsmapping.connectorSourceaslive+authorWarn(8368f1c), and that crashesos lintandos validateon any stack authoring it:describe()throws its integrity sentinel. This PR's hint control is scoped to rowsdescribe()answers, and its COVERAGE pin keeps the sentinel loud. Filed as [finding]os lint/os validatecrash on any stack whose mapping authorsconnectorSource: the ledger row islivewithauthorWarn: true, and the liveness rule throws its integrity sentinel #21127.Generated by Claude Code