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
4 changes: 4 additions & 0 deletions .changeset/9126-pack-dynamic-head-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Report-only, `scripts/`-only: `check-i18n-dead-keys.mjs`'s pack half now prints every dynamic template head it applied, how many `en` leaves fall under each, and how many of those that head alone keeps out of the candidate list. No verdict, tier or candidate changes — the sibling designer corpus already reports the same reading, and this closes the asymmetry without adopting its threshold (objectui#9126).
143 changes: 143 additions & 0 deletions scripts/__tests__/check-i18n-dead-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1806,3 +1806,146 @@ describe('the text sweep does not descend into `.objectui-tmp` (objectui#9201)',
expect(String(thrown?.stderr ?? '')).toContain('No such file or directory');
});
});

/**
* objectui#9126 — the pack half applies every collected dynamic head unfiltered
* and, until this leg, said NOTHING about which heads those were or how far
* each reached. The whole deliverable is that reading: `sweep()` now returns
* one row per applied head, and the CLI prints them.
*
* ⛔ The card's fences are what these tests are for, and the sharpest one is
* NEGATIVE: reporting must not become filtering. Every test below that measures
* a row is paired with one measuring that the candidate list did not move —
* a row is a description of a subtraction that already happened, never a new
* one. The threshold question (`MIN_HEAD_SEGMENTS`, which the DESIGNER half
* applies to its own corpus) is deliberately not answered here; it is a
* maintainer's call and no test here may smuggle one in.
*/
describe('the applied dynamic heads are REPORTED, not filtered (objectui#9126)', () => {
/** A pack with one family under a head that names a top-level namespace and
* nothing else, and one under a head that names a segment of its own. */
const HEAD_REPORT_EN = `const en = {
wideNs: { alpha: 'A', beta: 'B', gamma: 'C' },
narrowNs: { group: { one: 'One', two: 'Two' } },
} as const;
export default en;
`;

/** Builds a key under each head from a runtime value, and ALSO asks for one
* leaf under the wide head by its literal name — so `leavesUnder` and
* `heldLive` are different numbers for that row and the difference has a
* cause a reader can point at. */
const HEAD_REPORT_CONSUMER = `
import { useObjectTranslation } from '${I18N_PKG}';
export function Widget({ id }: { id: string }) {
const { t } = useObjectTranslation();
return [t('wideNs.alpha'), t(\`wideNs.\${id}\`), t(\`narrowNs.group.\${id}\`)];
}
`;

function headReportRoot() {
return repoWith({
'packages/i18n/src/locales/en.ts': HEAD_REPORT_EN,
'packages/x/src/Widget.tsx': HEAD_REPORT_CONSUMER,
});
}

const rowFor = (root: string, head: string) => sweep(root).appliedHeads.find((row) => row.head === head);

it('reports every head it applied, including one naming only a top-level namespace', () => {
const heads = sweep(headReportRoot()).appliedHeads.map((row) => row.head);
expect(heads.sort()).toEqual(['narrowNs.group.', 'wideNs.']);
});

it('counts the head’s own segments, so a namespace-wide head is visible AS one', () => {
const root = headReportRoot();
// The reading the card asks for: `wideNs.` names a top-level namespace and
// no more. Reported as `1` — and still applied, which the next test pins.
expect(rowFor(root, 'wideNs.')?.ownSegments).toBe(1);
expect(rowFor(root, 'narrowNs.group.')?.ownSegments).toBe(2);
});

it('⛔ NEGATIVE CONTROL: the namespace-wide head is still APPLIED — no key under it is a candidate', () => {
// The fence, as a test. If a future edit turns `ownSegments` into a filter,
// the three `wideNs.*` leaves become candidates and this goes red.
const { confirmed, needsReview, candidateCount } = sweep(headReportRoot());
expect(candidateCount).toBe(0);
expect(confirmed).toEqual([]);
expect(needsReview).toEqual([]);
});

it('counts every leaf UNDER the head, whether or not another leg already holds it', () => {
const root = headReportRoot();
expect(rowFor(root, 'wideNs.')?.leavesUnder).toBe(3);
expect(rowFor(root, 'narrowNs.group.')?.leavesUnder).toBe(2);
});

it('counts as HELD only the leaves no other leg keeps live', () => {
// `wideNs.alpha` has a literal call site, so the head is not what takes it
// out of the candidate list. Reporting it as held would overstate the
// head's reach — the same overstatement in the opposite direction to the
// silence this leg closes.
const root = headReportRoot();
expect(rowFor(root, 'wideNs.')?.heldLive).toBe(2);
expect(rowFor(root, 'narrowNs.group.')?.heldLive).toBe(2);
});

it('marks where each head came from', () => {
const root = headReportRoot();
expect(rowFor(root, 'wideNs.')?.via).toBe('direct');
expect(rowFor(root, 'narrowNs.group.')?.via).toBe('direct');
});

describe('measured on THIS repository', () => {
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const result = sweep(repoRoot);

it('reports one row per head the sweep actually applied — no head goes unreported', () => {
const gate = analyze(repoRoot);
const indirect = collectIndirectTemplateHeads(repoRoot, collectEnKeys(repoRoot));
const applied = new Set([...gate.dynamicHeads, ...indirect.heads.keys()]);
expect(applied.size, 'no heads at all — the whole cross-check would be vacuous').toBeGreaterThan(0);
expect(result.appliedHeads.map((row) => row.head).sort()).toEqual([...applied].sort());
});

it('every reported row describes a REAL subtraction — no key it counts is still a candidate', () => {
// The report must be a reading of the candidate list, not a parallel
// claim about it. A row whose keys were still candidates would be worse
// than the silence it replaces.
const candidates = new Set([...result.confirmed, ...result.needsReview.map((entry) => entry.key)]);
const leaves = [...collectEnKeys(repoRoot).leaves];
for (const row of result.appliedHeads) {
const under = leaves.filter((key) => key.startsWith(row.head));
expect(under.length, `${row.head} holds no leaves — its row would be vacuous`).toBe(row.leavesUnder);
expect(under.filter((key) => candidates.has(key)), `${row.head} reaches a key still in a tier`).toEqual([]);
}
});

it('⛔ the reading does not move the candidate list — heads stay unfiltered at EVERY depth', () => {
// The card's lit control, at repository scale. The pack half applies each
// collected head whatever its depth; the report says so and changes
// nothing. `heldLive` summing to the keys held out of the tiers is the
// arithmetic a reviewer can redo from the printed table.
const leaves = [...collectEnKeys(repoRoot).leaves];
const heldByAHead = leaves.filter((key) => result.appliedHeads.some((row) => key.startsWith(row.head)));
const candidates = new Set([...result.confirmed, ...result.needsReview.map((entry) => entry.key)]);
expect(heldByAHead.some((key) => candidates.has(key))).toBe(false);
expect(result.candidateCount).toBe(result.confirmed.length + result.needsReview.length);
});

it('⛔ does NOT adopt the designer half’s threshold for the packs', () => {
// objectui#9126 fences the threshold off as a maintainer's call. A head
// with no segment of its own is REPORTED here and REFUSED on the designer
// corpus; if the packs ever start refusing one too, that is a deliberate
// decision and this test is where it gets re-argued.
const rootOnly = result.appliedHeads.filter((row) => row.ownSegments < 2);
expect(rootOnly.length, 'no namespace-wide head on the tree — the pin has nothing to hold').toBeGreaterThan(0);
const leaves = [...collectEnKeys(repoRoot).leaves];
const candidates = new Set([...result.confirmed, ...result.needsReview.map((entry) => entry.key)]);
for (const row of rootOnly) {
const under = leaves.filter((key) => key.startsWith(row.head));
expect(under.filter((key) => candidates.has(key)), `${row.head} was filtered, not merely reported`).toEqual([]);
}
});
});
});
95 changes: 95 additions & 0 deletions scripts/check-i18n-dead-keys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1590,6 +1590,9 @@ export function collectIndirectTemplateHeads(root, packKeys = collectEnKeys(root
* needsReview: Array<{ key: string, hits: string[] }>,
* byNamespace: Map<string, { confirmed: string[], needsReview: string[] }>,
* indirectTemplateHeads: ReturnType<typeof collectIndirectTemplateHeads>,
* appliedHeads: Array<{
* head: string, ownSegments: number, leavesUnder: number, heldLive: number, via: string,
* }>,
* }}
*/
export function sweep(root) {
Expand Down Expand Up @@ -1619,6 +1622,46 @@ export function sweep(root) {
})
.sort();

// objectui#9126 — what the head leg above ACTUALLY applied, as a reading.
//
// REPORT ONLY, and the ORDER of these two statements is the whole guarantee:
// `candidates` is already computed, from `heads` unfiltered, exactly as
// before. Nothing below feeds back into it — this block only measures a
// subtraction that was already made, so the candidate list is byte-identical
// with and without it. That is deliberate and fenced: the sibling corpus
// below DOES refuse a head with no segment of its own (`MIN_HEAD_SEGMENTS`),
// and whether the packs should adopt a threshold of their own is a
// maintainer's call this script does not make. What it can do without any
// threshold decision is stop being SILENT about it — a reader who sees only
// the candidate count has no way to learn how many keys were never offered,
// nor that a handful of heads account for nearly all of them.
//
// `ownSegments` is that reading, not a filter: the head minus its trailing
// dot, counted in segments, so a head naming ONLY a top-level namespace
// (`someNamespace.` -> 1) is visible as such in the report while still being
// applied in full.
//
// `heldLive` is the designer half's `headHeldCounts` definition, mirrored: of
// the leaves under this head, how many NO OTHER leg here already holds — i.e.
// exactly the keys this head alone keeps out of `candidates`. Heads do not
// nest today, but if two ever did, a leaf under both counts in both rows;
// the sibling counts the same way, and a per-head row that quietly dropped
// shared keys would under-report the same way this block exists to fix.
const appliedHeads = heads
.map((head) => {
const under = [...leaves].filter((key) => key.startsWith(head));
return {
head,
ownSegments: head.replace(/\.$/, '').split('.').length,
leavesUnder: under.length,
heldLive: under.filter(
(key) => !referencedKeys.has(key) && !branchPrefixes.some((prefix) => key.startsWith(prefix)),
).length,
via: dynamicHeads.has(head) ? (indirect.heads.has(head) ? 'direct+indirect' : 'direct') : 'indirect',
};
})
.sort((a, b) => b.leavesUnder - a.leavesUnder || a.head.localeCompare(b.head));

const footprints = textFootprint(root, candidates);
const confirmed = candidates.filter((key) => footprints.get(key).length === 0);
const needsReview = candidates
Expand All @@ -1645,6 +1688,7 @@ export function sweep(root) {
needsReview,
byNamespace,
indirectTemplateHeads: indirect,
appliedHeads,
};
}

Expand Down Expand Up @@ -2118,6 +2162,9 @@ if (invokedDirectly) {
candidateCount: result.candidateCount,
confirmed: result.confirmed,
needsReview: result.needsReview,
// objectui#9126 — every head the pack half applied, and what each
// holds. Reporting only: `candidateCount` above is unchanged by it.
appliedDynamicHeads: result.appliedHeads,
indirectTemplateHeads: {
counters: result.indirectTemplateHeads.counters,
heads: Object.fromEntries(
Expand Down Expand Up @@ -2219,6 +2266,54 @@ if (invokedDirectly) {
}
}

// ── the dynamic heads this half APPLIED (objectui#9126) ─────────────────
// Every head the candidate list above was computed against, printed
// because until now it was not: this half applies each collected head
// unfiltered, so a key is held live by merely starting with one, and the
// report said nothing at all about which heads those were or how much each
// reached. The counts read as a candidate list that is simply short.
//
// ⛔ This block changes no verdict. The sibling corpus below REFUSES a head
// with no segment of its own; whether the packs want a threshold of their
// own is a maintainer's call and is deliberately not made here. Reporting
// needs no such decision — it only turns the silence into a reading.
{
const rows = result.appliedHeads;
const totalUnder = rows.reduce((sum, row) => sum + row.leavesUnder, 0);
const totalHeld = rows.reduce((sum, row) => sum + row.heldLive, 0);
const rootOnly = rows.filter((row) => row.ownSegments < 2);
const rootOnlyUnder = rootOnly.reduce((sum, row) => sum + row.leavesUnder, 0);
const rootOnlyHeld = rootOnly.reduce((sum, row) => sum + row.heldLive, 0);
console.log(
`\n${'='.repeat(78)}\ndynamic template heads APPLIED to the pack corpus — all ${rows.length}, ` +
'none filtered' +
`\n\n"under" is every en leaf sharing the head. "held" is how many of those NO other leg here ` +
`already keeps live — i.e. exactly the keys this head alone takes out of the candidate list, ` +
`so ${result.candidateCount} candidate(s) is a reading of the pack MINUS ${totalHeld} key(s) ` +
`across ${rows.length} head(s) (${totalUnder} leaves fall under a head in total; the ` +
'difference is leaves a literal call site holds anyway). "own" is how many segments the head ' +
'names beyond nothing: 1 means it names a top-level namespace and no more. Each head comes ' +
'from a real call site building a key from a runtime value — the row measures its REACH, and ' +
'says nothing about whether the head is right:',
);
console.log(`\n ${'head'.padEnd(40)} ${'own'.padStart(3)} ${'under'.padStart(5)} ${'held'.padStart(5)} via`);
for (const row of rows) {
console.log(
` ${row.head.padEnd(40)} ${String(row.ownSegments).padStart(3)} ` +
`${String(row.leavesUnder).padStart(5)} ${String(row.heldLive).padStart(5)} ${row.via}`,
);
}
if (rootOnly.length > 0) {
console.log(
`\n ⚠️ ${rootOnly.length} of those head(s) name a top-level namespace and nothing else ` +
`(${rootOnly.map((row) => row.head).join(', ')}), together reaching ${rootOnlyUnder} leaf/leaves ` +
`and holding ${rootOnlyHeld} key(s) out of the candidate list on their own. That is the class ` +
'the designer half below refuses outright and this half applies in full. Both are reported; ' +
'only one is a decision, and it is not this one.',
);
}
}

// ── the one-hop indirect template leg (objectui#8754) ───────────────────
// Printed for the same reason the property-read rows above are: the leg
// subtracts keys from the candidate set, so the tiers are shorter than they
Expand Down
Loading