Skip to content

fix(lint): a liveness finding's fix text never comes from the ledger note, for any row class - #21169

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21096-liveness-hint-no-note
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21096-liveness-hint-no-note

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #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:

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 feat(lint): a ledger-dead or live-elsewhere key warns without an authorWarn opt-in, and never shows the ledger note (#16094) #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

#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

@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 4 documentable anchor(s).

⛔ 2 release-owned page(s) 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 f0cc16e8d55b02269c7db708254e2917ba7e84e2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3a8e95838807d9a98c9d997f2b631d9add3ffc3c — the merge of head 88dc06a2012516079c9d5b70230ce23a2ee18df5 into base f0cc16e8d55b02269c7db708254e2917ba7e84e2, 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 3a8e95838807d9a98c9d997f2b631d9add3ffc3c && git checkout 3a8e95838807d9a98c9d997f2b631d9add3ffc3c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f0cc16e8d55b02269c7db708254e2917ba7e84e2 88dc06a2012516079c9d5b70230ce23a2ee18df5 && git checkout -B drift-repro f0cc16e8d55b02269c7db708254e2917ba7e84e2 && git merge --no-ff 88dc06a2012516079c9d5b70230ce23a2ee18df5

node scripts/docs-audit/affected-docs.mjs --json f0cc16e8d55b02269c7db708254e2917ba7e84e2

⚠️ 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 f0cc16e8d55b02269c7db708254e2917ba7e84e2 → 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: 88dc06a2012516079c9d5b70230ce23a2ee18df5
Local-runs: none

① Derived judgments

The checkout has no node_modules, so the one permitted vitest file was not run. Every judgment below comes from reading the diff, the branch source, the main-side test and the shipped ledger JSON.

Code (packages/lint/src/lint-liveness-properties.ts).

  • const hint = entry.authorHint ?? defaultHint; is correct for every class that can reach it.
    • describe(entry) returns a non-empty defaultHint for experimental, planned, dead and live-elsewhere.
    • Any other status (live + authorWarn: true, or a missing status + authorWarn: true) throws the integrity sentinel inside describe() before hint is computed.
    • The shipped ledgers have no live + authorWarn row at the depth the warn map reads (720 live, all unwarned), so the sentinel is not newly reachable.
  • Nothing is left dangling.

Tests (packages/lint/src/lint-liveness-properties.test.ts).

  • The object.externalSharingModel real-ledger pin guards that the row is still authorWarn: true, has no authorHint, and carries a note matching /#\d+/; all three hold, and the note cites ADR-0090: Permission Model v2 — concept convergence, final naming, AI-authoring safety (tracking) #2696. It then asserts the planned default hint, no tracker id, and not the note. This is strictly stronger than main's not.toMatch(/^Remove it/), and it would fail on main.
  • The opted-in/experimental real-ledger pin.
    • Its three anti-vacuity guards are non-vacuous against the shipped ledgers: 1 opted-in row and 5 experimental rows have a note and no authorHint, and 1 of those notes carries a tracker id.
    • The authorHint rows (3) are pinned byte for byte. Main's CONTROL (authorHint ?? note) is correctly inverted, not weakened.
    • For rows without an authorHint, this pin asserts only "not the note, no tracker id". The synthetic every-class pin and the externalSharingModel pin assert the default.
  • The synthetic every-class pin covers dead, live-elsewhere, planned and experimental, each with and without authorWarn, and asserts the default. A control asserts that authorHint wins. It keeps every main-side assertion for verdict-triggered rows and inverts exactly the opted-in/experimental ones.
  • The feat(lint): a ledger-dead or live-elsewhere key warns without an authorWarn opt-in, and never shows the ledger note (#16094) #21092 pins. The verdict-triggered real-ledger pin and the end-to-end dead-RLS pin are untouched. The live-elsewhere precedence pin moves from toBe('N') to toContain('sibling repo'), which is the required inversion. No pin was dropped without a stronger replacement.

Changeset (.changeset/21096-liveness-hint-never-note.md).

  • "728-character maintainer note that cites a tracker id": TRUE. packages/spec/liveness/object.json → props.externalSharingModel.note is 728 characters and contains #2696.
  • "the two liveness-planned-property findings for externalSharingModel" on app-showcase: TRUE (announcement.object.ts:34, account.object.ts:26).
  • The quoted fix text: TRUE, byte-identical to the planned defaultHint in describe().
  • "an opted-in row and an experimental row with no authorHint printed its note": TRUE of the main-side expression.
  • "No rule id, message, severity or exit code changes": TRUE; only the hint expression changed.
  • "a row that has an authorHint prints it exactly as before": TRUE, pinned by the CONTROL loops.
  • The prose names no tracker id other than (#21096): TRUE.

Merge-cleanliness. git merge-tree --write-tree origin/main FETCH_HEAD exits 0 (tree 7f3621751107ec08904c113c2671097e86aee959), with no conflicts.

② Semver level

patch for @objectstack/lint is right: a bug fix in a released package, with no authorable surface or rule id changed. Clause-②: no is right.

③ Boundary flags

Implemented-by: claude/issue-21096-liveness-hint-no-note
Reviewed-by: session_01VDtqoecgES7ScQYGbFVDRv

VERDICT: PASS


Generated by Claude Code

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

1 participant