From a9c15ad1ada6d2cf243e17b452de1ad5efce175c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 11:12:39 +0000 Subject: [PATCH 1/2] fix(lint): a liveness finding's fix text never comes from the ledger note, for any row class Claude-Session: https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv Co-authored-by: Claude --- .../lint/src/lint-liveness-properties.test.ts | 87 ++++++++++++------- packages/lint/src/lint-liveness-properties.ts | 33 ++----- 2 files changed, 66 insertions(+), 54 deletions(-) diff --git a/packages/lint/src/lint-liveness-properties.test.ts b/packages/lint/src/lint-liveness-properties.test.ts index 10f9d005e4b..60a90c62a7e 100644 --- a/packages/lint/src/lint-liveness-properties.test.ts +++ b/packages/lint/src/lint-liveness-properties.test.ts @@ -1138,13 +1138,23 @@ describe('dead / experimental / planned / live-elsewhere verdicts are distinct, // contract test on the two rows that are still planned — `translation.flows` // just above and `object.externalSharingModel` just below. - it('REAL LEDGER: object.externalSharingModel (planned, no authorHint — falls back to `note`) — planned rule id, note hint does not say Remove it', () => { + it('REAL LEDGER: object.externalSharingModel (planned, no authorHint) — planned rule id, the planned DEFAULT hint, never its ledger note', () => { const findings = lintLivenessProperties({ objects: [{ name: 'widget', externalSharingModel: 'read' }] }); const f = findings.find((x) => x.message.includes('externalSharingModel')); expect(f).toBeDefined(); expect(f!.rule).toBe('liveness-planned-property'); expect(f!.message).not.toContain('dead'); - expect(f!.hint).not.toMatch(/^Remove it/); + const ledger = JSON.parse(readFileSync(join(shippedLedgerDir(), 'object.json'), 'utf8')); + const row = ledger.props.externalSharingModel; + // Anti-vacuity: the row is still the leaking shape this pin exists for. + expect(row.authorWarn).toBe(true); + expect(row.authorHint).toBeUndefined(); + expect(row.note).toMatch(/#\d+/); + expect(f!.hint).toBe( + checkItemAgainstWarnMap('gadget', { name: 'g1', gizmo: 'x' }, "gadget 'g1'", oneEntry({ status: 'planned', authorWarn: true }))[0].hint, + ); + expect(f!.hint).not.toMatch(/#\d+/); + expect(f!.hint).not.toContain(row.note); }); // ── SYNTHETIC: the default hint per verdict, when neither authorHint nor @@ -1218,11 +1228,11 @@ describe('dead / experimental / planned / live-elsewhere verdicts are distinct, expect(f.hint).toContain('evidence'); }); - it('SYNTHETIC: live-elsewhere keeps the shared hint precedence — authorHint over note over the default', () => { + it('SYNTHETIC: live-elsewhere shares the hint precedence — authorHint over the default, never the note', () => { const hintOf = (entry: Record) => checkItemAgainstWarnMap('gadget', { name: 'g1', gizmo: 'x' }, "gadget 'g1'", oneEntry(entry))[0].hint; expect(hintOf({ status: 'live-elsewhere', authorWarn: true, authorHint: 'H', note: 'N' })).toBe('H'); - expect(hintOf({ status: 'live-elsewhere', authorWarn: true, note: 'N' })).toBe('N'); + expect(hintOf({ status: 'live-elsewhere', authorWarn: true, note: 'N' })).toContain('sibling repo'); expect(hintOf({ status: 'live-elsewhere', authorWarn: true })).toContain('sibling repo'); }); @@ -1714,9 +1724,10 @@ describe('the dead and live-elsewhere verdicts warn on their own (#16094)', () = // the 105 newly warned entries' notes cite a tracker id, and three of the four // an author can reach say the row was deliberately not warned. AGENTS.md keeps // tracker numbers out of anything an author is shown. So such a row shows its -// `authorHint`, else the verdict's default hint; a row that opted in, and an -// `experimental` row, keep the hint they had before the ruling, byte for byte. -describe('the hint a verdict-triggered row shows an author (#16094)', () => { +// `authorHint`, else the verdict's default hint. The same rule holds for every +// other row class (an opted-in `planned`/`dead`/`live-elsewhere` row and an +// `experimental` row), pinned below (#21096). +describe('the hint a warned row shows an author (#16094, #21096)', () => { type ShippedRow = { type: string; path: string; entry: Record }; /** Every shipped row at the depth the warn map reads (props + one level of `children`). */ @@ -1774,38 +1785,56 @@ describe('the hint a verdict-triggered row shows an author (#16094)', () => { } }); - it('CONTROL, REAL LEDGER: every row that warned before the ruling keeps its hint byte for byte', () => { + it('REAL LEDGER: no opted-in or experimental row shows its note either, and none without an authorHint carries a tracker id', () => { // A `live` row that opts in is outside this population: `describe()` throws - // on it before and after the ruling (the COVERAGE pin above holds that it - // stays loud), so it has no hint to keep. - const before = shippedRows().filter( + // on it (the COVERAGE pin above holds that it stays loud). + const warned = shippedRows().filter( (r) => (r.entry.authorWarn === true || r.entry.status === 'experimental') && r.entry.status !== 'live', ); - // Anti-vacuity: an opted-in row and an experimental row both fall back to - // their note today, so the control would see a change in either. - expect(before.some((r) => r.entry.authorWarn === true && r.entry.authorHint === undefined && typeof r.entry.note === 'string')).toBe(true); - expect(before.some((r) => r.entry.status === 'experimental' && r.entry.authorHint === undefined && typeof r.entry.note === 'string')).toBe(true); - for (const row of before) { - const expected = row.entry.authorHint ?? row.entry.note; - if (typeof expected !== 'string') continue; // only the default remains — unchanged by construction - expect(hintFor(row), `${row.type}/${row.path}`).toBe(expected); + // Anti-vacuity: an opted-in row and an experimental row, each without an + // authorHint and each with a note, exist, so the leak this pin forbids is a real one. + const leakers = (pred: (r: ShippedRow) => boolean) => + warned.filter((r) => pred(r) && r.entry.authorHint === undefined && typeof r.entry.note === 'string'); + expect(leakers((r) => r.entry.authorWarn === true).length).toBeGreaterThan(0); + expect(leakers((r) => r.entry.status === 'experimental').length).toBeGreaterThan(0); + expect(leakers((r) => typeof r.entry.note === 'string' && TRACKER_ID.test(r.entry.note as string)).length).toBeGreaterThan(0); + for (const row of warned) { + const where = `${row.type}/${row.path}`; + const hint = hintFor(row); + if (typeof row.entry.authorHint === 'string') { + expect(hint, where).toBe(row.entry.authorHint); // CONTROL: the authored hint, exactly + } else { + expect(hint, where).not.toBe(row.entry.note); + expect(TRACKER_ID.test(hint), `${where} hint carries a tracker id`).toBe(false); + } } }); - it('SYNTHETIC: the opt-in, not the verdict, decides whether the note may be shown', () => { + it('SYNTHETIC: authorHint wins, otherwise the verdict default — for every row class, never the note', () => { const hintOf = (entry: Record) => checkItemAgainstWarnMap('gadget', { name: 'g1', gizmo: 'x' }, "gadget 'g1'", gizmoEntry(entry))[0].hint; const deadDefault = hintOf({ status: 'dead' }); const elsewhereDefault = hintOf({ status: 'live-elsewhere' }); - // Verdict-triggered: authorHint wins, otherwise the default — never the note. - expect(hintOf({ status: 'dead', note: 'N #123' })).toBe(deadDefault); - expect(hintOf({ status: 'live-elsewhere', note: 'N #123' })).toBe(elsewhereDefault); - expect(hintOf({ status: 'dead', authorHint: 'H', note: 'N' })).toBe('H'); - // Opted in: today's precedence, unchanged — the note is shown. - expect(hintOf({ status: 'dead', authorWarn: true, note: 'N' })).toBe('N'); - expect(hintOf({ status: 'live-elsewhere', authorWarn: true, note: 'N' })).toBe('N'); - // Experimental needs no opt-in and keeps its note, as before the ruling. - expect(hintOf({ status: 'experimental', note: 'N' })).toBe('N'); + const plannedDefault = hintOf({ status: 'planned', authorWarn: true }); + const experimentalDefault = hintOf({ status: 'experimental' }); + const NOTE = 'N #123'; + // Verdict-triggered, with or without the opt-in: the default, never the note. + expect(hintOf({ status: 'dead', note: NOTE })).toBe(deadDefault); + expect(hintOf({ status: 'live-elsewhere', note: NOTE })).toBe(elsewhereDefault); + expect(hintOf({ status: 'dead', authorWarn: true, note: NOTE })).toBe(deadDefault); + expect(hintOf({ status: 'live-elsewhere', authorWarn: true, note: NOTE })).toBe(elsewhereDefault); + // Opted-in planned and experimental rows: the default, never the note. + expect(hintOf({ status: 'planned', authorWarn: true, note: NOTE })).toBe(plannedDefault); + expect(hintOf({ status: 'experimental', note: NOTE })).toBe(experimentalDefault); + expect(hintOf({ status: 'experimental', authorWarn: true, note: NOTE })).toBe(experimentalDefault); + for (const hint of [deadDefault, elsewhereDefault, plannedDefault, experimentalDefault]) { + expect(hint).not.toMatch(/#\d+/); + } + // Control: an authorHint is printed exactly, note or no note. + for (const status of ['dead', 'live-elsewhere', 'planned', 'experimental']) { + expect(hintOf({ status, authorWarn: true, authorHint: 'H', note: NOTE }), status).toBe('H'); + expect(hintOf({ status, authorWarn: true, authorHint: 'H' }), status).toBe('H'); + } }); it('END TO END: the authored dead RLS keys show the dead default hint, not the ledger note', () => { diff --git a/packages/lint/src/lint-liveness-properties.ts b/packages/lint/src/lint-liveness-properties.ts index b59e574e3f9..85d077c140b 100644 --- a/packages/lint/src/lint-liveness-properties.ts +++ b/packages/lint/src/lint-liveness-properties.ts @@ -20,10 +20,9 @@ * `dead`, `live-elsewhere` and `experimental` warn on their own, because the * verdict itself is the author-facing warning (#16094, decision batch #60, * option A). `planned` and `live` warn only when the row opts in via - * `"authorWarn": true`. The hint is the row's `"authorHint"`; failing that, a - * row that opted in (or an `experimental` one) shows its ledger `note`, while a - * row warning only by its `dead` / `live-elsewhere` verdict shows the verdict's - * default hint and never its note (see `isVerdictTriggered`). Booleans warn + * `"authorWarn": true`. The hint is the row's `"authorHint"`; failing that, the + * verdict's default hint (see `describe`). Never the row's ledger `note`, for + * any row class: that text is written for the ledger's maintainers. Booleans warn * only when set truthy (so schema defaults like `enable.searchable` never trip * it); object/string/array props warn when present at all. */ @@ -176,24 +175,6 @@ function shouldWarn(entry: LedgerEntry | undefined): boolean { return (typeof entry.status === 'string' && VERDICTS_THAT_WARN.has(entry.status)) || entry.authorWarn === true; } -/** - * Is this row warning ONLY because of its ruled verdict — `dead` or - * `live-elsewhere`, with no `authorWarn` opt-in? - * - * Such a row never chose to address an author, so its `note` was written for - * the ledger's maintainers: audit prose, measured commit shas, tracker ids. A - * finding shows an author what the row WROTE for authors (`authorHint`) or the - * verdict's own default hint, never that note. AGENTS.md keeps tracker numbers - * out of anything an author is shown, and the ruling is what began routing these - * rows to authors, so the hint selection in `checkItem` closes the door it - * opened. A row that opts in with `authorWarn`, and an `experimental` row, keep - * the hint they had before the ruling (`authorHint`, else `note`, else the - * default) byte for byte. - */ -function isVerdictTriggered(entry: LedgerEntry): boolean { - return entry.authorWarn !== true && typeof entry.status === 'string' && RULED_VERDICTS.has(entry.status); -} - /** A value that signals authoring intent: booleans only when truthy; everything else when present. */ function isAuthored(value: unknown): boolean { if (value === undefined || value === null) return false; @@ -226,8 +207,8 @@ function isAuthored(value: unknown): boolean { * `evidence` — the enforcer's address — rather than at a delete key. * * Each verdict below also carries its own DEFAULT hint (used when the ledger - * entry has no `authorHint` and either has no `note` or warns only by its - * `dead` / `live-elsewhere` verdict — see `isVerdictTriggered`): the `dead` + * entry has no `authorHint`; the row's `note` is never shown to an author, since + * it is written for the ledger's maintainers): the `dead` * default says "Remove it"; `planned`'s and `live-elsewhere`'s must not, * because removing a planned or elsewhere-enforced property is exactly the * wrong author action. @@ -424,7 +405,9 @@ function checkItem( for (const value of values instanceof Array ? values : [values]) { if (!isAuthored(value)) continue; const { kind, rule, defaultHint } = describe(entry); - const hint = entry.authorHint ?? (isVerdictTriggered(entry) ? undefined : entry.note) ?? defaultHint; + // Author-facing text is `authorHint` or the verdict's default — never the + // row's `note`, which is maintainer prose (audit evidence, tracker ids). + const hint = entry.authorHint ?? defaultHint; findings.push({ where: whereBase, message: `sets \`${path}\` but this ${type} property ${kind}.`, From 88dc06a2012516079c9d5b70230ce23a2ee18df5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 11:13:52 +0000 Subject: [PATCH 2/2] chore: changeset for the liveness hint never showing the ledger note Claude-Session: https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv Co-authored-by: Claude --- .changeset/21096-liveness-hint-never-note.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/21096-liveness-hint-never-note.md diff --git a/.changeset/21096-liveness-hint-never-note.md b/.changeset/21096-liveness-hint-never-note.md new file mode 100644 index 00000000000..3c2bdc512da --- /dev/null +++ b/.changeset/21096-liveness-hint-never-note.md @@ -0,0 +1,7 @@ +--- +'@objectstack/lint': patch +--- + +fix(lint): a liveness finding's fix text is the row's `authorHint`, else the verdict's default hint, and never the row's internal ledger `note`, for every row class. Before this, a row that opted in with `authorWarn`, and an `experimental` row, with no `authorHint` printed its `note` as the fix text: on `app-showcase`, the two `liveness-planned-property` findings for `externalSharingModel` printed a 728-character maintainer note that cites a tracker id. They now print "Keep it — a consumer is being built against this property; it has no runtime effect yet." No rule id, message, severity or exit code changes, and a row that has an `authorHint` prints it exactly as before (#21096) + +Clause-②: no