Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .changeset/lint-liveness-live-elsewhere-rule-id.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@objectstack/lint': minor
---

`lintLivenessProperties` no longer crashes on the `live-elsewhere` verdict — and never tells an author to remove a key a sibling repo enforces

`describe()` in `lint-liveness-properties.ts` knew three verdicts
(`experimental`, `planned`, `dead`) and threw, loudly and by design, on any
other. #13483 then shipped the ledger's fifth status — `live-elsewhere`: dead
HERE by measurement, genuinely enforced in a sibling repo — and migrated
`manifest.runtime` onto it (its enforcer is the cloud marketplace publish
gate). Nothing taught `describe()` about it, so the day any `live-elsewhere`
row opts into `authorWarn: true`, `os lint` would raise that
shipped-ledger-integrity error instead of the advisory warning the author
should get. No shipped row carries `authorWarn` today, so this was a fuse
rather than a fire.

`describe()` now has a fourth branch. `live-elsewhere` gets its own rule id —
`liveness-live-elsewhere-property`, exported as `LIVENESS_LIVE_ELSEWHERE_PROPERTY`
beside `LIVENESS_DEAD_PROPERTY` / `LIVENESS_EXPERIMENTAL_PROPERTY` /
`LIVENESS_PLANNED_PROPERTY` and advisory-only like them — plus its own message
(`is enforced in a sibling repo, not here`) and its own default hint, which keeps
the property and points at the ledger row's `evidence` for the enforcer. It
deliberately does **not** reuse the `dead` branch: that is the #11384 lesson,
which is that verdicts imply OPPOSITE author actions, and "Remove it" is the
single most damaging sentence available about a key whose enforcement is real
and remote — deleting it tears out a live gate's input. The sentinel throw
stays for genuinely unknown statuses, with its enumeration of the known ones
updated.

The suite gains a coverage pin derived from the shipped ledgers rather than from
a hand-written list: every distinct `status` those ledgers actually carry must be
answered by `describe()` with a rule id of its own, or (for `live`, which reaches
`describe()` only through a ledger-authoring mistake) must still fail loud. A
sixth status now fails that pin by name instead of waiting for an author to trip
the sentinel.
4 changes: 4 additions & 0 deletions packages/lint/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -748,6 +748,10 @@ export {
LIVENESS_DEAD_PROPERTY,
LIVENESS_EXPERIMENTAL_PROPERTY,
LIVENESS_PLANNED_PROPERTY,
// #14057 — the fifth ledger verdict's own rule id. `live-elsewhere` is dead
// HERE by measurement but genuinely enforced in a sibling repo, so it must
// never share the `dead` id: the two ask the author for opposite actions.
LIVENESS_LIVE_ELSEWHERE_PROPERTY,
} from './lint-liveness-properties.js';

export { lintAutonumberFormats } from './lint-autonumber-formats.js';
Expand Down
109 changes: 108 additions & 1 deletion packages/lint/src/lint-liveness-properties.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import {
// source for why this ONE property is tested off the ledger.
checkItemAgainstWarnMap,
getNested,
// #14057 coverage seam — the statuses the shipped ledgers actually carry, so
// the coverage pin below is derived from the ledgers rather than hand-listed.
shippedLedgerStatuses,
} from './lint-liveness-properties.js';

/**
Expand DownExpand Up@@ -933,7 +936,7 @@ describe('the array fan-out, against a synthetic warn map (#10262)', () => {
// seam (#10262) — exactly the kind of verdict-level testing that seam exists
// for; the PLANNED branch is pinned against BOTH the real ledgers (so it stays
// a contract test) and a synthetic no-hint entry (to pin the DEFAULT wording).
describe('dead / experimental / planned verdicts are distinct, and unknown statuses fail loud (#11384)', () => {
describe('dead / experimental / planned / live-elsewhere verdicts are distinct, and unknown statuses fail loud (#11384, #14057)', () => {
const oneEntry = (entry: Record<string, unknown>) => new Map([['gizmo', entry]]);

// ── REAL LEDGER: the three rows the card captured ──────────────────────
Expand DownExpand Up@@ -1017,6 +1020,100 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
expect(findings[0].hint).not.toContain('Remove it');
});

// ── #14057: `live-elsewhere`, the fifth verdict — and the one whose wrong
// branch is the most expensive. #13483 added the status to the ledger and
// migrated `manifest.runtime` onto it (dead here by measurement, enforced by
// the cloud marketplace publish gate); `describe()` was not taught it, so the
// day any such row opts into `authorWarn` the author got a CRASH instead of
// the advisory finding — and the `dead` fallthrough it replaced would have
// been worse than the crash: "Remove it" about a key that is a live gate's
// input. ────────────────────────────────────────────────────────────────
it('SYNTHETIC: a live-elsewhere entry gets its OWN rule id and a keep-it hint — never the dead branch', () => {
const findings = checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'live-elsewhere', authorWarn: true }),
);
expect(findings).toHaveLength(1);
const [f] = findings;
expect(f.rule).toBe('liveness-live-elsewhere-property');
expect(f.rule).not.toBe('liveness-dead-property');
// The message must not read as a dead verdict...
expect(f.message).toContain('is enforced in a sibling repo');
expect(f.message).not.toContain('liveness: dead');
expect(f.message).not.toContain('has no runtime effect');
// ...and the default hint must point at the enforcer, not at a delete key.
expect(f.hint).not.toContain('Remove it');
expect(f.hint.toLowerCase()).toContain('keep it');
expect(f.hint).toContain('evidence');
});

it('SYNTHETIC: live-elsewhere keeps the shared hint precedence — authorHint over note over the default', () => {
const hintOf = (entry: Record<string, unknown>) =>
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 })).toContain('sibling repo');
});

// NEGATIVE CONTROL, on the REAL ledger, through the production path. The card
// is a fuse, not a fire: `shouldWarn()` gates entry to `describe()`, and the
// shipped `manifest.runtime` row does not carry `authorWarn`, so nothing
// reaches the new branch today. This pin holds that reading honest in both
// directions — if the row ever opts in, this goes red and the reviewer should
// UPDATE THIS PIN (the branch above is what makes that flip safe), never
// remove the branch.
//
// Anti-vacuity: `authorWarnedProperties('manifest')` would also be empty if
// the ledger were unreadable, so the guard is that `shippedLedgerStatuses()`
// sees `live-elsewhere` at all — the ONE row carrying it lives in that very
// file, so seeing the status proves the file was read.
it('REAL LEDGER: the live-elsewhere row exists and does NOT warn yet (the fuse, unlit)', () => {
expect(shippedLedgerStatuses().has('live-elsewhere')).toBe(true);
expect(authorWarnedProperties('manifest').has('runtime')).toBe(false);
});

// ── COVERAGE (#14057): describe() answers for every status the ledgers ship.
//
// Patching one status is what let this card repeat #11384 — so the pin is
// derived from the shipped rows rather than from a list somebody has to
// remember to edit. A sixth status appearing in any ledger fails HERE, by
// name, instead of waiting for an author to trip the sentinel throw.
//
// `live` is the one member that must NOT get a branch, and the source header
// says why: an entry only reaches `describe()` once `shouldWarn()` has said
// yes, so "`live` can in principle arrive here too (an entry marked
// `authorWarn: true` on a `live` row would be a ledger authoring mistake, not
// a user error)". A mistake in our own shipped data is exactly what the
// sentinel is for, so `live` is asserted LOUD here rather than handled.
it('COVERAGE: every status the shipped ledgers carry is answered by describe() — or is loud by design', () => {
const statuses = [...shippedLedgerStatuses()].sort();
// Anti-vacuity: an unreadable ledger dir returns the empty set, which would
// pass every assertion below without measuring anything.
expect(statuses.length).toBeGreaterThanOrEqual(4);
expect(statuses).toContain('live-elsewhere');

const rulesByStatus = new Map<string, string>();
for (const status of statuses) {
const run = () =>
checkItemAgainstWarnMap('gadget', { name: 'g1', gizmo: 'x' }, "gadget 'g1'", oneEntry({ status, authorWarn: true }));
if (status === 'live') {
expect(run).toThrow(/live/);
continue;
}
const findings = run();
expect(findings, `status ${status} produced no finding`).toHaveLength(1);
expect(findings[0].rule, `status ${status} has no rule id of its own`).toMatch(/^liveness-[a-z-]+-property$/);
expect(findings[0].hint.length, `status ${status} has an empty hint`).toBeGreaterThan(0);
rulesByStatus.set(status, findings[0].rule);
}

// #11384's lesson as an assertion: verdicts imply different author actions,
// so no two of them may share a rule id.
expect(new Set(rulesByStatus.values()).size).toBe(rulesByStatus.size);
});

// ── SYNTHETIC: the unknown-status boundary — loud, never silently `dead` ──
it('SYNTHETIC: an unrecognised status fails LOUD, naming the status, instead of silently grading as dead', () => {
expect(() =>
Expand All@@ -1027,6 +1124,16 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/quantum/);
// The message enumerates what describe() DOES know — #14057 is what happens
// when that list falls behind the branches, so pin them equal.
expect(() =>
checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/'experimental' \| 'planned' \| 'dead' \| 'live-elsewhere'/);
});

it('SYNTHETIC: a `live` row mistakenly marked authorWarn also fails LOUD rather than being graded dead', () => {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .changeset/lint-liveness-live-elsewhere-rule-id.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@objectstack/lint': minor
---

`lintLivenessProperties` no longer crashes on the `live-elsewhere` verdict — and never tells an author to remove a key a sibling repo enforces

`describe()` in `lint-liveness-properties.ts` knew three verdicts
(`experimental`, `planned`, `dead`) and threw, loudly and by design, on any
other. #13483 then shipped the ledger's fifth status — `live-elsewhere`: dead
HERE by measurement, genuinely enforced in a sibling repo — and migrated
`manifest.runtime` onto it (its enforcer is the cloud marketplace publish
gate). Nothing taught `describe()` about it, so the day any `live-elsewhere`
row opts into `authorWarn: true`, `os lint` would raise that
shipped-ledger-integrity error instead of the advisory warning the author
should get. No shipped row carries `authorWarn` today, so this was a fuse
rather than a fire.

`describe()` now has a fourth branch. `live-elsewhere` gets its own rule id —
`liveness-live-elsewhere-property`, exported as `LIVENESS_LIVE_ELSEWHERE_PROPERTY`
beside `LIVENESS_DEAD_PROPERTY` / `LIVENESS_EXPERIMENTAL_PROPERTY` /
`LIVENESS_PLANNED_PROPERTY` and advisory-only like them — plus its own message
(`is enforced in a sibling repo, not here`) and its own default hint, which keeps
the property and points at the ledger row's `evidence` for the enforcer. It
deliberately does **not** reuse the `dead` branch: that is the #11384 lesson,
which is that verdicts imply OPPOSITE author actions, and "Remove it" is the
single most damaging sentence available about a key whose enforcement is real
and remote — deleting it tears out a live gate's input. The sentinel throw
stays for genuinely unknown statuses, with its enumeration of the known ones
updated.

The suite gains a coverage pin derived from the shipped ledgers rather than from
a hand-written list: every distinct `status` those ledgers actually carry must be
answered by `describe()` with a rule id of its own, or (for `live`, which reaches
`describe()` only through a ledger-authoring mistake) must still fail loud. A
sixth status now fails that pin by name instead of waiting for an author to trip
the sentinel.
4 changes: 4 additions & 0 deletions packages/lint/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -748,6 +748,10 @@ export {
LIVENESS_DEAD_PROPERTY,
LIVENESS_EXPERIMENTAL_PROPERTY,
LIVENESS_PLANNED_PROPERTY,
// #14057 — the fifth ledger verdict's own rule id. `live-elsewhere` is dead
// HERE by measurement but genuinely enforced in a sibling repo, so it must
// never share the `dead` id: the two ask the author for opposite actions.
LIVENESS_LIVE_ELSEWHERE_PROPERTY,
} from './lint-liveness-properties.js';

export { lintAutonumberFormats } from './lint-autonumber-formats.js';
Expand Down
109 changes: 108 additions & 1 deletion packages/lint/src/lint-liveness-properties.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import {
// source for why this ONE property is tested off the ledger.
checkItemAgainstWarnMap,
getNested,
// #14057 coverage seam — the statuses the shipped ledgers actually carry, so
// the coverage pin below is derived from the ledgers rather than hand-listed.
shippedLedgerStatuses,
} from './lint-liveness-properties.js';

/**
Expand DownExpand Up@@ -933,7 +936,7 @@ describe('the array fan-out, against a synthetic warn map (#10262)', () => {
// seam (#10262) — exactly the kind of verdict-level testing that seam exists
// for; the PLANNED branch is pinned against BOTH the real ledgers (so it stays
// a contract test) and a synthetic no-hint entry (to pin the DEFAULT wording).
describe('dead / experimental / planned verdicts are distinct, and unknown statuses fail loud (#11384)', () => {
describe('dead / experimental / planned / live-elsewhere verdicts are distinct, and unknown statuses fail loud (#11384, #14057)', () => {
const oneEntry = (entry: Record<string, unknown>) => new Map([['gizmo', entry]]);

// ── REAL LEDGER: the three rows the card captured ──────────────────────
Expand DownExpand Up@@ -1017,6 +1020,100 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
expect(findings[0].hint).not.toContain('Remove it');
});

// ── #14057: `live-elsewhere`, the fifth verdict — and the one whose wrong
// branch is the most expensive. #13483 added the status to the ledger and
// migrated `manifest.runtime` onto it (dead here by measurement, enforced by
// the cloud marketplace publish gate); `describe()` was not taught it, so the
// day any such row opts into `authorWarn` the author got a CRASH instead of
// the advisory finding — and the `dead` fallthrough it replaced would have
// been worse than the crash: "Remove it" about a key that is a live gate's
// input. ────────────────────────────────────────────────────────────────
it('SYNTHETIC: a live-elsewhere entry gets its OWN rule id and a keep-it hint — never the dead branch', () => {
const findings = checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'live-elsewhere', authorWarn: true }),
);
expect(findings).toHaveLength(1);
const [f] = findings;
expect(f.rule).toBe('liveness-live-elsewhere-property');
expect(f.rule).not.toBe('liveness-dead-property');
// The message must not read as a dead verdict...
expect(f.message).toContain('is enforced in a sibling repo');
expect(f.message).not.toContain('liveness: dead');
expect(f.message).not.toContain('has no runtime effect');
// ...and the default hint must point at the enforcer, not at a delete key.
expect(f.hint).not.toContain('Remove it');
expect(f.hint.toLowerCase()).toContain('keep it');
expect(f.hint).toContain('evidence');
});

it('SYNTHETIC: live-elsewhere keeps the shared hint precedence — authorHint over note over the default', () => {
const hintOf = (entry: Record<string, unknown>) =>
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 })).toContain('sibling repo');
});

// NEGATIVE CONTROL, on the REAL ledger, through the production path. The card
// is a fuse, not a fire: `shouldWarn()` gates entry to `describe()`, and the
// shipped `manifest.runtime` row does not carry `authorWarn`, so nothing
// reaches the new branch today. This pin holds that reading honest in both
// directions — if the row ever opts in, this goes red and the reviewer should
// UPDATE THIS PIN (the branch above is what makes that flip safe), never
// remove the branch.
//
// Anti-vacuity: `authorWarnedProperties('manifest')` would also be empty if
// the ledger were unreadable, so the guard is that `shippedLedgerStatuses()`
// sees `live-elsewhere` at all — the ONE row carrying it lives in that very
// file, so seeing the status proves the file was read.
it('REAL LEDGER: the live-elsewhere row exists and does NOT warn yet (the fuse, unlit)', () => {
expect(shippedLedgerStatuses().has('live-elsewhere')).toBe(true);
expect(authorWarnedProperties('manifest').has('runtime')).toBe(false);
});

// ── COVERAGE (#14057): describe() answers for every status the ledgers ship.
//
// Patching one status is what let this card repeat #11384 — so the pin is
// derived from the shipped rows rather than from a list somebody has to
// remember to edit. A sixth status appearing in any ledger fails HERE, by
// name, instead of waiting for an author to trip the sentinel throw.
//
// `live` is the one member that must NOT get a branch, and the source header
// says why: an entry only reaches `describe()` once `shouldWarn()` has said
// yes, so "`live` can in principle arrive here too (an entry marked
// `authorWarn: true` on a `live` row would be a ledger authoring mistake, not
// a user error)". A mistake in our own shipped data is exactly what the
// sentinel is for, so `live` is asserted LOUD here rather than handled.
it('COVERAGE: every status the shipped ledgers carry is answered by describe() — or is loud by design', () => {
const statuses = [...shippedLedgerStatuses()].sort();
// Anti-vacuity: an unreadable ledger dir returns the empty set, which would
// pass every assertion below without measuring anything.
expect(statuses.length).toBeGreaterThanOrEqual(4);
expect(statuses).toContain('live-elsewhere');

const rulesByStatus = new Map<string, string>();
for (const status of statuses) {
const run = () =>
checkItemAgainstWarnMap('gadget', { name: 'g1', gizmo: 'x' }, "gadget 'g1'", oneEntry({ status, authorWarn: true }));
if (status === 'live') {
expect(run).toThrow(/live/);
continue;
}
const findings = run();
expect(findings, `status ${status} produced no finding`).toHaveLength(1);
expect(findings[0].rule, `status ${status} has no rule id of its own`).toMatch(/^liveness-[a-z-]+-property$/);
expect(findings[0].hint.length, `status ${status} has an empty hint`).toBeGreaterThan(0);
rulesByStatus.set(status, findings[0].rule);
}

// #11384's lesson as an assertion: verdicts imply different author actions,
// so no two of them may share a rule id.
expect(new Set(rulesByStatus.values()).size).toBe(rulesByStatus.size);
});

// ── SYNTHETIC: the unknown-status boundary — loud, never silently `dead` ──
it('SYNTHETIC: an unrecognised status fails LOUD, naming the status, instead of silently grading as dead', () => {
expect(() =>
Expand All@@ -1027,6 +1124,16 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/quantum/);
// The message enumerates what describe() DOES know — #14057 is what happens
// when that list falls behind the branches, so pin them equal.
expect(() =>
checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/'experimental' \| 'planned' \| 'dead' \| 'live-elsewhere'/);
});

it('SYNTHETIC: a `live` row mistakenly marked authorWarn also fails LOUD rather than being graded dead', () => {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .changeset/lint-liveness-live-elsewhere-rule-id.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@objectstack/lint': minor
---

`lintLivenessProperties` no longer crashes on the `live-elsewhere` verdict — and never tells an author to remove a key a sibling repo enforces

`describe()` in `lint-liveness-properties.ts` knew three verdicts
(`experimental`, `planned`, `dead`) and threw, loudly and by design, on any
other. #13483 then shipped the ledger's fifth status — `live-elsewhere`: dead
HERE by measurement, genuinely enforced in a sibling repo — and migrated
`manifest.runtime` onto it (its enforcer is the cloud marketplace publish
gate). Nothing taught `describe()` about it, so the day any `live-elsewhere`
row opts into `authorWarn: true`, `os lint` would raise that
shipped-ledger-integrity error instead of the advisory warning the author
should get. No shipped row carries `authorWarn` today, so this was a fuse
rather than a fire.

`describe()` now has a fourth branch. `live-elsewhere` gets its own rule id —
`liveness-live-elsewhere-property`, exported as `LIVENESS_LIVE_ELSEWHERE_PROPERTY`
beside `LIVENESS_DEAD_PROPERTY` / `LIVENESS_EXPERIMENTAL_PROPERTY` /
`LIVENESS_PLANNED_PROPERTY` and advisory-only like them — plus its own message
(`is enforced in a sibling repo, not here`) and its own default hint, which keeps
the property and points at the ledger row's `evidence` for the enforcer. It
deliberately does **not** reuse the `dead` branch: that is the #11384 lesson,
which is that verdicts imply OPPOSITE author actions, and "Remove it" is the
single most damaging sentence available about a key whose enforcement is real
and remote — deleting it tears out a live gate's input. The sentinel throw
stays for genuinely unknown statuses, with its enumeration of the known ones
updated.

The suite gains a coverage pin derived from the shipped ledgers rather than from
a hand-written list: every distinct `status` those ledgers actually carry must be
answered by `describe()` with a rule id of its own, or (for `live`, which reaches
`describe()` only through a ledger-authoring mistake) must still fail loud. A
sixth status now fails that pin by name instead of waiting for an author to trip
the sentinel.
4 changes: 4 additions & 0 deletions packages/lint/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -748,6 +748,10 @@ export {
LIVENESS_DEAD_PROPERTY,
LIVENESS_EXPERIMENTAL_PROPERTY,
LIVENESS_PLANNED_PROPERTY,
// #14057 — the fifth ledger verdict's own rule id. `live-elsewhere` is dead
// HERE by measurement but genuinely enforced in a sibling repo, so it must
// never share the `dead` id: the two ask the author for opposite actions.
LIVENESS_LIVE_ELSEWHERE_PROPERTY,
} from './lint-liveness-properties.js';

export { lintAutonumberFormats } from './lint-autonumber-formats.js';
Expand Down
109 changes: 108 additions & 1 deletion packages/lint/src/lint-liveness-properties.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import {
// source for why this ONE property is tested off the ledger.
checkItemAgainstWarnMap,
getNested,
// #14057 coverage seam — the statuses the shipped ledgers actually carry, so
// the coverage pin below is derived from the ledgers rather than hand-listed.
shippedLedgerStatuses,
} from './lint-liveness-properties.js';

/**
Expand DownExpand Up@@ -933,7 +936,7 @@ describe('the array fan-out, against a synthetic warn map (#10262)', () => {
// seam (#10262) — exactly the kind of verdict-level testing that seam exists
// for; the PLANNED branch is pinned against BOTH the real ledgers (so it stays
// a contract test) and a synthetic no-hint entry (to pin the DEFAULT wording).
describe('dead / experimental / planned verdicts are distinct, and unknown statuses fail loud (#11384)', () => {
describe('dead / experimental / planned / live-elsewhere verdicts are distinct, and unknown statuses fail loud (#11384, #14057)', () => {
const oneEntry = (entry: Record<string, unknown>) => new Map([['gizmo', entry]]);

// ── REAL LEDGER: the three rows the card captured ──────────────────────
Expand DownExpand Up@@ -1017,6 +1020,100 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
expect(findings[0].hint).not.toContain('Remove it');
});

// ── #14057: `live-elsewhere`, the fifth verdict — and the one whose wrong
// branch is the most expensive. #13483 added the status to the ledger and
// migrated `manifest.runtime` onto it (dead here by measurement, enforced by
// the cloud marketplace publish gate); `describe()` was not taught it, so the
// day any such row opts into `authorWarn` the author got a CRASH instead of
// the advisory finding — and the `dead` fallthrough it replaced would have
// been worse than the crash: "Remove it" about a key that is a live gate's
// input. ────────────────────────────────────────────────────────────────
it('SYNTHETIC: a live-elsewhere entry gets its OWN rule id and a keep-it hint — never the dead branch', () => {
const findings = checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'live-elsewhere', authorWarn: true }),
);
expect(findings).toHaveLength(1);
const [f] = findings;
expect(f.rule).toBe('liveness-live-elsewhere-property');
expect(f.rule).not.toBe('liveness-dead-property');
// The message must not read as a dead verdict...
expect(f.message).toContain('is enforced in a sibling repo');
expect(f.message).not.toContain('liveness: dead');
expect(f.message).not.toContain('has no runtime effect');
// ...and the default hint must point at the enforcer, not at a delete key.
expect(f.hint).not.toContain('Remove it');
expect(f.hint.toLowerCase()).toContain('keep it');
expect(f.hint).toContain('evidence');
});

it('SYNTHETIC: live-elsewhere keeps the shared hint precedence — authorHint over note over the default', () => {
const hintOf = (entry: Record<string, unknown>) =>
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 })).toContain('sibling repo');
});

// NEGATIVE CONTROL, on the REAL ledger, through the production path. The card
// is a fuse, not a fire: `shouldWarn()` gates entry to `describe()`, and the
// shipped `manifest.runtime` row does not carry `authorWarn`, so nothing
// reaches the new branch today. This pin holds that reading honest in both
// directions — if the row ever opts in, this goes red and the reviewer should
// UPDATE THIS PIN (the branch above is what makes that flip safe), never
// remove the branch.
//
// Anti-vacuity: `authorWarnedProperties('manifest')` would also be empty if
// the ledger were unreadable, so the guard is that `shippedLedgerStatuses()`
// sees `live-elsewhere` at all — the ONE row carrying it lives in that very
// file, so seeing the status proves the file was read.
it('REAL LEDGER: the live-elsewhere row exists and does NOT warn yet (the fuse, unlit)', () => {
expect(shippedLedgerStatuses().has('live-elsewhere')).toBe(true);
expect(authorWarnedProperties('manifest').has('runtime')).toBe(false);
});

// ── COVERAGE (#14057): describe() answers for every status the ledgers ship.
//
// Patching one status is what let this card repeat #11384 — so the pin is
// derived from the shipped rows rather than from a list somebody has to
// remember to edit. A sixth status appearing in any ledger fails HERE, by
// name, instead of waiting for an author to trip the sentinel throw.
//
// `live` is the one member that must NOT get a branch, and the source header
// says why: an entry only reaches `describe()` once `shouldWarn()` has said
// yes, so "`live` can in principle arrive here too (an entry marked
// `authorWarn: true` on a `live` row would be a ledger authoring mistake, not
// a user error)". A mistake in our own shipped data is exactly what the
// sentinel is for, so `live` is asserted LOUD here rather than handled.
it('COVERAGE: every status the shipped ledgers carry is answered by describe() — or is loud by design', () => {
const statuses = [...shippedLedgerStatuses()].sort();
// Anti-vacuity: an unreadable ledger dir returns the empty set, which would
// pass every assertion below without measuring anything.
expect(statuses.length).toBeGreaterThanOrEqual(4);
expect(statuses).toContain('live-elsewhere');

const rulesByStatus = new Map<string, string>();
for (const status of statuses) {
const run = () =>
checkItemAgainstWarnMap('gadget', { name: 'g1', gizmo: 'x' }, "gadget 'g1'", oneEntry({ status, authorWarn: true }));
if (status === 'live') {
expect(run).toThrow(/live/);
continue;
}
const findings = run();
expect(findings, `status ${status} produced no finding`).toHaveLength(1);
expect(findings[0].rule, `status ${status} has no rule id of its own`).toMatch(/^liveness-[a-z-]+-property$/);
expect(findings[0].hint.length, `status ${status} has an empty hint`).toBeGreaterThan(0);
rulesByStatus.set(status, findings[0].rule);
}

// #11384's lesson as an assertion: verdicts imply different author actions,
// so no two of them may share a rule id.
expect(new Set(rulesByStatus.values()).size).toBe(rulesByStatus.size);
});

// ── SYNTHETIC: the unknown-status boundary — loud, never silently `dead` ──
it('SYNTHETIC: an unrecognised status fails LOUD, naming the status, instead of silently grading as dead', () => {
expect(() =>
Expand All@@ -1027,6 +1124,16 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/quantum/);
// The message enumerates what describe() DOES know — #14057 is what happens
// when that list falls behind the branches, so pin them equal.
expect(() =>
checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/'experimental' \| 'planned' \| 'dead' \| 'live-elsewhere'/);
});

it('SYNTHETIC: a `live` row mistakenly marked authorWarn also fails LOUD rather than being graded dead', () => {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .changeset/lint-liveness-live-elsewhere-rule-id.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@objectstack/lint': minor
---

`lintLivenessProperties` no longer crashes on the `live-elsewhere` verdict — and never tells an author to remove a key a sibling repo enforces

`describe()` in `lint-liveness-properties.ts` knew three verdicts
(`experimental`, `planned`, `dead`) and threw, loudly and by design, on any
other. #13483 then shipped the ledger's fifth status — `live-elsewhere`: dead
HERE by measurement, genuinely enforced in a sibling repo — and migrated
`manifest.runtime` onto it (its enforcer is the cloud marketplace publish
gate). Nothing taught `describe()` about it, so the day any `live-elsewhere`
row opts into `authorWarn: true`, `os lint` would raise that
shipped-ledger-integrity error instead of the advisory warning the author
should get. No shipped row carries `authorWarn` today, so this was a fuse
rather than a fire.

`describe()` now has a fourth branch. `live-elsewhere` gets its own rule id —
`liveness-live-elsewhere-property`, exported as `LIVENESS_LIVE_ELSEWHERE_PROPERTY`
beside `LIVENESS_DEAD_PROPERTY` / `LIVENESS_EXPERIMENTAL_PROPERTY` /
`LIVENESS_PLANNED_PROPERTY` and advisory-only like them — plus its own message
(`is enforced in a sibling repo, not here`) and its own default hint, which keeps
the property and points at the ledger row's `evidence` for the enforcer. It
deliberately does **not** reuse the `dead` branch: that is the #11384 lesson,
which is that verdicts imply OPPOSITE author actions, and "Remove it" is the
single most damaging sentence available about a key whose enforcement is real
and remote — deleting it tears out a live gate's input. The sentinel throw
stays for genuinely unknown statuses, with its enumeration of the known ones
updated.

The suite gains a coverage pin derived from the shipped ledgers rather than from
a hand-written list: every distinct `status` those ledgers actually carry must be
answered by `describe()` with a rule id of its own, or (for `live`, which reaches
`describe()` only through a ledger-authoring mistake) must still fail loud. A
sixth status now fails that pin by name instead of waiting for an author to trip
the sentinel.
4 changes: 4 additions & 0 deletions packages/lint/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -748,6 +748,10 @@ export {
LIVENESS_DEAD_PROPERTY,
LIVENESS_EXPERIMENTAL_PROPERTY,
LIVENESS_PLANNED_PROPERTY,
// #14057 — the fifth ledger verdict's own rule id. `live-elsewhere` is dead
// HERE by measurement but genuinely enforced in a sibling repo, so it must
// never share the `dead` id: the two ask the author for opposite actions.
LIVENESS_LIVE_ELSEWHERE_PROPERTY,
} from './lint-liveness-properties.js';

export { lintAutonumberFormats } from './lint-autonumber-formats.js';
Expand Down
109 changes: 108 additions & 1 deletion packages/lint/src/lint-liveness-properties.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import {
// source for why this ONE property is tested off the ledger.
checkItemAgainstWarnMap,
getNested,
// #14057 coverage seam — the statuses the shipped ledgers actually carry, so
// the coverage pin below is derived from the ledgers rather than hand-listed.
shippedLedgerStatuses,
} from './lint-liveness-properties.js';

/**
Expand DownExpand Up@@ -933,7 +936,7 @@ describe('the array fan-out, against a synthetic warn map (#10262)', () => {
// seam (#10262) — exactly the kind of verdict-level testing that seam exists
// for; the PLANNED branch is pinned against BOTH the real ledgers (so it stays
// a contract test) and a synthetic no-hint entry (to pin the DEFAULT wording).
describe('dead / experimental / planned verdicts are distinct, and unknown statuses fail loud (#11384)', () => {
describe('dead / experimental / planned / live-elsewhere verdicts are distinct, and unknown statuses fail loud (#11384, #14057)', () => {
const oneEntry = (entry: Record<string, unknown>) => new Map([['gizmo', entry]]);

// ── REAL LEDGER: the three rows the card captured ──────────────────────
Expand DownExpand Up@@ -1017,6 +1020,100 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
expect(findings[0].hint).not.toContain('Remove it');
});

// ── #14057: `live-elsewhere`, the fifth verdict — and the one whose wrong
// branch is the most expensive. #13483 added the status to the ledger and
// migrated `manifest.runtime` onto it (dead here by measurement, enforced by
// the cloud marketplace publish gate); `describe()` was not taught it, so the
// day any such row opts into `authorWarn` the author got a CRASH instead of
// the advisory finding — and the `dead` fallthrough it replaced would have
// been worse than the crash: "Remove it" about a key that is a live gate's
// input. ────────────────────────────────────────────────────────────────
it('SYNTHETIC: a live-elsewhere entry gets its OWN rule id and a keep-it hint — never the dead branch', () => {
const findings = checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'live-elsewhere', authorWarn: true }),
);
expect(findings).toHaveLength(1);
const [f] = findings;
expect(f.rule).toBe('liveness-live-elsewhere-property');
expect(f.rule).not.toBe('liveness-dead-property');
// The message must not read as a dead verdict...
expect(f.message).toContain('is enforced in a sibling repo');
expect(f.message).not.toContain('liveness: dead');
expect(f.message).not.toContain('has no runtime effect');
// ...and the default hint must point at the enforcer, not at a delete key.
expect(f.hint).not.toContain('Remove it');
expect(f.hint.toLowerCase()).toContain('keep it');
expect(f.hint).toContain('evidence');
});

it('SYNTHETIC: live-elsewhere keeps the shared hint precedence — authorHint over note over the default', () => {
const hintOf = (entry: Record<string, unknown>) =>
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 })).toContain('sibling repo');
});

// NEGATIVE CONTROL, on the REAL ledger, through the production path. The card
// is a fuse, not a fire: `shouldWarn()` gates entry to `describe()`, and the
// shipped `manifest.runtime` row does not carry `authorWarn`, so nothing
// reaches the new branch today. This pin holds that reading honest in both
// directions — if the row ever opts in, this goes red and the reviewer should
// UPDATE THIS PIN (the branch above is what makes that flip safe), never
// remove the branch.
//
// Anti-vacuity: `authorWarnedProperties('manifest')` would also be empty if
// the ledger were unreadable, so the guard is that `shippedLedgerStatuses()`
// sees `live-elsewhere` at all — the ONE row carrying it lives in that very
// file, so seeing the status proves the file was read.
it('REAL LEDGER: the live-elsewhere row exists and does NOT warn yet (the fuse, unlit)', () => {
expect(shippedLedgerStatuses().has('live-elsewhere')).toBe(true);
expect(authorWarnedProperties('manifest').has('runtime')).toBe(false);
});

// ── COVERAGE (#14057): describe() answers for every status the ledgers ship.
//
// Patching one status is what let this card repeat #11384 — so the pin is
// derived from the shipped rows rather than from a list somebody has to
// remember to edit. A sixth status appearing in any ledger fails HERE, by
// name, instead of waiting for an author to trip the sentinel throw.
//
// `live` is the one member that must NOT get a branch, and the source header
// says why: an entry only reaches `describe()` once `shouldWarn()` has said
// yes, so "`live` can in principle arrive here too (an entry marked
// `authorWarn: true` on a `live` row would be a ledger authoring mistake, not
// a user error)". A mistake in our own shipped data is exactly what the
// sentinel is for, so `live` is asserted LOUD here rather than handled.
it('COVERAGE: every status the shipped ledgers carry is answered by describe() — or is loud by design', () => {
const statuses = [...shippedLedgerStatuses()].sort();
// Anti-vacuity: an unreadable ledger dir returns the empty set, which would
// pass every assertion below without measuring anything.
expect(statuses.length).toBeGreaterThanOrEqual(4);
expect(statuses).toContain('live-elsewhere');

const rulesByStatus = new Map<string, string>();
for (const status of statuses) {
const run = () =>
checkItemAgainstWarnMap('gadget', { name: 'g1', gizmo: 'x' }, "gadget 'g1'", oneEntry({ status, authorWarn: true }));
if (status === 'live') {
expect(run).toThrow(/live/);
continue;
}
const findings = run();
expect(findings, `status ${status} produced no finding`).toHaveLength(1);
expect(findings[0].rule, `status ${status} has no rule id of its own`).toMatch(/^liveness-[a-z-]+-property$/);
expect(findings[0].hint.length, `status ${status} has an empty hint`).toBeGreaterThan(0);
rulesByStatus.set(status, findings[0].rule);
}

// #11384's lesson as an assertion: verdicts imply different author actions,
// so no two of them may share a rule id.
expect(new Set(rulesByStatus.values()).size).toBe(rulesByStatus.size);
});

// ── SYNTHETIC: the unknown-status boundary — loud, never silently `dead` ──
it('SYNTHETIC: an unrecognised status fails LOUD, naming the status, instead of silently grading as dead', () => {
expect(() =>
Expand All@@ -1027,6 +1124,16 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/quantum/);
// The message enumerates what describe() DOES know — #14057 is what happens
// when that list falls behind the branches, so pin them equal.
expect(() =>
checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/'experimental' \| 'planned' \| 'dead' \| 'live-elsewhere'/);
});

it('SYNTHETIC: a `live` row mistakenly marked authorWarn also fails LOUD rather than being graded dead', () => {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .changeset/lint-liveness-live-elsewhere-rule-id.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@objectstack/lint': minor
---

`lintLivenessProperties` no longer crashes on the `live-elsewhere` verdict — and never tells an author to remove a key a sibling repo enforces

`describe()` in `lint-liveness-properties.ts` knew three verdicts
(`experimental`, `planned`, `dead`) and threw, loudly and by design, on any
other. #13483 then shipped the ledger's fifth status — `live-elsewhere`: dead
HERE by measurement, genuinely enforced in a sibling repo — and migrated
`manifest.runtime` onto it (its enforcer is the cloud marketplace publish
gate). Nothing taught `describe()` about it, so the day any `live-elsewhere`
row opts into `authorWarn: true`, `os lint` would raise that
shipped-ledger-integrity error instead of the advisory warning the author
should get. No shipped row carries `authorWarn` today, so this was a fuse
rather than a fire.

`describe()` now has a fourth branch. `live-elsewhere` gets its own rule id —
`liveness-live-elsewhere-property`, exported as `LIVENESS_LIVE_ELSEWHERE_PROPERTY`
beside `LIVENESS_DEAD_PROPERTY` / `LIVENESS_EXPERIMENTAL_PROPERTY` /
`LIVENESS_PLANNED_PROPERTY` and advisory-only like them — plus its own message
(`is enforced in a sibling repo, not here`) and its own default hint, which keeps
the property and points at the ledger row's `evidence` for the enforcer. It
deliberately does **not** reuse the `dead` branch: that is the #11384 lesson,
which is that verdicts imply OPPOSITE author actions, and "Remove it" is the
single most damaging sentence available about a key whose enforcement is real
and remote — deleting it tears out a live gate's input. The sentinel throw
stays for genuinely unknown statuses, with its enumeration of the known ones
updated.

The suite gains a coverage pin derived from the shipped ledgers rather than from
a hand-written list: every distinct `status` those ledgers actually carry must be
answered by `describe()` with a rule id of its own, or (for `live`, which reaches
`describe()` only through a ledger-authoring mistake) must still fail loud. A
sixth status now fails that pin by name instead of waiting for an author to trip
the sentinel.
4 changes: 4 additions & 0 deletions packages/lint/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -748,6 +748,10 @@ export {
LIVENESS_DEAD_PROPERTY,
LIVENESS_EXPERIMENTAL_PROPERTY,
LIVENESS_PLANNED_PROPERTY,
// #14057 — the fifth ledger verdict's own rule id. `live-elsewhere` is dead
// HERE by measurement but genuinely enforced in a sibling repo, so it must
// never share the `dead` id: the two ask the author for opposite actions.
LIVENESS_LIVE_ELSEWHERE_PROPERTY,
} from './lint-liveness-properties.js';

export { lintAutonumberFormats } from './lint-autonumber-formats.js';
Expand Down
109 changes: 108 additions & 1 deletion packages/lint/src/lint-liveness-properties.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import {
// source for why this ONE property is tested off the ledger.
checkItemAgainstWarnMap,
getNested,
// #14057 coverage seam — the statuses the shipped ledgers actually carry, so
// the coverage pin below is derived from the ledgers rather than hand-listed.
shippedLedgerStatuses,
} from './lint-liveness-properties.js';

/**
Expand DownExpand Up@@ -933,7 +936,7 @@ describe('the array fan-out, against a synthetic warn map (#10262)', () => {
// seam (#10262) — exactly the kind of verdict-level testing that seam exists
// for; the PLANNED branch is pinned against BOTH the real ledgers (so it stays
// a contract test) and a synthetic no-hint entry (to pin the DEFAULT wording).
describe('dead / experimental / planned verdicts are distinct, and unknown statuses fail loud (#11384)', () => {
describe('dead / experimental / planned / live-elsewhere verdicts are distinct, and unknown statuses fail loud (#11384, #14057)', () => {
const oneEntry = (entry: Record<string, unknown>) => new Map([['gizmo', entry]]);

// ── REAL LEDGER: the three rows the card captured ──────────────────────
Expand DownExpand Up@@ -1017,6 +1020,100 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
expect(findings[0].hint).not.toContain('Remove it');
});

// ── #14057: `live-elsewhere`, the fifth verdict — and the one whose wrong
// branch is the most expensive. #13483 added the status to the ledger and
// migrated `manifest.runtime` onto it (dead here by measurement, enforced by
// the cloud marketplace publish gate); `describe()` was not taught it, so the
// day any such row opts into `authorWarn` the author got a CRASH instead of
// the advisory finding — and the `dead` fallthrough it replaced would have
// been worse than the crash: "Remove it" about a key that is a live gate's
// input. ────────────────────────────────────────────────────────────────
it('SYNTHETIC: a live-elsewhere entry gets its OWN rule id and a keep-it hint — never the dead branch', () => {
const findings = checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'live-elsewhere', authorWarn: true }),
);
expect(findings).toHaveLength(1);
const [f] = findings;
expect(f.rule).toBe('liveness-live-elsewhere-property');
expect(f.rule).not.toBe('liveness-dead-property');
// The message must not read as a dead verdict...
expect(f.message).toContain('is enforced in a sibling repo');
expect(f.message).not.toContain('liveness: dead');
expect(f.message).not.toContain('has no runtime effect');
// ...and the default hint must point at the enforcer, not at a delete key.
expect(f.hint).not.toContain('Remove it');
expect(f.hint.toLowerCase()).toContain('keep it');
expect(f.hint).toContain('evidence');
});

it('SYNTHETIC: live-elsewhere keeps the shared hint precedence — authorHint over note over the default', () => {
const hintOf = (entry: Record<string, unknown>) =>
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 })).toContain('sibling repo');
});

// NEGATIVE CONTROL, on the REAL ledger, through the production path. The card
// is a fuse, not a fire: `shouldWarn()` gates entry to `describe()`, and the
// shipped `manifest.runtime` row does not carry `authorWarn`, so nothing
// reaches the new branch today. This pin holds that reading honest in both
// directions — if the row ever opts in, this goes red and the reviewer should
// UPDATE THIS PIN (the branch above is what makes that flip safe), never
// remove the branch.
//
// Anti-vacuity: `authorWarnedProperties('manifest')` would also be empty if
// the ledger were unreadable, so the guard is that `shippedLedgerStatuses()`
// sees `live-elsewhere` at all — the ONE row carrying it lives in that very
// file, so seeing the status proves the file was read.
it('REAL LEDGER: the live-elsewhere row exists and does NOT warn yet (the fuse, unlit)', () => {
expect(shippedLedgerStatuses().has('live-elsewhere')).toBe(true);
expect(authorWarnedProperties('manifest').has('runtime')).toBe(false);
});

// ── COVERAGE (#14057): describe() answers for every status the ledgers ship.
//
// Patching one status is what let this card repeat #11384 — so the pin is
// derived from the shipped rows rather than from a list somebody has to
// remember to edit. A sixth status appearing in any ledger fails HERE, by
// name, instead of waiting for an author to trip the sentinel throw.
//
// `live` is the one member that must NOT get a branch, and the source header
// says why: an entry only reaches `describe()` once `shouldWarn()` has said
// yes, so "`live` can in principle arrive here too (an entry marked
// `authorWarn: true` on a `live` row would be a ledger authoring mistake, not
// a user error)". A mistake in our own shipped data is exactly what the
// sentinel is for, so `live` is asserted LOUD here rather than handled.
it('COVERAGE: every status the shipped ledgers carry is answered by describe() — or is loud by design', () => {
const statuses = [...shippedLedgerStatuses()].sort();
// Anti-vacuity: an unreadable ledger dir returns the empty set, which would
// pass every assertion below without measuring anything.
expect(statuses.length).toBeGreaterThanOrEqual(4);
expect(statuses).toContain('live-elsewhere');

const rulesByStatus = new Map<string, string>();
for (const status of statuses) {
const run = () =>
checkItemAgainstWarnMap('gadget', { name: 'g1', gizmo: 'x' }, "gadget 'g1'", oneEntry({ status, authorWarn: true }));
if (status === 'live') {
expect(run).toThrow(/live/);
continue;
}
const findings = run();
expect(findings, `status ${status} produced no finding`).toHaveLength(1);
expect(findings[0].rule, `status ${status} has no rule id of its own`).toMatch(/^liveness-[a-z-]+-property$/);
expect(findings[0].hint.length, `status ${status} has an empty hint`).toBeGreaterThan(0);
rulesByStatus.set(status, findings[0].rule);
}

// #11384's lesson as an assertion: verdicts imply different author actions,
// so no two of them may share a rule id.
expect(new Set(rulesByStatus.values()).size).toBe(rulesByStatus.size);
});

// ── SYNTHETIC: the unknown-status boundary — loud, never silently `dead` ──
it('SYNTHETIC: an unrecognised status fails LOUD, naming the status, instead of silently grading as dead', () => {
expect(() =>
Expand All@@ -1027,6 +1124,16 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/quantum/);
// The message enumerates what describe() DOES know — #14057 is what happens
// when that list falls behind the branches, so pin them equal.
expect(() =>
checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/'experimental' \| 'planned' \| 'dead' \| 'live-elsewhere'/);
});

it('SYNTHETIC: a `live` row mistakenly marked authorWarn also fails LOUD rather than being graded dead', () => {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .changeset/lint-liveness-live-elsewhere-rule-id.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@objectstack/lint': minor
---

`lintLivenessProperties` no longer crashes on the `live-elsewhere` verdict — and never tells an author to remove a key a sibling repo enforces

`describe()` in `lint-liveness-properties.ts` knew three verdicts
(`experimental`, `planned`, `dead`) and threw, loudly and by design, on any
other. #13483 then shipped the ledger's fifth status — `live-elsewhere`: dead
HERE by measurement, genuinely enforced in a sibling repo — and migrated
`manifest.runtime` onto it (its enforcer is the cloud marketplace publish
gate). Nothing taught `describe()` about it, so the day any `live-elsewhere`
row opts into `authorWarn: true`, `os lint` would raise that
shipped-ledger-integrity error instead of the advisory warning the author
should get. No shipped row carries `authorWarn` today, so this was a fuse
rather than a fire.

`describe()` now has a fourth branch. `live-elsewhere` gets its own rule id —
`liveness-live-elsewhere-property`, exported as `LIVENESS_LIVE_ELSEWHERE_PROPERTY`
beside `LIVENESS_DEAD_PROPERTY` / `LIVENESS_EXPERIMENTAL_PROPERTY` /
`LIVENESS_PLANNED_PROPERTY` and advisory-only like them — plus its own message
(`is enforced in a sibling repo, not here`) and its own default hint, which keeps
the property and points at the ledger row's `evidence` for the enforcer. It
deliberately does **not** reuse the `dead` branch: that is the #11384 lesson,
which is that verdicts imply OPPOSITE author actions, and "Remove it" is the
single most damaging sentence available about a key whose enforcement is real
and remote — deleting it tears out a live gate's input. The sentinel throw
stays for genuinely unknown statuses, with its enumeration of the known ones
updated.

The suite gains a coverage pin derived from the shipped ledgers rather than from
a hand-written list: every distinct `status` those ledgers actually carry must be
answered by `describe()` with a rule id of its own, or (for `live`, which reaches
`describe()` only through a ledger-authoring mistake) must still fail loud. A
sixth status now fails that pin by name instead of waiting for an author to trip
the sentinel.
4 changes: 4 additions & 0 deletions packages/lint/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -748,6 +748,10 @@ export {
LIVENESS_DEAD_PROPERTY,
LIVENESS_EXPERIMENTAL_PROPERTY,
LIVENESS_PLANNED_PROPERTY,
// #14057 — the fifth ledger verdict's own rule id. `live-elsewhere` is dead
// HERE by measurement but genuinely enforced in a sibling repo, so it must
// never share the `dead` id: the two ask the author for opposite actions.
LIVENESS_LIVE_ELSEWHERE_PROPERTY,
} from './lint-liveness-properties.js';

export { lintAutonumberFormats } from './lint-autonumber-formats.js';
Expand Down
109 changes: 108 additions & 1 deletion packages/lint/src/lint-liveness-properties.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import {
// source for why this ONE property is tested off the ledger.
checkItemAgainstWarnMap,
getNested,
// #14057 coverage seam — the statuses the shipped ledgers actually carry, so
// the coverage pin below is derived from the ledgers rather than hand-listed.
shippedLedgerStatuses,
} from './lint-liveness-properties.js';

/**
Expand DownExpand Up@@ -933,7 +936,7 @@ describe('the array fan-out, against a synthetic warn map (#10262)', () => {
// seam (#10262) — exactly the kind of verdict-level testing that seam exists
// for; the PLANNED branch is pinned against BOTH the real ledgers (so it stays
// a contract test) and a synthetic no-hint entry (to pin the DEFAULT wording).
describe('dead / experimental / planned verdicts are distinct, and unknown statuses fail loud (#11384)', () => {
describe('dead / experimental / planned / live-elsewhere verdicts are distinct, and unknown statuses fail loud (#11384, #14057)', () => {
const oneEntry = (entry: Record<string, unknown>) => new Map([['gizmo', entry]]);

// ── REAL LEDGER: the three rows the card captured ──────────────────────
Expand DownExpand Up@@ -1017,6 +1020,100 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
expect(findings[0].hint).not.toContain('Remove it');
});

// ── #14057: `live-elsewhere`, the fifth verdict — and the one whose wrong
// branch is the most expensive. #13483 added the status to the ledger and
// migrated `manifest.runtime` onto it (dead here by measurement, enforced by
// the cloud marketplace publish gate); `describe()` was not taught it, so the
// day any such row opts into `authorWarn` the author got a CRASH instead of
// the advisory finding — and the `dead` fallthrough it replaced would have
// been worse than the crash: "Remove it" about a key that is a live gate's
// input. ────────────────────────────────────────────────────────────────
it('SYNTHETIC: a live-elsewhere entry gets its OWN rule id and a keep-it hint — never the dead branch', () => {
const findings = checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'live-elsewhere', authorWarn: true }),
);
expect(findings).toHaveLength(1);
const [f] = findings;
expect(f.rule).toBe('liveness-live-elsewhere-property');
expect(f.rule).not.toBe('liveness-dead-property');
// The message must not read as a dead verdict...
expect(f.message).toContain('is enforced in a sibling repo');
expect(f.message).not.toContain('liveness: dead');
expect(f.message).not.toContain('has no runtime effect');
// ...and the default hint must point at the enforcer, not at a delete key.
expect(f.hint).not.toContain('Remove it');
expect(f.hint.toLowerCase()).toContain('keep it');
expect(f.hint).toContain('evidence');
});

it('SYNTHETIC: live-elsewhere keeps the shared hint precedence — authorHint over note over the default', () => {
const hintOf = (entry: Record<string, unknown>) =>
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 })).toContain('sibling repo');
});

// NEGATIVE CONTROL, on the REAL ledger, through the production path. The card
// is a fuse, not a fire: `shouldWarn()` gates entry to `describe()`, and the
// shipped `manifest.runtime` row does not carry `authorWarn`, so nothing
// reaches the new branch today. This pin holds that reading honest in both
// directions — if the row ever opts in, this goes red and the reviewer should
// UPDATE THIS PIN (the branch above is what makes that flip safe), never
// remove the branch.
//
// Anti-vacuity: `authorWarnedProperties('manifest')` would also be empty if
// the ledger were unreadable, so the guard is that `shippedLedgerStatuses()`
// sees `live-elsewhere` at all — the ONE row carrying it lives in that very
// file, so seeing the status proves the file was read.
it('REAL LEDGER: the live-elsewhere row exists and does NOT warn yet (the fuse, unlit)', () => {
expect(shippedLedgerStatuses().has('live-elsewhere')).toBe(true);
expect(authorWarnedProperties('manifest').has('runtime')).toBe(false);
});

// ── COVERAGE (#14057): describe() answers for every status the ledgers ship.
//
// Patching one status is what let this card repeat #11384 — so the pin is
// derived from the shipped rows rather than from a list somebody has to
// remember to edit. A sixth status appearing in any ledger fails HERE, by
// name, instead of waiting for an author to trip the sentinel throw.
//
// `live` is the one member that must NOT get a branch, and the source header
// says why: an entry only reaches `describe()` once `shouldWarn()` has said
// yes, so "`live` can in principle arrive here too (an entry marked
// `authorWarn: true` on a `live` row would be a ledger authoring mistake, not
// a user error)". A mistake in our own shipped data is exactly what the
// sentinel is for, so `live` is asserted LOUD here rather than handled.
it('COVERAGE: every status the shipped ledgers carry is answered by describe() — or is loud by design', () => {
const statuses = [...shippedLedgerStatuses()].sort();
// Anti-vacuity: an unreadable ledger dir returns the empty set, which would
// pass every assertion below without measuring anything.
expect(statuses.length).toBeGreaterThanOrEqual(4);
expect(statuses).toContain('live-elsewhere');

const rulesByStatus = new Map<string, string>();
for (const status of statuses) {
const run = () =>
checkItemAgainstWarnMap('gadget', { name: 'g1', gizmo: 'x' }, "gadget 'g1'", oneEntry({ status, authorWarn: true }));
if (status === 'live') {
expect(run).toThrow(/live/);
continue;
}
const findings = run();
expect(findings, `status ${status} produced no finding`).toHaveLength(1);
expect(findings[0].rule, `status ${status} has no rule id of its own`).toMatch(/^liveness-[a-z-]+-property$/);
expect(findings[0].hint.length, `status ${status} has an empty hint`).toBeGreaterThan(0);
rulesByStatus.set(status, findings[0].rule);
}

// #11384's lesson as an assertion: verdicts imply different author actions,
// so no two of them may share a rule id.
expect(new Set(rulesByStatus.values()).size).toBe(rulesByStatus.size);
});

// ── SYNTHETIC: the unknown-status boundary — loud, never silently `dead` ──
it('SYNTHETIC: an unrecognised status fails LOUD, naming the status, instead of silently grading as dead', () => {
expect(() =>
Expand All@@ -1027,6 +1124,16 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/quantum/);
// The message enumerates what describe() DOES know — #14057 is what happens
// when that list falls behind the branches, so pin them equal.
expect(() =>
checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/'experimental' \| 'planned' \| 'dead' \| 'live-elsewhere'/);
});

it('SYNTHETIC: a `live` row mistakenly marked authorWarn also fails LOUD rather than being graded dead', () => {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .changeset/lint-liveness-live-elsewhere-rule-id.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@objectstack/lint': minor
---

`lintLivenessProperties` no longer crashes on the `live-elsewhere` verdict — and never tells an author to remove a key a sibling repo enforces

`describe()` in `lint-liveness-properties.ts` knew three verdicts
(`experimental`, `planned`, `dead`) and threw, loudly and by design, on any
other. #13483 then shipped the ledger's fifth status — `live-elsewhere`: dead
HERE by measurement, genuinely enforced in a sibling repo — and migrated
`manifest.runtime` onto it (its enforcer is the cloud marketplace publish
gate). Nothing taught `describe()` about it, so the day any `live-elsewhere`
row opts into `authorWarn: true`, `os lint` would raise that
shipped-ledger-integrity error instead of the advisory warning the author
should get. No shipped row carries `authorWarn` today, so this was a fuse
rather than a fire.

`describe()` now has a fourth branch. `live-elsewhere` gets its own rule id —
`liveness-live-elsewhere-property`, exported as `LIVENESS_LIVE_ELSEWHERE_PROPERTY`
beside `LIVENESS_DEAD_PROPERTY` / `LIVENESS_EXPERIMENTAL_PROPERTY` /
`LIVENESS_PLANNED_PROPERTY` and advisory-only like them — plus its own message
(`is enforced in a sibling repo, not here`) and its own default hint, which keeps
the property and points at the ledger row's `evidence` for the enforcer. It
deliberately does **not** reuse the `dead` branch: that is the #11384 lesson,
which is that verdicts imply OPPOSITE author actions, and "Remove it" is the
single most damaging sentence available about a key whose enforcement is real
and remote — deleting it tears out a live gate's input. The sentinel throw
stays for genuinely unknown statuses, with its enumeration of the known ones
updated.

The suite gains a coverage pin derived from the shipped ledgers rather than from
a hand-written list: every distinct `status` those ledgers actually carry must be
answered by `describe()` with a rule id of its own, or (for `live`, which reaches
`describe()` only through a ledger-authoring mistake) must still fail loud. A
sixth status now fails that pin by name instead of waiting for an author to trip
the sentinel.
4 changes: 4 additions & 0 deletions packages/lint/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -748,6 +748,10 @@ export {
LIVENESS_DEAD_PROPERTY,
LIVENESS_EXPERIMENTAL_PROPERTY,
LIVENESS_PLANNED_PROPERTY,
// #14057 — the fifth ledger verdict's own rule id. `live-elsewhere` is dead
// HERE by measurement but genuinely enforced in a sibling repo, so it must
// never share the `dead` id: the two ask the author for opposite actions.
LIVENESS_LIVE_ELSEWHERE_PROPERTY,
} from './lint-liveness-properties.js';

export { lintAutonumberFormats } from './lint-autonumber-formats.js';
Expand Down
109 changes: 108 additions & 1 deletion packages/lint/src/lint-liveness-properties.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import {
// source for why this ONE property is tested off the ledger.
checkItemAgainstWarnMap,
getNested,
// #14057 coverage seam — the statuses the shipped ledgers actually carry, so
// the coverage pin below is derived from the ledgers rather than hand-listed.
shippedLedgerStatuses,
} from './lint-liveness-properties.js';

/**
Expand DownExpand Up@@ -933,7 +936,7 @@ describe('the array fan-out, against a synthetic warn map (#10262)', () => {
// seam (#10262) — exactly the kind of verdict-level testing that seam exists
// for; the PLANNED branch is pinned against BOTH the real ledgers (so it stays
// a contract test) and a synthetic no-hint entry (to pin the DEFAULT wording).
describe('dead / experimental / planned verdicts are distinct, and unknown statuses fail loud (#11384)', () => {
describe('dead / experimental / planned / live-elsewhere verdicts are distinct, and unknown statuses fail loud (#11384, #14057)', () => {
const oneEntry = (entry: Record<string, unknown>) => new Map([['gizmo', entry]]);

// ── REAL LEDGER: the three rows the card captured ──────────────────────
Expand DownExpand Up@@ -1017,6 +1020,100 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
expect(findings[0].hint).not.toContain('Remove it');
});

// ── #14057: `live-elsewhere`, the fifth verdict — and the one whose wrong
// branch is the most expensive. #13483 added the status to the ledger and
// migrated `manifest.runtime` onto it (dead here by measurement, enforced by
// the cloud marketplace publish gate); `describe()` was not taught it, so the
// day any such row opts into `authorWarn` the author got a CRASH instead of
// the advisory finding — and the `dead` fallthrough it replaced would have
// been worse than the crash: "Remove it" about a key that is a live gate's
// input. ────────────────────────────────────────────────────────────────
it('SYNTHETIC: a live-elsewhere entry gets its OWN rule id and a keep-it hint — never the dead branch', () => {
const findings = checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'live-elsewhere', authorWarn: true }),
);
expect(findings).toHaveLength(1);
const [f] = findings;
expect(f.rule).toBe('liveness-live-elsewhere-property');
expect(f.rule).not.toBe('liveness-dead-property');
// The message must not read as a dead verdict...
expect(f.message).toContain('is enforced in a sibling repo');
expect(f.message).not.toContain('liveness: dead');
expect(f.message).not.toContain('has no runtime effect');
// ...and the default hint must point at the enforcer, not at a delete key.
expect(f.hint).not.toContain('Remove it');
expect(f.hint.toLowerCase()).toContain('keep it');
expect(f.hint).toContain('evidence');
});

it('SYNTHETIC: live-elsewhere keeps the shared hint precedence — authorHint over note over the default', () => {
const hintOf = (entry: Record<string, unknown>) =>
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 })).toContain('sibling repo');
});

// NEGATIVE CONTROL, on the REAL ledger, through the production path. The card
// is a fuse, not a fire: `shouldWarn()` gates entry to `describe()`, and the
// shipped `manifest.runtime` row does not carry `authorWarn`, so nothing
// reaches the new branch today. This pin holds that reading honest in both
// directions — if the row ever opts in, this goes red and the reviewer should
// UPDATE THIS PIN (the branch above is what makes that flip safe), never
// remove the branch.
//
// Anti-vacuity: `authorWarnedProperties('manifest')` would also be empty if
// the ledger were unreadable, so the guard is that `shippedLedgerStatuses()`
// sees `live-elsewhere` at all — the ONE row carrying it lives in that very
// file, so seeing the status proves the file was read.
it('REAL LEDGER: the live-elsewhere row exists and does NOT warn yet (the fuse, unlit)', () => {
expect(shippedLedgerStatuses().has('live-elsewhere')).toBe(true);
expect(authorWarnedProperties('manifest').has('runtime')).toBe(false);
});

// ── COVERAGE (#14057): describe() answers for every status the ledgers ship.
//
// Patching one status is what let this card repeat #11384 — so the pin is
// derived from the shipped rows rather than from a list somebody has to
// remember to edit. A sixth status appearing in any ledger fails HERE, by
// name, instead of waiting for an author to trip the sentinel throw.
//
// `live` is the one member that must NOT get a branch, and the source header
// says why: an entry only reaches `describe()` once `shouldWarn()` has said
// yes, so "`live` can in principle arrive here too (an entry marked
// `authorWarn: true` on a `live` row would be a ledger authoring mistake, not
// a user error)". A mistake in our own shipped data is exactly what the
// sentinel is for, so `live` is asserted LOUD here rather than handled.
it('COVERAGE: every status the shipped ledgers carry is answered by describe() — or is loud by design', () => {
const statuses = [...shippedLedgerStatuses()].sort();
// Anti-vacuity: an unreadable ledger dir returns the empty set, which would
// pass every assertion below without measuring anything.
expect(statuses.length).toBeGreaterThanOrEqual(4);
expect(statuses).toContain('live-elsewhere');

const rulesByStatus = new Map<string, string>();
for (const status of statuses) {
const run = () =>
checkItemAgainstWarnMap('gadget', { name: 'g1', gizmo: 'x' }, "gadget 'g1'", oneEntry({ status, authorWarn: true }));
if (status === 'live') {
expect(run).toThrow(/live/);
continue;
}
const findings = run();
expect(findings, `status ${status} produced no finding`).toHaveLength(1);
expect(findings[0].rule, `status ${status} has no rule id of its own`).toMatch(/^liveness-[a-z-]+-property$/);
expect(findings[0].hint.length, `status ${status} has an empty hint`).toBeGreaterThan(0);
rulesByStatus.set(status, findings[0].rule);
}

// #11384's lesson as an assertion: verdicts imply different author actions,
// so no two of them may share a rule id.
expect(new Set(rulesByStatus.values()).size).toBe(rulesByStatus.size);
});

// ── SYNTHETIC: the unknown-status boundary — loud, never silently `dead` ──
it('SYNTHETIC: an unrecognised status fails LOUD, naming the status, instead of silently grading as dead', () => {
expect(() =>
Expand All@@ -1027,6 +1124,16 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/quantum/);
// The message enumerates what describe() DOES know — #14057 is what happens
// when that list falls behind the branches, so pin them equal.
expect(() =>
checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/'experimental' \| 'planned' \| 'dead' \| 'live-elsewhere'/);
});

it('SYNTHETIC: a `live` row mistakenly marked authorWarn also fails LOUD rather than being graded dead', () => {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .changeset/lint-liveness-live-elsewhere-rule-id.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
'@objectstack/lint': minor
---

`lintLivenessProperties` no longer crashes on the `live-elsewhere` verdict — and never tells an author to remove a key a sibling repo enforces

`describe()` in `lint-liveness-properties.ts` knew three verdicts
(`experimental`, `planned`, `dead`) and threw, loudly and by design, on any
other. #13483 then shipped the ledger's fifth status — `live-elsewhere`: dead
HERE by measurement, genuinely enforced in a sibling repo — and migrated
`manifest.runtime` onto it (its enforcer is the cloud marketplace publish
gate). Nothing taught `describe()` about it, so the day any `live-elsewhere`
row opts into `authorWarn: true`, `os lint` would raise that
shipped-ledger-integrity error instead of the advisory warning the author
should get. No shipped row carries `authorWarn` today, so this was a fuse
rather than a fire.

`describe()` now has a fourth branch. `live-elsewhere` gets its own rule id —
`liveness-live-elsewhere-property`, exported as `LIVENESS_LIVE_ELSEWHERE_PROPERTY`
beside `LIVENESS_DEAD_PROPERTY` / `LIVENESS_EXPERIMENTAL_PROPERTY` /
`LIVENESS_PLANNED_PROPERTY` and advisory-only like them — plus its own message
(`is enforced in a sibling repo, not here`) and its own default hint, which keeps
the property and points at the ledger row's `evidence` for the enforcer. It
deliberately does **not** reuse the `dead` branch: that is the #11384 lesson,
which is that verdicts imply OPPOSITE author actions, and "Remove it" is the
single most damaging sentence available about a key whose enforcement is real
and remote — deleting it tears out a live gate's input. The sentinel throw
stays for genuinely unknown statuses, with its enumeration of the known ones
updated.

The suite gains a coverage pin derived from the shipped ledgers rather than from
a hand-written list: every distinct `status` those ledgers actually carry must be
answered by `describe()` with a rule id of its own, or (for `live`, which reaches
`describe()` only through a ledger-authoring mistake) must still fail loud. A
sixth status now fails that pin by name instead of waiting for an author to trip
the sentinel.
4 changes: 4 additions & 0 deletions packages/lint/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -748,6 +748,10 @@ export {
LIVENESS_DEAD_PROPERTY,
LIVENESS_EXPERIMENTAL_PROPERTY,
LIVENESS_PLANNED_PROPERTY,
// #14057 — the fifth ledger verdict's own rule id. `live-elsewhere` is dead
// HERE by measurement but genuinely enforced in a sibling repo, so it must
// never share the `dead` id: the two ask the author for opposite actions.
LIVENESS_LIVE_ELSEWHERE_PROPERTY,
} from './lint-liveness-properties.js';

export { lintAutonumberFormats } from './lint-autonumber-formats.js';
Expand Down
109 changes: 108 additions & 1 deletion packages/lint/src/lint-liveness-properties.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,9 @@ import {
// source for why this ONE property is tested off the ledger.
checkItemAgainstWarnMap,
getNested,
// #14057 coverage seam — the statuses the shipped ledgers actually carry, so
// the coverage pin below is derived from the ledgers rather than hand-listed.
shippedLedgerStatuses,
} from './lint-liveness-properties.js';

/**
Expand DownExpand Up@@ -933,7 +936,7 @@ describe('the array fan-out, against a synthetic warn map (#10262)', () => {
// seam (#10262) — exactly the kind of verdict-level testing that seam exists
// for; the PLANNED branch is pinned against BOTH the real ledgers (so it stays
// a contract test) and a synthetic no-hint entry (to pin the DEFAULT wording).
describe('dead / experimental / planned verdicts are distinct, and unknown statuses fail loud (#11384)', () => {
describe('dead / experimental / planned / live-elsewhere verdicts are distinct, and unknown statuses fail loud (#11384, #14057)', () => {
const oneEntry = (entry: Record<string, unknown>) => new Map([['gizmo', entry]]);

// ── REAL LEDGER: the three rows the card captured ──────────────────────
Expand DownExpand Up@@ -1017,6 +1020,100 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
expect(findings[0].hint).not.toContain('Remove it');
});

// ── #14057: `live-elsewhere`, the fifth verdict — and the one whose wrong
// branch is the most expensive. #13483 added the status to the ledger and
// migrated `manifest.runtime` onto it (dead here by measurement, enforced by
// the cloud marketplace publish gate); `describe()` was not taught it, so the
// day any such row opts into `authorWarn` the author got a CRASH instead of
// the advisory finding — and the `dead` fallthrough it replaced would have
// been worse than the crash: "Remove it" about a key that is a live gate's
// input. ────────────────────────────────────────────────────────────────
it('SYNTHETIC: a live-elsewhere entry gets its OWN rule id and a keep-it hint — never the dead branch', () => {
const findings = checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'live-elsewhere', authorWarn: true }),
);
expect(findings).toHaveLength(1);
const [f] = findings;
expect(f.rule).toBe('liveness-live-elsewhere-property');
expect(f.rule).not.toBe('liveness-dead-property');
// The message must not read as a dead verdict...
expect(f.message).toContain('is enforced in a sibling repo');
expect(f.message).not.toContain('liveness: dead');
expect(f.message).not.toContain('has no runtime effect');
// ...and the default hint must point at the enforcer, not at a delete key.
expect(f.hint).not.toContain('Remove it');
expect(f.hint.toLowerCase()).toContain('keep it');
expect(f.hint).toContain('evidence');
});

it('SYNTHETIC: live-elsewhere keeps the shared hint precedence — authorHint over note over the default', () => {
const hintOf = (entry: Record<string, unknown>) =>
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 })).toContain('sibling repo');
});

// NEGATIVE CONTROL, on the REAL ledger, through the production path. The card
// is a fuse, not a fire: `shouldWarn()` gates entry to `describe()`, and the
// shipped `manifest.runtime` row does not carry `authorWarn`, so nothing
// reaches the new branch today. This pin holds that reading honest in both
// directions — if the row ever opts in, this goes red and the reviewer should
// UPDATE THIS PIN (the branch above is what makes that flip safe), never
// remove the branch.
//
// Anti-vacuity: `authorWarnedProperties('manifest')` would also be empty if
// the ledger were unreadable, so the guard is that `shippedLedgerStatuses()`
// sees `live-elsewhere` at all — the ONE row carrying it lives in that very
// file, so seeing the status proves the file was read.
it('REAL LEDGER: the live-elsewhere row exists and does NOT warn yet (the fuse, unlit)', () => {
expect(shippedLedgerStatuses().has('live-elsewhere')).toBe(true);
expect(authorWarnedProperties('manifest').has('runtime')).toBe(false);
});

// ── COVERAGE (#14057): describe() answers for every status the ledgers ship.
//
// Patching one status is what let this card repeat #11384 — so the pin is
// derived from the shipped rows rather than from a list somebody has to
// remember to edit. A sixth status appearing in any ledger fails HERE, by
// name, instead of waiting for an author to trip the sentinel throw.
//
// `live` is the one member that must NOT get a branch, and the source header
// says why: an entry only reaches `describe()` once `shouldWarn()` has said
// yes, so "`live` can in principle arrive here too (an entry marked
// `authorWarn: true` on a `live` row would be a ledger authoring mistake, not
// a user error)". A mistake in our own shipped data is exactly what the
// sentinel is for, so `live` is asserted LOUD here rather than handled.
it('COVERAGE: every status the shipped ledgers carry is answered by describe() — or is loud by design', () => {
const statuses = [...shippedLedgerStatuses()].sort();
// Anti-vacuity: an unreadable ledger dir returns the empty set, which would
// pass every assertion below without measuring anything.
expect(statuses.length).toBeGreaterThanOrEqual(4);
expect(statuses).toContain('live-elsewhere');

const rulesByStatus = new Map<string, string>();
for (const status of statuses) {
const run = () =>
checkItemAgainstWarnMap('gadget', { name: 'g1', gizmo: 'x' }, "gadget 'g1'", oneEntry({ status, authorWarn: true }));
if (status === 'live') {
expect(run).toThrow(/live/);
continue;
}
const findings = run();
expect(findings, `status ${status} produced no finding`).toHaveLength(1);
expect(findings[0].rule, `status ${status} has no rule id of its own`).toMatch(/^liveness-[a-z-]+-property$/);
expect(findings[0].hint.length, `status ${status} has an empty hint`).toBeGreaterThan(0);
rulesByStatus.set(status, findings[0].rule);
}

// #11384's lesson as an assertion: verdicts imply different author actions,
// so no two of them may share a rule id.
expect(new Set(rulesByStatus.values()).size).toBe(rulesByStatus.size);
});

// ── SYNTHETIC: the unknown-status boundary — loud, never silently `dead` ──
it('SYNTHETIC: an unrecognised status fails LOUD, naming the status, instead of silently grading as dead', () => {
expect(() =>
Expand All@@ -1027,6 +1124,16 @@ describe('dead / experimental / planned verdicts are distinct, and unknown statu
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/quantum/);
// The message enumerates what describe() DOES know — #14057 is what happens
// when that list falls behind the branches, so pin them equal.
expect(() =>
checkItemAgainstWarnMap(
'gadget',
{ name: 'g1', gizmo: 'x' },
"gadget 'g1'",
oneEntry({ status: 'quantum', authorWarn: true }),
),
).toThrow(/'experimental' \| 'planned' \| 'dead' \| 'live-elsewhere'/);
});

it('SYNTHETIC: a `live` row mistakenly marked authorWarn also fails LOUD rather than being graded dead', () => {
Expand Down
Loading
Loading