fix(morfo): source-census fix-2 - la particion c1a/c1b deja de mentir (14 filas re-particionadas)

Hallazgo del dossier unificado: el clasificador c1a probaba la escritura manual solo sobre ficheros que REGISTRAN fuentes - se le escapaban el atributo de plantilla y el eidos que pinta sin registrar. 14 filas c1a eran c1b disfrazadas (field-langs x5, onion-menu x4, menu-dial x2, metrics x2, dialog aria-label), cada una con su sitio de escritura citado. Particion: c1a 31->17, c1b 41->55, c2 17; advisory 89 (la cadena completa: firmado 78 -> wave1 +12 fabricadas 90 -> hotfix f48acab37 89 -> fix-2 re-particiona sin mover el total). Gates intactos (orphans 20, deadPerPart 0), 20 excepciones sin tocar, negativo nuevo attr-en-plantilla probado. 10/10. Cierra el segundo bloqueo del flip advisory->gate; el primero (fila cero, composicion) espera la firma del autor sobre el dossier.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-background
dev 1 month ago
parent f48acab379
commit 168a0fba03

@ -36,15 +36,26 @@ import type { Morfo, MorfoCondition, MorfoPart, MorfoValueSource } from './types
* provider writes the attr by hand (inert declaration, correct DOM), 20
* naming-only (`consumerWins`) refs, and 1 per-part dead source.
*
* WHAT THIS TREE MEASURES TODAY (2026-08-27, re-run): 729 keys in 716
* registrations across the same 104 morfos (66 states / 602 props / 61 parts),
* 1 of them unreadable ⇒ 20 orphans, 0 per-part dead sources, advisory cube 78
* = 22 (c1a) / 36 (c1b) / 20 (c2). Both gated numbers fell below the founding
* WHAT THIS TREE MEASURES TODAY (2026-08-27 at `f48acab37`, after wave 1 of the
* ARIA harvest and FIX-2 of the classifier): 729 keys in 716 registrations
* across the same 104 morfos (66 states / 602 props / 61 parts), 1 of them
* unreadable ⇒ 20 orphans, 0 per-part dead sources, advisory cube 89
* = 17 (c1a) / 55 (c1b) / 17 (c2). Both gated numbers fell below the founding
* measure because wave C2 FIXED four defects at the source, not because the
* census looked away; the key count moved TWICE and in opposite directions —
* −4 for those retreats (725→721) and +8 when the dynamic-part blind spot
* closed (721→729, see `DYNAMIC_LEVEL`).
*
* THE CUBE HAS MOVED THREE TIMES SINCE SIGNING, and only one movement is a
* cure. Wave 1 (`338c49d67`) FABRICATED 12 rows (10 c1a + 2 c1b) and
* reclassified 3 (c2 → c1b) by giving refs to attrs no provider registers:
* 78 (22/36/20) → 90 (32/41/17). Its hotfix (`f48acab37`) retired ONE of those
* fabricated rows — `chatMessage.states.open`, a mirrored `stateRef` that
* clobbered the composed Popover's live `aria-expanded` — for 89 (31/41/17).
* Then FIX-2 (2026-08-27) moved 14 rows c1a → c1b because the PARTITION was
* lying, not because anything got fixed: the same 89 rows, cut where the
* evidence actually falls (see `handWriteOf`).
*
* THE THREE COMMITMENTS OF SIGNATURE C, so this file is read for what it is:
*
* 1. Generifying the builders is ITS OWN AXIS, not this one. `v.stateRef` /
@ -56,12 +67,17 @@ import type { Morfo, MorfoCondition, MorfoPart, MorfoValueSource } from './types
* 2. The flip to a BIDIRECTIONAL gate — also REQUIRING the source, the shape
* `SomaRuntimeDirSource<M>` already uses for `dir` — happens after the
* author adjudicates the harvest below, never before. Today the (c) cube
* is 78 rows: demanding a source would be a migration, not a gate. And
* three of those rows are stamped UNCERTAIN — a cube that still hides an
* opaque bag is not an adjudicable harvest yet.
* 3. The (c1b) wave — the 36 hand-written attrs that short-circuit the
* is 89 rows: demanding a source would be a migration, not a gate. And
* six of those rows are stamped UNCERTAIN — a cube that still hides an
* opaque bag is not an adjudicable harvest yet. (Three at signing; wave 1
* gave chronos three more refs and every one landed on the opaque bag.)
* 3. The (c1b) wave — the 55 hand-written attrs that short-circuit the
* «single pipeline of attrs» P0 phase C established — folds into the
* render bag under that law. It is a wave, not a census exception.
* render bag under that law. It is a wave, not a census exception. FIX-2
* sharpened what it is a wave OF: 14 of the 55 are written by an EIDOS
* template, not by the provider — 7 of them in a file that registers
* nothing at all (`field-langs-switcher.svelte` ×5, `dialog-close.svelte`,
* `metrics-delta.svelte`), where the bag governs no attr of that part.
*
* WHAT THIS FILE GATES (red the day it grows): new orphans, and new per-part
* dead sources. Both are dead code by construction, and both are measured
@ -408,7 +424,11 @@ interface Census {
readonly deadPerPart: CensusRow[];
/** ADVISORY: ref with no source and no hand-written attr — the attr is NOT in the DOM. */
readonly c1a: CensusRow[];
/** ADVISORY: ref with no source, but the provider writes the attr by hand. */
/**
* ADVISORY: ref with no source, but SOMEONE writes the attr by hand — the
* provider, or a template of the component that registers nothing. The row
* cites the site, because the two cases carry different debts.
*/
readonly c1b: CensusRow[];
/** ADVISORY: ref with no source, naming attrs only — the consumer may supply it. */
readonly c2: CensusRow[];
@ -435,6 +455,62 @@ function textOf(file: string): string {
return text;
}
/**
* The files that may write an attr BY HAND for a component: soma's providers
* and eidos' templates / wrappers, at the kebab the morfo declares. `.css` is
* excluded ON PURPOSE — a rule SELECTS the attr, it does not write it, and
* counting it would erase the sharpest finding of the (c1a) cube: `tooltip.css`
* and `time-range-picker.css` select attrs that never reach the DOM (dead
* rules). A component whose directory is not its kebab simply finds nothing,
* and its rows stay in (c1a) — the conservative side of the cut.
*/
const dirFiles = new Map<string, string[]>();
function componentDirFiles(kebab: string): string[] {
const cached = dirFiles.get(kebab);
if (cached) return cached;
const files: string[] = [];
const walk = (dir: string) => {
if (!existsSync(dir)) return;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) walk(full);
else if (/\.(svelte|ts)$/.test(entry.name) && !entry.name.includes('.test.'))
files.push(full);
}
};
for (const root of SOURCE_ROOTS) walk(join(root, kebab));
dirFiles.set(kebab, files);
return files;
}
/**
* The two shapes in which a component actually writes an attr by hand: a QUOTED
* KEY in a props bag (`'aria-level': depth.current + 1`) and a TEMPLATE
* ATTRIBUTE (`data-mode="segmented"`). The whitespace before the `=` is
* load-bearing: without it the probe also swallows CSS attribute selectors
* inside strings — measured, `chronos-provider.svelte.ts:580` queries
* `[data-minute="${minute}"]`, which is a READ, and the loose form moved that
* row into (c1b) as if someone had written it.
*/
function writesAttr(text: string, attr: string): boolean {
return (
text.includes(`'${attr}'`) ||
text.includes(`"${attr}"`) ||
new RegExp(`(^|\\s)${attr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}=`).test(text)
);
}
/** Where `attr` is written by hand, `path:line`, or undefined if nowhere. */
function handWriteOf(files: readonly string[], attr: string): string | undefined {
for (const file of files) {
const text = textOf(file);
if (!writesAttr(text, attr)) continue;
const line = text.split(/\r?\n/).findIndex((entry) => writesAttr(entry, attr)) + 1;
return `${relative(ROOT, file).replace(/\\/g, '/')}:${line}`;
}
return undefined;
}
/**
* Pure over its input, so the negative can mutate a registration in memory and
* watch the classifier bite.
@ -467,10 +543,26 @@ function classify(registrations: readonly Registration[]): Census {
if (!morfo) continue;
const uses = usesOf(morfo);
const compiled = compileMorfo(morfo);
// Every file that registers for this morfo, so an inert DECLARATION can be
// told from a DEAD attr: tree-view is the annotated archetype — its
// provider writes `'aria-level': depth.current + 1` by hand.
const providerText = [...new Set(regs.map((reg) => reg.file))].map(textOf).join('\n');
// Where an inert DECLARATION can be told from a DEAD attr. Tree-view is the
// annotated archetype — its provider writes `'aria-level': depth.current + 1`
// by hand — but the registering files are NOT the whole surface, and the
// narrow version of this line was the second blocker of the flip.
//
// FIX-2 (2026-08-27, unified dossier §2.2 + ledger L2): the probe used to
// read ONLY the files that REGISTER, and only for a quoted object key. It
// missed both real shapes of the hand: the attr written as a TEMPLATE
// attribute (`data-mode="segmented"`), and the file that WRITES BUT NEVER
// REGISTERS — an eidos painting a part it does not declare. 14 of the 31
// (c1a) rows were (c1b) in disguise (32 in the dossier, before `f48acab37`
// retired one), and (c1a) is the class that says «THE ATTR IS NOT IN THE
// DOM»: five of them are field-langs, whose switcher and lang-option are
// painted by `field-langs-switcher.svelte`, a file that registers nothing
// and names no morfo — while 13 rules of `field-langs.css` live off exactly
// those attrs. The debt is the same either way; what was false was the NAME
// on the rows, and the flip to a gate had its second blocker right here.
const handFiles = [
...new Set([...regs.map((reg) => reg.file), ...componentDirFiles(morfo.kebab)])
];
census.calls += regs.length;
const registered: Record<Bucket, Map<string, string>> = {
@ -510,23 +602,25 @@ function classify(registrations: readonly Registration[]): Census {
// from the LIVE registry before any map overwrites it.
if (bucket === 'parts') continue;
const id = `${name}.${bucket}.${key}`;
const row = {
id,
label:
`${id} ← ${sites.map((use) => `${use.part}.${use.attr}(${use.how})`).join(' ')}` +
(opaque
? ` ⚠ UNCERTAIN — ${opaque} opaque registration(s) on this morfo may already feed it`
: '')
};
const attrs = [...new Set(sites.map((use) => use.attr))];
if (sites.every((use) => use.consumerWins)) census.c2.push(row);
else if (
attrs.every(
(attr) => providerText.includes(`'${attr}'`) || providerText.includes(`"${attr}"`)
)
)
census.c1b.push(row);
else census.c1a.push(row);
const base = `${id} ← ${sites.map((use) => `${use.part}.${use.attr}(${use.how})`).join(' ')}`;
const uncertain = opaque
? ` ⚠ UNCERTAIN — ${opaque} opaque registration(s) on this morfo may already feed it`
: '';
if (sites.every((use) => use.consumerWins)) {
census.c2.push({ id, label: base + uncertain });
continue;
}
// EVERY attr of the row must be hand-written for the declaration to be
// merely inert: one attr left to the bag is one attr missing from the DOM.
const written = [...new Set(sites.map((use) => use.attr))].map((attr) =>
handWriteOf(handFiles, attr)
);
if (written.every((site) => site !== undefined))
census.c1b.push({
id,
label: `${base} — written by hand at ${[...new Set(written)].join(' ')}${uncertain}`
});
else census.c1a.push({ id, label: base + uncertain });
}
}
@ -670,7 +764,11 @@ describe('source census — every registered source has a reader, and every read
`\n[source-census] ${title} — ${rows.length}\n${rows.map((row) => ` ${row.label}`).join('\n')}`
);
show('(c1a) NO source, NO hand-written attr — THE ATTR IS NOT IN THE DOM', census.c1a);
show('(c1b) NO source, the provider writes the attr by hand — inert declaration', census.c1b);
show(
'(c1b) NO source, the attr is written BY HAND (provider, or a template that ' +
'registers nothing) — inert declaration',
census.c1b
);
show(
'(c2) NO source, naming attrs only (consumerWins) — the consumer may supply it',
census.c2
@ -682,7 +780,7 @@ describe('source census — every registered source has a reader, and every read
`gated: orphans=${census.orphans.length} deadPerPart=${census.deadPerPart.length} · ` +
`advisory (c)=${census.c1a.length + census.c1b.length + census.c2.length}`
);
// Lax pin, anti-empty only: the harvest is 22/36/20 on 2026-08-27 and
// Lax pin, anti-empty only: the harvest is 17/55/17 on 2026-08-27 and
// shrinking it is the POINT. A zero here means the cross-reference broke,
// not that the cube was cleared — that day the number becomes a gate.
expect(census.c1a.length + census.c1b.length + census.c2.length).toBeGreaterThan(0);
@ -741,4 +839,35 @@ describe('source census — every registered source has a reader, and every read
);
expect(census.orphans.map((row) => row.id)).not.toContain('chronosMorfo.props.redonly');
});
it('the NEGATIVE of FIX-2 — an attr written in a TEMPLATE is a hand, a SELECTOR is not', () => {
// The old probe, reconstructed in memory: a quoted object key, nothing else.
// It is what put 14 rows in (c1a) — «THE ATTR IS NOT IN THE DOM» — while the
// attr was in the DOM, painted by a template. Both halves matter: without the
// first the widening is unproven, without the second it is unbounded, and an
// unbounded probe would have quietly absolved `tooltip` and `chronos`.
const old = (text: string, attr: string) =>
text.includes(`'${attr}'`) || text.includes(`"${attr}"`);
const template = '<div data-field-langs-switcher data-mode="segmented" role="radiogroup">';
expect(old(template, 'data-mode'), 'the OLD probe misses the template attr').toBe(false);
expect(writesAttr(template, 'data-mode'), 'the WIDENED probe sees it').toBe(true);
// The counterfactual, taken from the shape that actually exists in the tree:
// a CSS attribute selector inside a query string is a READ of the attr, and
// crediting it as a write moved `chronos.props.minute` — a live (c1a) row —
// into (c1b) while the loose form was in place.
const query = 'root.querySelector(`[data-chronos-time-slot][data-minute="${minute}"]`)';
expect(old(query, 'data-minute'), 'the old probe was right about this one').toBe(false);
expect(writesAttr(query, 'data-minute'), 'a selector is not a hand-write').toBe(false);
// And the consequence the cube must carry: a (c1b) row NAMES its site, so the
// class can be adjudicated without re-deriving it. It holds at 55 rows and at 0.
expect(
census.c1b
.filter((row) => !row.label.includes(' — written by hand at '))
.map((row) => row.id),
'a (c1b) row that cites no site is an unfalsifiable claim'
).toEqual([]);
});
});

Loading…
Cancel
Save

Powered by TurnKey Linux.