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 |
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