Skip to content

lintLivenessProperties tells the author a planned property is dead — the message and the hint on the same finding contradict each other #11384

Description

@os-steve

Summary

describe() in packages/lint/src/lint-liveness-properties.ts knows two verdicts, and the ledger ships three:

functiondescribe(entry: LedgerEntry): {kind: string;rule: string}{if(entry.status==='experimental'){return{kind: 'is experimental — declared but NOT enforced at runtime',rule: LIVENESS_EXPERIMENTAL_PROPERTY};}return{kind: 'has no runtime effect (liveness: dead)',rule: LIVENESS_DEAD_PROPERTY};}

Anything that is not experimental falls into the dead branch, planned included — so a row the ledger deliberately grades planned ("declared, and a consumer is being built against it") is reported to the author as liveness: dead under the rule id liveness-dead-property.

Measurement (origin/main @ b863f01)

Census of every warned row across all 30 shipped ledgers (authorWarn: true or status: experimental) — 8 rows, and 3 of the 8 are planned:

agent · lifecycle [status=experimental]
agent · memory [status=experimental]
agent · guardrails [status=experimental]
agent · structuredOutput [status=experimental]
field · relatedListFilter [status=planned] <- reported as dead
object · externalSharingModel [status=planned] <- reported as dead
tool · outputSchema [status=experimental]
translation · flows [status=planned] <- reported as dead

A real finding, captured from a test run on translation.flows:

message: "sets `flows` but this translation property has no runtime effect (liveness: dead)."
hint: "No shipped screen-flow runner reads this group yet — until the objectui half of #7646
lands, a `type: 'screen'` flow renders the strings authored on the flow ... in every locale."

The message says the property is dead; the hint attached to the same finding says a runner is coming and names the card. The ledger's own note is explicit that this is not a dead row: "dead means declared with no consumer and no plan, while this group was ruled into the vocabulary by the maintainer specifically so the runner half could be built against it".

Why it matters

The two verdicts imply opposite author actions. dead means remove it — which is what the rule's default hint says when a row has no authorHint ("Remove it — it is declared in the spec but not consumed at runtime"). planned means keep it, it is contract-first and the reader is being built. An author who follows the message deletes metadata the platform asked them to write; an author who reads the hint keeps it and wonders why the linter called it dead. The rule id is also what suppression and reporting key off, so the two states are indistinguishable to anything downstream.

Not urgent: all three rows carry an authorHint today, so the honest sentence is present on every finding — just underneath a contradicting one.

Suggested shape

A third branch in describe() for status === 'planned' with its own wording (declared, not read yet) and its own rule id, plus a decision on whether the new id is advisory-only like the other two. Worth checking at the same time what describe() should do with a status it does not recognise — the current fallthrough grades an unknown status as dead, silently.

Provenance

Found while implementing #11288 (the translation liveness walk, PR #11383) — that fix is what first made a planned translation row reach an author at all, but the defect is older and independent of it: field.relatedListFilter and object.externalSharingModel have been reported this way for as long as they have been planned. Out of scope there, filed here; no fix attempted.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions