Skip to content

feat(lint): a ledger-dead or live-elsewhere key warns without an authorWarn opt-in, and never shows the ledger note (#16094) - #21092

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-16094-liveness-dead-warns
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-16094-liveness-dead-warns

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

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


Generated by Claude Code

claude added 4 commits October 1, 2026 04:45
…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>
…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 9 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via authorWarn (literal, a string literal in a comment on a changed line))
  • content/docs/releases/v17/index.mdx (via authorWarn (literal, a string literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9c8b65aa23781f2c1d2aa0abe92fc11e0e71b3a1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1df43dac3cacbe7ec536a06d92d130a9b1a26a44 — the merge of head 49fb6cfad312b18cd24f397b8631454bdd375485 into base 9c8b65aa23781f2c1d2aa0abe92fc11e0e71b3a1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9c8b65aa23781f2c1d2aa0abe92fc11e0e71b3a1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 64e0275024e442ee52b095ce6c218a405f39034e
Local-runs: none

Scope: PR #21092 on card #16094, net diff against main (4 files, +361 −75), the card's body and all ten comments (ruling 5560227939, audit 5567387110, re-queue 5857212133, seat answers 5925292339, both os-dev-reports), the PR body and its one comment, and the head's check-runs read twice (05:55Z while 17 were running, 06:16Z after all 38 completed). Ledger counts below were read off the ledger JSON at the head and at current main (a read of repository data, no build, test or gate run); the merge state was read with an in-memory git merge-tree, no checkout.

① Derived judgments

Accept set and public surface, each named.

  1. shouldWarn now admits status ∈ {dead, live-elsewhere, experimental} || authorWarn === true through one VERDICTS_THAT_WARN set; describe()'s mapping to the two rule ids is untouched. RIGHT, and exactly the ruled formula.
  2. Public surface: nothing added or removed. RULED_VERDICTS, VERDICTS_THAT_WARN and isVerdictTriggered are module-private; both rule ids were already exported. RIGHT.
  3. Default-face accept set: unchanged. The rule is tier: 'advisory'; lint.ts:1052 counts failing = errors + (strict ? warnings : 0), validate.ts:81 documents --strict as "treat warnings as errors". Nothing is refused at any default door. RIGHT.
  4. os lint --strict / os validate --strict move 0 → 1 on a stack that authors one of the four keys. RIGHT as a fact; its classification is ②.
  5. The hint expression authorHint ?? (isVerdictTriggered(entry) ? undefined : entry.note) ?? defaultHint: a row warning only by a dead / live-elsewhere verdict never reaches note; a row with authorWarn: true or experimental status takes the pre-change branch byte for byte. On the head ledger, 105 verdict-triggered rows sit at the depth the warn map reads (24 types), 0 carry an authorHint, 80 notes cite a tracker id; 9 rows warned before the ruling (8 on current main, field.picklist having gone live in feat(objectql): resolve picklist references at runtime — served options, additive extensions, write validation, load-time refusal #21047), and 0 dead / live-elsewhere rows carry authorWarn. So no internal note is routed to an author on any newly warned row, and no pre-existing row's output changes. RIGHT; the three Q3 pins and the ledger-wide CONTROL pin hold it.

The changeset, sentence by sentence (.changeset/16094-liveness-dead-warns.md).

  1. "both rule ids were exported and never produced, because no shipped dead or live-elsewhere row opted in" — RIGHT (0 such rows at head and at main).
  2. "warns when its status is dead, live-elsewhere or experimental, or when it sets authorWarn: true (still the only way a planned row warns)" — RIGHT.
  3. "four are dead today: a view container's own name and label (the defineView container, not list.label), and … rowLevelSecurity[].label and rowLevelSecurity[].description" — RIGHT. Of the 110 dead + live-elsewhere rows, every walked one except those four has a retiredKey( tombstone in the spec at the head (list.tabs included, since spec: re-derive the liveness ledger before its verdicts start warning authors — 3 errors in 33 rows (9.1%), list.tabs flipped to dead #16542's re-grade and its retirement; rowLevelSecurity.priority / .tags and objects.allowRestore / .allowPurge are retiredKey in rls.zod.ts / permission.zod.ts). view.list.label is live. The 13 authorable rows are those four plus connector.metadata, manifest.runtime, manifest.integrity and six realtime_subscription rows; the other un-walked types' dead rows are all tombstones.
  4. "No walk visits manifest, connectors or realtime subscriptions" — RIGHT; none is in TYPE_COLLECTIONS or a bespoke walk.
  5. The hint paragraph: the quoted default is byte-identical to describe()'s dead hint; "never shows the ledger's internal note"; "Rows that opt in with authorWarn, and experimental rows, show exactly the hint they showed before" — RIGHT (item 5).
  6. Retired keys: retiredKey() is z.never with the guidance, so every Zod door refuses first; os lint runs normalizeStackInput, which metadata-collection.zod.ts documents as "applied to the raw input before Zod validation" and converts only, so a raw config with a retired key is graded dead where it got nothing. RIGHT. The runtime publish gate runs this rule only for email_template / mapping / datasource (registry runtimeTypes), none of which gains a row.
  7. "os lint --strict and os validate --strict now exit 1 instead of 0 …" and "Nothing is refused, and nothing changes without --strict" — RIGHT.
  8. "only app-showcase gains warnings: two, on one permission set's policy label and description, and no example's exit code changes" — RIGHT. showcase_contributor carries three policies with both keys; checkItem breaks after one finding per item-and-path, so two findings. The other examples author list.label only (live), views as arrays, no RLS label / description.
  9. "To clear the warning, delete the key: nothing reads it" — RIGHT per the ledger's own evidence (all four keys optional in the schema; the notes record no mounted reader). Reading caution for the spec lane, no carrier: ViewSchema.label's description still says "Human-readable label shown in metadata lists", which the ledger's dead verdict contradicts in wording; and the view.name note says the platform's own writers send name (saveMetaItem, artifact-shipped containers, the GET /meta/diagnostics sweep) — the CLI-only walk never sees those writers, so the warning reaches only a name the author wrote, but see item 17.

The pin requirement. The dead end-to-end pin is delivered against the shipped ledger (RLS label / description through lintLivenessProperties, live siblings silent, fan-out past index 0, fixture parses through PermissionSetSchema, tombstone refused at parse as the control). The literal live-elsewhere pin is measured impossible — manifest is unwalked — and the substitute (row admitted; row maps to the id through checkItemAgainstWarnMap; no walk visits manifest, red by name the day one does) is EQUIVALENT for the ruling's stated purpose, a ledger change that empties the verdict goes red by name, and adds a reach sentinel that keeps manifest.integrity off authors. "Stronger" in the PR body is overstated: the id remains unproducible through any command, and the changeset says so. Accepted; the PR-body word is not shipped prose.

What the diff gets wrong or misses.

  1. The head is red on a derived gate, and the red is this diff's. Test Core (5/6) (cli package tests) fails in packages/cli/test/lint-per-package-authoring-parity.test.ts, two tests, same signature AssertionError: expected 5 to be 1 at lines 241 and 279. That fixture authors name and label on its two view containers (pp_account, pp_order), so the flip adds four liveness-dead-property findings to a fixture pinned at one warning; the second failing test is the [finding] os lint is the THIRD door with the same union-only authoring-rule gap — #18677 closed two of three, and the card's own table said 2 of 2 #18778 pin titled "THE NARROWING — os lint --strict now EXITS 1". main at 88b484e00c is green on the same shard, so this is not a same-signature red on main. The dev's gate list ran @objectstack/lint tests and the 60 derived families, never @objectstack/cli's tests; the PR body's "No repo gate asserts a zero-warning count on the examples" was true and beside the point. Fix owed: re-judge that fixture (drop the two containers' name / label, which bind by object, or absorb the four findings) and run the cli package's tests before pushing.
  2. feat(objectql): resolve picklist references at runtime — served options, additive extensions, write validation, load-time refusal #21047 landed first. It merged at 06:08Z, after this head, and is on main as 88b484e00c: field.picklist is now live, and the head conflicts with main in lint-liveness-properties.test.ts. The field-set pin ['conditionalRequired', 'picklist'] is false against current main; the PR's own Acceptance note names the resolution (['conditionalRequired']). Merge main and resolve in the patch round.
  3. Reach: the cli fixture in item 15 is the shape the view.name note calls "the platform's own writers" — a container carrying its identity in the body. The changeset's blast-radius sentence is scoped to the example apps and true there; a project whose view containers carry name / label (as this repository's own fixture does) draws two warnings per container and flips under --strict. One sentence naming that shape would make the changeset's "who starts warning" complete. Reading caution only: a raw map-shaped views (off-spec, z.array and not in MAP_SUPPORTED_FIELDS) reaches recordsOf, which injects the map key as name, so os lint would say "sets name" about a key never written — on input every parsing door refuses; no fix owed.
  4. A comment in the diff is false: the enforce-or-remove: DashboardWidgetSchema 的 5 个 dead 键(#4956 下钻首次给出裁决) #5010 block in the test file says a retired key is graded dead "(the row's note, which records the removal, is the hint)"; after the Q3 fix those rows show the dead default, never the note. Fix in the patch round. Pre-existing and untouched: the translation-walk comment in lint-liveness-properties.ts says this rule is surfaces: CLI_ONLY; the registry says CLI_AND_RUNTIME with three runtimeTypes. Noted, not this diff's.

② Semver level

patch — WRONG; minor. Triage classed the card enhancement and wrote "not a bug"; the PR is feat(lint); two rule ids become producible, which to a consumer is two new advisories, and a non-zero exit becomes reachable under a published flag. The repo's own precedents on this shape both grade it minor: .changeset/20654-flow-credential-literal-advisory.md (a new advisory, Clause-②: no) is '@objectstack/lint': minor — the seat's Q1 answer cites it as patch, which it is not — and the cli 17.5.0 entry for os validate --strict reads "Graded minor rather than patch for the new observable step line, the new advisories and the newly reachable non-zero exit". The one patch precedent cited, d753744 (changeset 20400), is a fix that un-waived a mis-scoped waiver on an existing finding, not a new advisory. A patch release that can turn a --strict CI from green to red is the one thing a patch-range consumer is promised not to get.

Clause-②: no — the yes/no holds; the narrowing arm is a house split, named here. The criterion is "widens the accept set or enlarges the public surface" on a published contract face; nothing widens and no export moves, so no is right. On the arm: cli 17.5.0 (9bd631f, 2026-09-17, released) marks "os lint --strict can now fail a project it passed before" as Clause-②: yes (narrowing), BREAKING, with an adr-0087 not-required (no-migration-prescription) HTML-comment marker, and the cli test this head reds is that entry's "THE NARROWING" pin; lint .changeset/20654 (2026-09-29, pending) marks a new advisory Clause-②: no with "--strict promotes it, as it promotes every warning". The criterion's own words measure the published accept set at the default face, which does not move, so this record adopts no with no arm: ADR-0087 registration is owed only by a declared-breaking changeset (check-adr-0087-registration.mjs reads the BREAKING banner, a major bump, feat!:, or a (narrowing) arm), and none is declared. If the maintainer prefers the released cli reading, the changeset takes Clause-②: no (narrowing), the banner and the not-required (no-migration-prescription) marker, and the level stays minor either way.

The changeset body carries no Clause-② line. AGENTS.md Post-Task Checklist step 3 says the changeset body "also carries the PR's Clause-② line" and the registration gate "reads the arm there"; both cited precedents carry Clause-②: no; 164 of 234 pending changesets do. Add the line in the patch round. The --strict sentence, the "nothing is refused" sentence and the example-app sentence are true and stay.

③ Boundary flags

Round 1 open_questions (seat answers 5925292339).

  • Q1 (Clause-②, level): the seat's no holds; its patch does not (②), and its precedent reading mis-states 20654 as patch.
  • Q2 (live-elsewhere pin): option A accepted as delivered; the substitute is equivalent for the ruling's purpose plus a reach sentinel; "stronger" is overstated (①). manifest.integrity is not surfaced, pinned by name. The follow-up (a manifest walk after the spec lane re-grades manifest.integrity) has zero measured pull and stays an Acceptance note.
  • Q3 (author-facing text): fixed narrowly and verified in code and pins (① item 5); the stale enforce-or-remove: DashboardWidgetSchema 的 5 个 dead 键(#4956 下钻首次给出裁决) #5010 comment (① item 18) is the one loose end.

Round 1 out_of_scope_findings. (a) The pre-existing note leak on opted-in rows (object.externalSharingModel, 728 chars, cites a tracker id; 6 of the 9 pre-ruling rows show their note) is correctly left out of this diff as ruled — but "no carrier" is not an answer to a hard AGENTS.md rule measured on a shipped example: ESCALATED, a devx-lane card is owed (lint side, the opted-in branch of the same hint expression, or an authorHint on the row). (b) The authoring-rules.ts comment: fixed in this PR. (c) connector.metadata in an unregistered collection: noted, no carrier, agreed.

Round 1 deviations (7). No PR / no label-write: resolved by the patch round. Comments beyond shouldWarn and the ten re-judged pins: inside the amended claim surface, and each re-judge asserts the dead grade with the parse control alongside — agreed. /verify not run: no UI, agreed. origin/main not merged: merged in the patch round, now stale again (① item 16). Model-free commit trailers per AGENTS.md: agreed; this record carries no model identifier either. The --maxWorkers note: immaterial.

Patch round deviations (4). PR footer in AGENTS.md's form: agreed. /verify not run: agreed. "origin/main moved two commits, neither touches packages/lint or the ledger, not re-merged": superseded — #21047 merged at 06:08Z and touches both the test file and field.json (① item 16). Worktree removed: agreed.

Patch round out_of_scope_findings (3). As above: (a) escalated, (b) manifest walk and manifest.integrity re-grade — noted, pinned, no carrier, agreed, (c) connector.metadata — agreed.

Check-runs on the head, as read at 06:16Z: 30 success, 6 skipped, 2 failure (Test Core (5/6) and the Test Core rollup), 0 running. The red is PR-caused (① item 15), not a same-signature red on main. Check Changeset, Governed Surface Queue Guard, Lint & Repo Gates and every type-check job are green; the registration and no-major gates ask nothing of a changeset that declares no breaking change.

Owed before a PASS on the next head: (1) re-judge lint-per-package-authoring-parity.test.ts and run the cli package's tests; (2) merge main, resolve the test-file conflict, field-set pin to ['conditionalRequired']; (3) changeset to minor with a Clause-②: no line in the body (the narrowing arm, banner and marker only if the maintainer adopts the cli 17.5.0 reading), and one sentence naming view containers that carry their own name / label as a shape that now warns; (4) fix the #5010 test comment. The ruled flip, the hint fix and the pins are right as they stand and need no change.

Implemented-by: claude/issue-16094-liveness-dead-warns
Reviewed-by: session_01JAhu8u8QfBvRjVZDox7CP9

VERDICT: FAIL

claude added 4 commits October 1, 2026 06:25
# 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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 49fb6cfad312b18cd24f397b8631454bdd375485
Local-runs: none

Scope: PR #21092 on card #16094 at head 49fb6cfad3, net diff against main (5 files, +378 −79; merge-base 9c8b65aa23, which carries #21047 at 88b484e00c and #21084 at 8368f1c005), the card's body and all twelve comments (ruling 5560227939, audit 5567387110, re-queue 5857212133, seat answers 5925292339, the seat's public correction 5925927494, three os-dev-reports, newest 5927529740), the PR body as the seat edited it, its two comments including the prior FAIL record 5925918475 on 64e0275024, the check-runs on this head read once at 08:21Z, and issue #21127. Ledger counts below were read off the liveness JSON at the head with a read-only script in scratch (a read of repository data; nothing built, run or re-run). The liveness JSON is byte-identical between this head and current origin/main; the 7 main commits past the merge-base touch neither packages/lint, packages/spec/liveness, the CLI parity test nor the CLI's lint / validate commands, and the one examples/ change among them (#21104) is judged in item 13.

① Derived judgments

Accept set and public surface, each named (the source is unchanged since the prior record; re-read on this head).

  1. shouldWarn admits status ∈ {dead, live-elsewhere, experimental} || authorWarn === true through one VERDICTS_THAT_WARN set; describe()'s mapping to the two rule ids is untouched (its diff is comment lines only). RIGHT, the ruled formula.
  2. Public surface: nothing added or removed. RULED_VERDICTS, VERDICTS_THAT_WARN and isVerdictTriggered are module-private; both rule ids were already exported. RIGHT.
  3. Default-face accept set: unchanged. The rule stays tier: 'advisory'; lint.ts:1052 counts failing = errors + (strict ? warnings : 0); validate.ts:922 exits 1 only under flags.strict. Nothing is refused at any default door. RIGHT.
  4. os lint --strict / os validate --strict move 0 → 1 on a stack authoring one of the four keys. RIGHT as a fact; classified in ②.
  5. The hint expression authorHint ?? (isVerdictTriggered(entry) ? undefined : entry.note) ?? defaultHint. At this head's ledger: 110 dead + live-elsewhere rows at all depths, 0 carry authorWarn; 105 verdict-triggered rows at the depth the warn map reads (24 types), 0 carry an authorHint, 80 notes cite a tracker id; 8 rows warned before the ruling at that depth, of which 7 reach describe() and 1 (mapping.connectorSource, item 17) throws. No internal note reaches an author on any newly warned row, and no pre-existing row's output changes. RIGHT; the three Q3 pins and the ledger-wide CONTROL pin hold it.

The changeset, sentence by sentence (.changeset/16094-liveness-dead-warns.md, now '@objectstack/lint': minor).

  1. "both rule ids were exported and never produced, because no shipped dead or live-elsewhere row opted in" — RIGHT (0 of 110 at head and at main).
  2. Clause-②: no as its own line in the body — present, the form AGENTS.md Post-Task Checklist step 3 asks for. RIGHT.
  3. "warns when its status is dead, live-elsewhere or experimental, or when it sets authorWarn: true (still the only way a planned row warns)" — RIGHT.
  4. "Among authorable keys in the metadata types the rule walks, four are dead today: a view container's own name and label … and … rowLevelSecurity[].label and rowLevelSecurity[].description" — RIGHT. Re-read at this head: view admits name, label plus nine rows that are all retiredKey tombstones in view.zod.ts (list.pageName / tabs / responsive / performance / striped / bordered / virtualScroll, form.defaultSort / aria); permission admits the two policy keys plus four tombstones (objects.allowRestore / allowPurge in permission.zod.ts, rowLevelSecurity.priority / tags in rls.zod.ts); field admits conditionalRequired, a tombstone at field.zod.ts:1809. ViewSchema's container name / label are optional authorable strings, not tombstones. The ledger delta between the prior head and this one (field, mapping, picklist, translation) is planned → live flips and one authorHint text, no dead or live-elsewhere row added or removed, so the prior record's census of the other walked types carries over: 13 authorable rows, 4 reachable.
  5. "No walk visits manifest, connectors or realtime subscriptions … That includes manifest.runtime, the one live-elsewhere row" — RIGHT; none is in TYPE_COLLECTIONS or a bespoke walk.
  6. The hint paragraph: the quoted dead default is byte-identical to describe()'s; "never shows the ledger's internal note"; "Rows that opt in with authorWarn, and experimental rows, show exactly the hint they showed before" — RIGHT (item 5; the one opted-in live row showed nothing before and shows nothing now, item 17).
  7. Retired keys: every parsing door (retiredKey() is z.never with the guidance) refuses first; os lint runs normalizeStackInput, which converts and does not validate, so a raw config with a retired key is graded dead where it got nothing — RIGHT. The runtime publish gate runs this rule only for email_template / mapping / datasource (registry runtimeTypes); none gains a row from the flip (mapping's warn map is {connectorSource} at base and at head; the other two are empty).
  8. "Nothing is refused, and nothing changes without --strict"; "os lint --strict and os validate --strict now exit 1 instead of 0 …"; "A view container that carries its own name and label beside its object binding … draws two warnings per container" — RIGHT; the two-per-container number is the prior head's own CI reading (expected 5 to be 1: four findings on two containers). "only app-showcase gains warnings: two, on one permission set's policy label and description, and no example's exit code changes" — RIGHT at the merge-base, and still right against current main: fix(showcase): the contributor set's row-level policies apply to every holder of the set #21104 (99398542b3) only drops positions from the three showcase_contributor policies, leaving their label / description in place and adding no policy elsewhere, so checkItem's one-finding-per-path gives two; every label: in the examples' view files sits under list: / listViews (live), no defineView container carries its own name / label, consistent with +0 on app-crm, app-todo, app-multi-package.
  9. "To clear the warning, delete the key: nothing reads it" — RIGHT per the ledger's evidence (both keys optional; no mounted reader recorded). Reading caution for the spec lane only, as before: ViewSchema.label's description still says it is shown in metadata lists.

The merge of main (#21047).

  1. 88b484e00c is an ancestor of the merge-base, and the conflict in lint-liveness-properties.test.ts is resolved to the measured value: field.picklist is live with no authorWarn at this head, field.conditionalRequired is dead (tombstone), and the pin reads .toEqual(['conditionalRequired']). RIGHT. Side effect checked: authorWarnedProperties('translation'), the one caller outside the package (i18n-extract.ts:1089), still answers {flows} — translation.json carries no dead / live-elsewhere / experimental row at the warn-map depth, so the i18n demand side does not move.

The CLI fixture re-judge (lint-per-package-authoring-parity.test.ts).

  1. The two ordersViews containers now bind by object alone, list.label kept (live). Each test keeps its intent: the union run raises nothing (no liveness finding is left to raise), the per-package survivor is the one field-no-consumers finding on pp_account.industry in core, --strict fails with exactly 1, the single-package control is untouched; the walk still names the item from item.object when name is absent (the source's "view containers bind via object, not name" branch). RIGHT, and the right choice: absorbing the four findings would have turned a parity fixture into a liveness pin and falsified the "union raises NOTHING" premise the file exists to hold; the ruled behaviour's pins live in packages/lint. Test Core (5/6), the shard that was red on 64e0275024, is success on this head. Reading caution, nothing owed: no pin in packages/lint names view.name / view.label through lintLivenessProperties (the ruling asks one pin per rule id, delivered on the RLS rows), so the changeset's view-container sentence rests on the ledger rows and the prior head's CI reading rather than on a standing pin.

The byte-for-byte hint control, re-scoped in 49fb6cfad3.

  1. The control's population is now (authorWarn === true || status === 'experimental') && status !== 'live': 7 rows (four agent.* and tool.outputSchema, experimental, note and no authorHint; object.externalSharingModel, planned + opt-in, note and no authorHint; translation.flows, planned + opt-in, authorHint). Both anti-vacuity guards hold on real rows, 6 of the 7 resolve to their note, so widening the hint rule to drop any note flips six comparisons: the control can still fail for exactly the rows it protects (the dev's ablation on the prior head already showed it among the three reds, and the hint source did not change this round). The one excluded row, mapping.connectorSource (live + authorWarn), has no hint before or after — describe() throws on live in both versions, and the COVERAGE pin asserts a live + authorWarn entry throws — so excluding it hides nothing about hint selection. Disclosed in the test comment, the commit message, the report's deviations and the PR's Acceptance notes. Justified. One wording nit: the PR body's "scoped to rows describe() answers" overstates the code, which excludes live only — an opted-in row with a status outside the five would still enter the control and throw, the louder direction. Nothing owed.

The mapping.connectorSource crash: main's, confirmed; neither introduced nor widened here.

  1. 8368f1c005 (feat(service-automation,core,types): the connector sync executor pulls a mapping's connectorSource through the import runner, moved beside bulkWrite #21084, merged 07:05Z) is an ancestor of the merge-base; the ledger JSON is identical on both sides; at base shouldWarn admits the row on authorWarn === true and at head on the same arm (VERDICTS_THAT_WARN.has('live') is false); describe()'s status set and throw are untouched; in checkItem, describe(entry) runs before the new hint expression, so isVerdictTriggered never executes for a throwing row; mapping's warn map is the same one-row map at base and head. Reach is unchanged on every door: the CLI commands catch at command level (lint.ts:1172, validate.ts:966), so the throw is exit 1 with the sentinel as the message — the dev's measured reading, consistent with the code shape — and runtime-gate.ts:894 wraps a throwing rule as authoring-rule-threw. Filed as [finding] os lint / os validate crash on any stack whose mapping authors connectorSource: the ledger row is live with authorWarn: true, and the liveness rule throws its integrity sentinel #21127 (open, 08:18:56Z, routed to the owner of mapping.json). Right that this PR pins nothing about the shipped row: the sentinel is loud by design, and a pin asserting the crash would pin a defect.

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 lint-liveness-properties.ts says this rule is surfaces: CLI_ONLY while the registry entry says commands: ALL with three runtimeTypes.

② Semver level

minor — RIGHT. No export moves and the default-face accept set is unchanged, but two rule ids become producible — two new advisories to a consumer — and a non-zero exit becomes reachable under a published flag. Both house precedents on this shape grade it minor: .changeset/20654-flow-credential-literal-advisory.md ('@objectstack/lint': minor, Clause-②: no, "--strict promotes it, as it promotes every warning"), and the cli 17.5.0 entry for os validate --strict, "Graded minor rather than patch for the new observable step line, the new advisories and the newly reachable non-zero exit". The prior patch, and the seat's reading of 20654 as patch, are corrected in the changeset and in public (5925927494).

Clause-②: no, no arm — RIGHT, with the house split named. The criterion asks whether the card widens the accept set or enlarges the public surface, on the published contract face; nothing widens and no export moves. The same-package, newest precedent (20654) reads a new advisory this way with no arm. The split: cli 17.5.0 9bd631f marks "os lint --strict can now fail a project it passed before" Clause-②: yes (narrowing), BREAKING, with a not-required (no-migration-prescription) marker — a command that began running a whole pass it never ran — and the same CHANGELOG carries Clause-②: no (narrowing) for a command retirement. This record adopts no with no arm, as the prior record did; nothing is declared breaking, so check-adr-0087-registration asks nothing of this changeset, and Check Changeset is success twice on this head. If the maintainer prefers the 9bd631f reading, the changeset takes Clause-②: no (narrowing), the BREAKING banner and the not-required (no-migration-prescription) marker, and the level stays minor either way. The line is present in the PR body and in the changeset body.

③ Boundary flags

The prior FAIL record's four owed items (5925918475), each judged on this head.

  • (1) Re-judge the parity fixture and run the cli package's tests: met — 1d239706f2, ① item 16; Test Core (5/6) and the Test Core rollup are success.
  • (2) Merge main, resolve the conflict, field-set pin to ['conditionalRequired']: met — 6ed56d2dbf and b0705c1530, ① item 15.
  • (3) Changeset minor, a Clause-②: no line in the body, one sentence naming view containers that carry their own name / label: met — all three present, ① items 7, 9, 13; ②.
  • (4) Fix the enforce-or-remove: DashboardWidgetSchema 的 5 个 dead 键(#4956 下钻首次给出裁决) #5010 test comment: met — it now reads that a retired key's hint "is the dead default, never the row's note".

Newest os-dev-report (5927529740), deviations (4).

  • PR body not PATCHed, edits listed for the seat: the seat applied them — "Bump: minor", "equivalent invariant plus a reach sentinel", the CLI parity section, the feat(objectql): resolve picklist references at runtime — served options, additive extensions, write validation, load-time refusal #21047 and merge-state bullets, the connectorSource acceptance note are all in the body as read. Agreed; the duplicated Tests paragraphs (① "still wrong or stale") are the seat's to tidy, nothing owed.
  • The hint control excludes live rows: judged ① item 17 — justified, disclosed, still able to fail.
  • CLI integration tier run as four lock-held runs: immaterial; the head's Test Core shards are the gate verdict.
  • /verify not run: no UI, agreed.

Newest os-dev-report, out_of_scope_findings (3).

One flag this review adds, outside the claim surface and outside the review faces: ESCALATED, not owed on this PR. packages/spec/liveness/README.md ships in the @objectstack/spec tarball (files lists liveness), and its "Author warnings — closing the loop" section (lines 557 and 572 at this head) still says a row warns only when it sets "authorWarn": true (plus experimental) and that "coverage grows by marking more entries authorWarn". Both sentences are false after this diff, and the second steers a ledger maintainer into the one move the Q3 fix guards against: adding authorWarn to a dead row turns it opted-in and routes its note to authors. The claim fenced packages/spec/liveness/** (⛔ no edit), so the dev was right not to touch it; the spec lane owes a docs-only follow-up, and the dispatching seat files the card. No content/docs page names authorWarn, the rule ids or lintLivenessProperties; validating-metadata.mdx lists "liveness" in a door matrix only, not falsified; the docs-drift bot's one hand-written page needs no change.

Check-runs on 49fb6cfad3, read at 08:21Z: 41 runs, 36 success, 5 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke, and the 08:20Z re-runs of Auto Label and Check PR Size), 0 failure, 0 in progress. All seven required contexts are success: Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. No governed path in the file list; head repo is the base repo; the PR is a draft with needs:contract-review, which the writing seat removes on this PASS.

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: claude/issue-16094-liveness-dead-warns
Reviewed-by: session_01JAhu8u8QfBvRjVZDox7CP9

VERDICT: PASS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants