From e812d3ffd3318836ba2226896bc21d3c729933ce Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 25 Aug 2026 23:45:33 +0200 Subject: [PATCH] feat(theming): el censo distingue PUENTE y CANAL - 318 privados dejan de ser un bulto Primera pieza de la firma "clasificacion de los 318" (F1). Dos clases nuevas de knob, MEDIDAS contra el emisor y no adivinadas por nombre (la ceguera del chooser, escarmentada en este mismo eje): - bridge (132): el knob lee el privado que el forward THM-2 ESCRIBE - verificado contra la CSS generada real (la linea `--_{c}-palette-{slot}: var(--palette-{slot}, ...)` de renderRecipePaletteForward), no contra la forma del nombre: un `--_c-palette-shadow` que nadie forwardea se queda en residuo. Alcanzable DOS veces (capa --palette-* + tonos publicos); un publico encima dejaria a un tema PINearlo y matar el color= por instancia (los SS5 de card/tags-input/avatar ya lo decian). - channel (36): el privado que NADIE declara - ni la receta ni el generador - porque soma o el envoltorio lo escriben POR INSTANCIA (29 nombres, cada uno con su escritor real verificado). Un tema no debe alcanzarlo. - Precedencia puente > canal > residuo; `every`, no `some`; un privado GENERADO nunca es canal (los 4 knobs de gradient finish quedan en residuo con su razon medida - adjudicar una clase "finish" seria inventarla sin firma). Aritmetica: 318 = 132 puente + 36 canal + 150 RESIDUO (la adjudicacion por clave es de F2). Ambas clases salen del denominador como system/structural: reach 69% -> 71% (3074/4312). Cero knobs de otras clases movidos (diff programatico de las 162 filas). Mutacion del instrumento sobre copia: vaciar los forwards -> bridge 0; declarar-todo -> channel 0 - las clases las manda la medida. Nuevo `--residue ` vuelca el residuo con la cadena de fuentes de cada privado (el material de F2). Casos firmados, contrastados: card 8 = 4 puente + 4 residuo (su SS5 exacto) - tags-input 4/4 puente - toolbar 2 residuo ("esta barra no tiene paleta") - avatar 6 residuo (su canal custom esta un nivel POR DEBAJO del knob) - el indicador de tabs NO esta en los 318 (es clase global y ya vive en el ledger de deuda). Suelo 5/5 verde (el ledger literal|global no se toca; el ratchet grueso sube en F3), audit 162 PASS identico, tsc 0 propios, prettier sin desajustes nuevos. Deuda de prosa (los "318" en cabeceras/docs y el suelo en 69) queda para F3, nombrada. Co-Authored-By: Claude Fable 5 --- scripts/theming-census.ts | 324 +++++++++++++++++++++++++++++++++++--- 1 file changed, 305 insertions(+), 19 deletions(-) diff --git a/scripts/theming-census.ts b/scripts/theming-census.ts index 6e1f6eef8..0484a6158 100644 --- a/scripts/theming-census.ts +++ b/scripts/theming-census.ts @@ -7,6 +7,8 @@ * node --import tsx/esm scripts/theming-census.ts --only tabs * node --import tsx/esm scripts/theming-census.ts --report # docs/audit/theming/ * node --import tsx/esm scripts/theming-census.ts --names # naming grammar (D-TH.6) + * node --import tsx/esm scripts/theming-census.ts --residue [out.tsv] + * # privates still to adjudicate * * WHY. The recipe contract already says every recipe declares its knobs in * `lib/recipes/base.ts` (recipe-contract §1, theming §6), and the R-4.x guards @@ -38,11 +40,19 @@ * structural — EVERY knob of a component whose 0 % is its NATURE and not its * debt, because it has no contract to write: the signed list is * `STRUCTURAL_COMPONENTS` below. - * Reach = public / (public + private + global + literal). `system`, `exception` - * and `structural` are reported but excluded from the ratio on purpose: the - * first is themeable at the system level by design (recipe-contract §2), the - * second is a deviation the canon already accepted in writing (§3), and the - * third has nothing a theme could name in the first place. + * bridge — `var(--_{c}-palette-{slot})`: the private the THM-2 palette + * forward WRITES. The recipe reads the mechanism of the + * per-instance palette, not a name of its own. + * channel — `var(--_{c}-x, fallback)` where NOBODY declares `--_{c}-x`: + * soma or the wrapper writes it per INSTANCE (a %, a measured + * rect, the size a prop asks for). A theme must not reach it. + * Reach = public / (public + private + global + literal). `system`, + * `exception`, `structural`, `bridge` and `channel` are reported but excluded + * from the ratio on purpose: the first is themeable at the system level by + * design (recipe-contract §2), the second is a deviation the canon already + * accepted in writing (§3), the third has nothing a theme could name in the + * first place, and the last two are the two shapes of private the doctrine of + * this axis already adjudicated as NOT debt (see `PALETTE_BRIDGE` below). * * REPORT. `--report` writes the audit under `docs/audit/theming/`: a root * `README.md` with the whole-catalogue view, and one `{c}.md` per component @@ -108,7 +118,9 @@ export type KnobClass = | 'global' | 'literal' | 'exception' - | 'structural'; + | 'structural' + | 'bridge' + | 'channel'; export interface Knob { file: string; @@ -142,7 +154,11 @@ export interface CensusRow { exception: number; /** Knobs of a component whose 0 % is its nature — `STRUCTURAL_COMPONENTS`. */ structural: number; - /** public / (public + private + global + literal) — the other three out. */ + /** Knobs reading the private the THM-2 palette forward writes — `PALETTE_BRIDGE`. */ + bridge: number; + /** Knobs reading a private nobody declares: soma / the wrapper writes it per instance. */ + channel: number; + /** public / (public + private + global + literal) — the other five out. */ reach: number; contractKeys: number; hasSize: boolean; @@ -256,6 +272,70 @@ const STRUCTURAL_COMPONENTS = new Map([ ] ]); +/** + * The two shapes of private this axis' doctrine already adjudicated as NOT + * debt — read off the EMITTER'S OUTPUT, never guessed from the name. + * + * The anchor is `src/uix/eidos/generated/base.css`: `generated-css.test.ts` + * keeps it byte-identical to `renderStaticCss()`, and `recipe-css-contract` + * already reads it to assert that every palette recipe emits its forward. So + * the question asked here is «what does the generator WRITE», not «what does + * this name look like» — and that difference is the whole point: adjudicating + * by name is the blindness this same axis was burned by. + * + * `bridge` — the THM-2 forward (`renderRecipePaletteForward` + + * `isPaletteSlotToken`, render-css.ts) writes `--_{c}-palette-{slot}` under + * the presence guard `[data-{c}]:where([data-color], [data-color-custom])`, + * with the component's OWN host default as the fallback. A recipe reading it + * consumes the MECHANISM of the per-instance palette, and it is reachable + * TWICE: through the shared `--palette-*` layer and through the component's + * own tone tokens (`--card-neutral-track`) — both public. A public on top + * would let a theme PIN it and kill the `color=` of every instance in + * silence; that is written, measured, in the §5 verdicts of `card`, + * `tags-input` and `avatar`. + * The test is the forward LINE, not a name shaped like one: only + * `--_{c}-palette-{slot}: var(--palette-{slot}, …)` counts, so a + * `--_c-palette-shadow` nobody forwards stays residue (same discipline as + * `isPaletteSlotToken`, which matches the slot const and not a loose regex). + * + * `channel` — the value channel: a private the recipe READS and NOBODY + * declares, neither the component's CSS nor the generator. Soma or the + * wrapper writes it per INSTANCE (`--_progress-value-pct`, + * `--_dialog-content-width-override`, `--_chronos-event-accent`). A theme + * must not reach it: pinning it breaks the behaviour, which is why `tabs` + * refused its §4.1 proposal in writing. Measured 2026-08-25: all 29 names in + * this class have a writer in a wrapper (`*.svelte`) or in soma. + * + * Both leave the ratio's DENOMINATOR, exactly like `system` and `structural`; + * their knobs are still counted and still listed. A private that is NEITHER + * stays `private`: the RESIDUE this signature deliberately leaves to be + * adjudicated key by key (`--residue`). + */ +const GENERATED_CSS = readFileSync(resolve('src/uix/eidos/generated/base.css'), 'utf8').replace( + /\r\n/g, + '\n' +); + +/** `--_{c}-palette-{slot}` AS THE FORWARD EMITS IT — the bridge, verbatim. */ +const PALETTE_BRIDGE = new Set( + [ + ...GENERATED_CSS.matchAll( + /(--_[a-z0-9-]+-palette-([a-z-]+))\s*:\s*var\(\s*--palette-\2\s*[,)]/g + ) + ].map((m) => m[1]) +); + +/** + * Every private the GENERATOR declares — recipe keys, palette forwards and the + * gradient-finish ramp (`--_{c}-fill-finish`). A private in here is written by + * the build, so it is NOT a runtime channel however undeclared the CSS leaves + * it: without this test the four gradient-finish knobs read as «written by + * soma», which is the wrong reason for the right answer. + */ +const GENERATED_PRIVATES = new Set( + [...GENERATED_CSS.matchAll(/(?<=^\s*|;\s*)(--_[a-z0-9-]+)\s*:/gm)].map((m) => m[1]) +); + function classify( value: string, pubNeedle: string, @@ -343,6 +423,8 @@ export function scanComponent(dir: string): Scan | null { literal: 0, exception: 0, structural: 0, + bridge: 0, + channel: 0, reach: 0, contractKeys: contractKeysFor(dir), hasSize: false @@ -477,8 +559,32 @@ export function scanComponent(dir: string): Scan | null { k.klass = 'public'; } + // ── Third pass: the palette BRIDGE and the value CHANNEL ── + // The two shapes of private the doctrine adjudicated as not-debt, decided by + // what the GENERATOR writes (see `PALETTE_BRIDGE`). `every`, not `some`: a + // knob mixing a bridge with anything else stays residue, because the + // anything else is precisely what still needs adjudicating. A FOREIGN + // private (another component's `--_x-…`) can be a bridge — the forward is + // the same mechanism wherever it is emitted — but never a channel: this + // component's CSS is not where it would be declared. + const own = '--_' + dir + '-'; + for (const k of knobs) { + if (k.klass !== 'private') continue; + const refs = [...k.value.matchAll(/var\(\s*(--_[a-z0-9-]+)/g)].map((m) => m[1]); + if (refs.length === 0) continue; + const klass: KnobClass | null = refs.every((r) => PALETTE_BRIDGE.has(r)) + ? 'bridge' + : refs.every((r) => r.startsWith(own) && !declBySource.has(r) && !GENERATED_PRIVATES.has(r)) + ? 'channel' + : null; + if (klass === null) continue; + row.private--; + row[klass]++; + k.klass = klass; + } + const themeable = row.public + row.private + row.global + row.literal; - row.knobs = themeable + row.system + row.structural; + row.knobs = themeable + row.system + row.structural + row.bridge + row.channel; row.reach = themeable === 0 ? 1 : row.public / themeable; const declared = new Set(privates.map((p) => p.name)); const privatesFromContract = [...usedPrivates] @@ -671,6 +777,106 @@ function reportDebt(only?: string, asJson = false) { console.log(' the ledger matches the CSS exactly.'); } +// ─── `--residue`: the privates that are NEITHER bridge NOR channel ─────────── + +/** + * What the two mechanical signatures deliberately did NOT decide. + * + * The bridge and the channel are adjudicated by measurement; everything else + * that reads a private without deriving from a public is a per-KEY question — + * a switch whose branches are the identity of a variant (`toolbar`), a + * conmutador the TSC feeds (`avatar`), a private that reads a raw global in + * one branch (`proof-of-human`). Each row carries the knob AND where every + * private it reads is declared, with the class of each declaration, because + * that is the whole material the adjudication needs. + * + * It is a REPORT, not a gate: the per-key ratchet for this class is the piece + * this file does not yet have (the hole named in the header of + * `theming-census-debt.ts` and in recipe-contract §4). + */ +export interface ResidueRow { + component: string; + file: string; + line: number; + selector: string; + prop: string; + value: string; + /** One entry per private read: `--_x → file:line class | file:line class`. */ + sources: string[]; +} + +export function residue(only?: string): ResidueRow[] { + const out: ResidueRow[] = []; + for (const dir of [...readdirSync(ROOT)].sort()) { + if (only && dir !== only) continue; + const scan = scanComponent(dir); + if (!scan) continue; + for (const k of scan.knobs) { + if (k.klass !== 'private') continue; + const refs = [...new Set([...k.value.matchAll(/var\(\s*(--_[a-z0-9-]+)/g)].map((m) => m[1]))]; + out.push({ + component: dir, + file: k.file, + line: k.line, + selector: k.selector, + prop: k.prop, + value: k.value, + sources: refs.map((r) => { + const decls = scan.privates.filter((p) => p.name === r); + if (decls.length > 0) + return r + ' → ' + decls.map((d) => `${d.file}:${d.line} ${d.source}`).join(' | '); + return r + ' → ' + (GENERATED_PRIVATES.has(r) ? 'generated/base.css' : 'undeclared'); + }) + }); + } + } + return out; +} + +function reportResidue(only?: string, out?: string, asJson = false) { + const rows = residue(only); + if (asJson) { + process.stdout.write(JSON.stringify(rows, null, '\t') + '\n'); + return; + } + const per = new Map(); + for (const r of rows) per.set(r.component, (per.get(r.component) ?? 0) + 1); + const tsv = + ['component', 'file:line', 'selector', 'property', 'value', 'sources'].join('\t') + + '\n' + + rows + .map((r) => + [ + r.component, + `${r.file}:${r.line}`, + r.selector, + r.prop, + r.value, + r.sources.join(' ;; ') + ].join('\t') + ) + .join('\n') + + '\n'; + console.log( + `theming-census --residue — ${rows.length} private knob(s) in ${per.size} component(s): ` + + 'neither the palette bridge nor a value channel, so each one is a decision.' + ); + console.log( + ' worst: ' + + [...per.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, 12) + .map(([c, n]) => `${c}:${n}`) + .join(' ') + ); + if (out) { + writeFileSync(resolve(out), tsv, 'utf8'); + console.log(` wrote ${out}`); + return; + } + process.stdout.write('\n' + tsv); +} + // ─── The proposal — derived from doctrine, never invented ──────────────────── /** @@ -1007,8 +1213,18 @@ function proposalSection(scan: Scan): string { const c = scan.row.component; // `structural` out with `public` and `system`: proposing a token for a knob // whose §5 verdict says there is no contract to write would be inventing debt. + // `bridge` and `channel` out for the same reason, one floor down: a public + // over the palette forward or over a value channel is a token a theme could + // PIN, and pinning it kills the per-instance `color=` / the behaviour. The + // sheets of `card`, `tags-input` and `tabs` had to refuse exactly that + // proposal by hand — the classifier must not keep making it. const targets = scan.knobs.filter( - (k) => k.klass !== 'public' && k.klass !== 'system' && k.klass !== 'structural' + (k) => + k.klass !== 'public' && + k.klass !== 'system' && + k.klass !== 'structural' && + k.klass !== 'bridge' && + k.klass !== 'channel' ); if (targets.length === 0) return STRUCTURAL_COMPONENTS.has(c) @@ -1256,7 +1472,7 @@ function sheet(scan: Scan, today: string, verdict: string): string { ? 'ESTRUCTURAL: sin denominador que medir, y eso es la respuesta' : `${row.public} de ${themeable} knobs por token público` } -- **Knobs de apariencia**: ${row.knobs} — público ${row.public} · privado ${row.private} · global ${row.global} · literal ${row.literal} · sistema ${row.system} · excepción ${row.exception} · estructural ${row.structural} _(los tres últimos, fuera del ratio)_ +- **Knobs de apariencia**: ${row.knobs} — público ${row.public} · privado ${row.private} · global ${row.global} · literal ${row.literal} · sistema ${row.system} · excepción ${row.exception} · estructural ${row.structural} · puente ${row.bridge} · canal ${row.channel} _(los cinco últimos, fuera del ratio)_ - **Contrato hoy** (\`lib/recipes/base.ts\`): ${contractLine} - **Eje \`size\`**: ${row.hasSize ? 'sí' : 'no'} · **ficheros**: ${list(scan.files)} @@ -1266,7 +1482,11 @@ function sheet(scan: Scan, today: string, verdict: string): string { ${knobTable(by('global'), c)} ### 1.2 A través de un privado (${by('private').length}) - +${ + row.bridge + row.channel > 0 + ? '\nSólo el **residuo**: el puente de paleta (§2-ter) y el canal de valor\n(§2-quater) salen aparte, porque no son deuda ni tienen nombre que acuñar.\n' + : '' +} ${knobTable(by('private'), c)} ### 1.3 Literales (${by('literal').length}) @@ -1295,6 +1515,33 @@ lean, **no** para acuñarlos. ${knobTable(by('structural'), c)}` : '' + }${ + row.bridge > 0 + ? ` +## 2-ter. Puente de paleta THM-2 (${row.bridge}) — fuera del ratio + +La receta lee \`var(--_${c}-palette-{slot})\`, que **lo escribe el forward** de la +cascada de paleta bajo \`[data-${c}]:where([data-color], [data-color-custom])\` +(\`renderRecipePaletteForward\`, firma B′). Es el MECANISMO de la paleta por +instancia, y se alcanza **dos veces**: por la capa compartida \`--palette-*\` y +por los tonos públicos del propio componente. **No se acuña**: un público +encima dejaría que un tema lo fijara y matara en silencio el \`color=\` de cada +instancia (veredictos §5 de \`card\`, \`tags-input\`, \`avatar\`). + +${knobTable(by('bridge'), c)}` + : '' + }${ + row.channel > 0 + ? ` +## 2-quater. Canal de valor (${row.channel}) — fuera del ratio + +La receta lee un privado que **nadie declara** — ni su CSS ni el generador: lo +escribe soma o el envoltorio **por instancia** (un %, un rect medido, la talla +que pide una prop). Un tema no debe alcanzarlo: fijarlo rompe el +comportamiento, y por eso \`tabs\` rechazó por escrito esa misma propuesta. + +${knobTable(by('channel'), c)}` + : '' } ## 3. Privados de la receta — ¿de dónde sale su valor? @@ -1331,6 +1578,8 @@ function rootReadme(scans: Scan[], empties: EmptyScan[], today: string, verdict: const noContract = rows .filter((r) => r.contractKeys === 0 && !STRUCTURAL_COMPONENTS.has(r.component)) .map((r) => r.component); + const bridgeRows = rows.filter((r) => r.bridge > 0).sort((a, b) => b.bridge - a.bridge); + const channelRows = rows.filter((r) => r.channel > 0).sort((a, b) => b.channel - a.channel); const worst = [...rows] .map((r) => ({ c: r.component, out: r.private + r.global + r.literal, r })) .sort((a, b) => b.out - a.out) @@ -1349,7 +1598,7 @@ function rootReadme(scans: Scan[], empties: EmptyScan[], today: string, verdict: - **Medido**: ${today} · **${rows.length} recetas** con CSS + **${empties.length} componentes sin receta** = ${rows.length + empties.length} fichas, el árbol entero de \`eidos/components/\` - **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**? - **Alcance global**: **${pct(sum('public'), themeable)}** — ${sum('public')} de ${themeable} knobs pasan por un token público del componente -- **Reparto**: público ${sum('public')} · privado ${sum('private')} · global ${sum('global')} · literal ${sum('literal')} · sistema transversal ${sum('system')} · excepción firmada ${sum('exception')} · estructural ${sum('structural')} _(los tres últimos, fuera del ratio)_ +- **Reparto**: público ${sum('public')} · privado ${sum('private')} · global ${sum('global')} · literal ${sum('literal')} · sistema transversal ${sum('system')} · excepción firmada ${sum('exception')} · estructural ${sum('structural')} · puente de paleta ${sum('bridge')} · canal de valor ${sum('channel')} _(los cinco últimos, fuera del ratio)_ - **Sin token público propio**: ${noContract.length} · **alcance < 20 %**: ${rows.filter((r) => r.reach < 0.2).length} · **alcance 100 %**: ${rows.filter((r) => r.reach === 1 && r.public > 0).length} · **con eje \`size\`**: ${rows.filter((r) => r.hasSize).length} · **estructurales**: ${structural.length} ## Cómo se lee @@ -1363,11 +1612,17 @@ function rootReadme(scans: Scan[], empties: EmptyScan[], today: string, verdict: | \`system\` | sistemas transversales que la receta CONSUME por contrato (capa de estado, anillo de foco, planos de depth, motion, bandas z, opacidad, shape, floating-gap) | sí, **a nivel de sistema**, por diseño (recipe-contract §2) — fuera del ratio | | \`exception\` | un literal con su anotación \`/* literal: */\` en la propia declaración | no hace falta: es la válvula de recipe-contract §3, una desviación ya firmada — fuera del ratio | | \`structural\` | **todos** los knobs de un componente cuyo 0 % es su NATURALEZA y no su deuda: no hay contrato que escribir (lista firmada abajo) | no hace falta: no hay superficie que un tema pueda nombrar — fuera del ratio, y su fila lee \`strct\`, no \`0 %\` | +| \`bridge\` | \`var(--_{c}-palette-{slot})\`: el privado que **escribe el forward** de la cascada de paleta (THM-2) | sí, **por la paleta** — y también por los tonos públicos del componente; acuñar encima mataría el \`color=\` por instancia — fuera del ratio | +| \`channel\` | \`var(--_{c}-x, fallback)\` que **nadie declara**: lo escribe soma o el envoltorio por INSTANCIA (un %, un rect medido, la talla de una prop) | **no debe**: fijarlo desde un tema rompe el comportamiento — fuera del ratio | **Alcance** = \`public / (public + private + global + literal)\`. \`system\`, -\`exception\` y \`structural\` quedan fuera del denominador: el primero es -tematizable a nivel de sistema por diseño, el segundo es una desviación que el -canon ya aceptó por escrito, y el tercero no tiene nada que un tema pueda nombrar. +\`exception\`, \`structural\`, \`bridge\` y \`channel\` quedan fuera del denominador: el +primero es tematizable a nivel de sistema por diseño, el segundo es una +desviación que el canon ya aceptó por escrito, el tercero no tiene nada que un +tema pueda nombrar, y los dos últimos son las dos formas de privado que la +doctrina de este eje ya adjudicó como NO deuda — el mecanismo de la paleta y el +valor de runtime. Lo que queda en \`private\` es el **residuo**: se adjudica clave +por clave (\`theming-census.ts --residue\`). **Límites de la medida** (regex sobre el CSS; sobre-reporta, nunca infra-reporta): una declaración con varios tokens se clasifica por la primera clase que casa @@ -1389,6 +1644,28 @@ sólo salen del **denominador**, igual que \`system\`. La lista firmada vive en ${structural.map((r) => `- [\`${r.component}\`](./${r.component}.md) (${r.structural}): ${STRUCTURAL_COMPONENTS.get(r.component)}`).join('\n')} +## Puente de paleta (${sum('bridge')} knobs) y canal de valor (${sum('channel')}) — privados que NO son deuda + +Las dos formas de privado que la doctrina de este eje ya adjudicó, y que ahora +se distinguen **mecánicamente** — medidas contra el emisor, no adivinadas por el +nombre. La fuente es la SALIDA del generador (\`src/uix/eidos/generated/base.css\`, +que \`generated-css.test.ts\` mantiene idéntica a \`renderStaticCss()\`): + +- **puente** — el nombre aparece en la CSS generada como la línea del forward + \`--_{c}-palette-{slot}: var(--palette-{slot}, …)\`. Un nombre con forma de + puente que nadie reenvía **no cuenta**. +- **canal** — el privado no lo declara nadie: ni la CSS del componente ni el + generador. Lo escribe soma o el envoltorio por instancia. + +**Puente** (${bridgeRows.length}): ${bridgeRows.map((r) => `[\`${r.component}\`](./${r.component}.md) ${r.bridge}`).join(' · ')} + +**Canal** (${channelRows.length}): ${channelRows.map((r) => `[\`${r.component}\`](./${r.component}.md) ${r.channel}`).join(' · ')} + +Lo que NO cae en ninguna de las dos es el **residuo** (${sum('private')} knobs en ${rows.filter((r) => r.private > 0).length} componentes): +conmutadores por variante, privados que leen un global en una rama, escaleras +por talla sin público detrás. Se adjudican clave a clave — \`theming-census.ts +--residue\` los vuelca con las fuentes de cada declaración. + ## Qué NO propone una ficha La propuesta deriva nombres de la doctrina; **no la contradice**. Por eso una @@ -1438,12 +1715,12 @@ ${noContract.map((c) => `[\`${c}\`](./${c}.md)`).join(' · ')} La columna «contrato» cuenta las claves **públicas** del bloque del componente. -| componente | alcance | knobs | público | privado | global | literal | sistema | estructural | contrato | size | -| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | :-: | +| componente | alcance | knobs | público | privado | global | literal | sistema | estructural | puente | canal | contrato | size | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | :-: | ${sorted .map( (r) => - `| [${r.component}](./${r.component}.md) | ${reachOf(r)} | ${r.knobs} | ${r.public} | ${r.private} | ${r.global} | ${r.literal} | ${r.system} | ${r.structural} | ${r.contractKeys} | ${r.hasSize ? 'y' : '–'} |` + `| [${r.component}](./${r.component}.md) | ${reachOf(r)} | ${r.knobs} | ${r.public} | ${r.private} | ${r.global} | ${r.literal} | ${r.system} | ${r.structural} | ${r.bridge} | ${r.channel} | ${r.contractKeys} | ${r.hasSize ? 'y' : '–'} |` ) .join('\n')} @@ -1892,6 +2169,11 @@ function main() { else reportDebt(only, args.includes('--json')); return; } + if (args.includes('--residue')) { + const next = args[args.indexOf('--residue') + 1]; + reportResidue(only, next && !next.startsWith('--') ? next : undefined, args.includes('--json')); + return; + } if (args.includes('--report')) { writeReport(); return; @@ -1905,7 +2187,7 @@ function main() { const themeable = sum('public') + sum('private') + sum('global') + sum('literal'); console.log(`theming-census — ${rows.length} component recipe(s)`); console.log( - ` appearance knobs ${sum('knobs')} · public ${sum('public')} (${pct(sum('public'), themeable)} reach) · private ${sum('private')} · global ${sum('global')} · literal ${sum('literal')} · system ${sum('system')} · exception ${sum('exception')} · structural ${sum('structural')}` + ` appearance knobs ${sum('knobs')} · public ${sum('public')} (${pct(sum('public'), themeable)} reach) · private ${sum('private')} · global ${sum('global')} · literal ${sum('literal')} · system ${sum('system')} · exception ${sum('exception')} · structural ${sum('structural')} · bridge ${sum('bridge')} · channel ${sum('channel')}` ); console.log( ` no contract entry: ${rows.filter((r) => r.contractKeys === 0 && !STRUCTURAL_COMPONENTS.has(r.component)).length} · reach < 20%: ${rows.filter((r) => r.reach < 0.2).length} · reach = 100%: ${rows.filter((r) => r.reach === 1 && r.public > 0).length} · with data-size: ${rows.filter((r) => r.hasSize).length} · structural: ${rows.filter((r) => STRUCTURAL_COMPONENTS.has(r.component)).length}` @@ -1928,6 +2210,8 @@ function main() { 'excep'.padStart(7) + 'system'.padStart(8) + 'strct'.padStart(7) + + 'bridge'.padStart(8) + + 'chan'.padStart(6) + 'contract'.padStart(10) + ' size' ); @@ -1943,6 +2227,8 @@ function main() { String(r.exception).padStart(7) + String(r.system).padStart(8) + String(r.structural).padStart(7) + + String(r.bridge).padStart(8) + + String(r.channel).padStart(6) + String(r.contractKeys).padStart(10) + (r.hasSize ? ' y' : ' -') );