You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/scripts/docs-vocabularies.ts

235 lines
9.6 KiB

feat(docs): S1 — generated canonical-vocabulary appendix (STUMBLES #1) An agent building a component from the docs-book alone could not assign archetypes / holds / haptic kinds: the closed sets live only in code consts and the corpus (correctly) forbids copying them into prose, so they were invisible from the docs. STUMBLES #1, the top stumble. Fix: docs/canon/vocabularies.md is GENERATED from the consts by 'npm run docs:vocabularies' — the ONE sanctioned place the lists are spelled out (generated = no drift objection), which every other doc links to. It covers part archetypes (with a one-line role each), sema families with their default hold/persistence + per-intent overrides, verbs by family, intents, the perceptual-duration scale, haptic kinds, the 33 palette scales, sizes, variant archetypes, and the shared common.* strings. - scripts/docs-vocabularies.ts imports the consts (robust — no fragile JSDoc/regex parsing) and formats them; the generation is an exported function so docs-check can compare. - Added ARCHETYPE_DESCRIPTIONS to morfo/types.ts (co-located with ARCHETYPE_VOCABULARY, satisfies Record<MorfoArchetype,string>) as the authoritative one-liners the appendix reads — descriptions become first-class data instead of inline union JSDoc a tool would have to parse. - docs-check gains I7: it regenerates in-memory and fails if the committed file drifts from the consts (negative-tested: corrupt -> error, regen -> green). The appendix's own counts pass I1, so I1 doubles as a second guard. - Linked from README (E2 canon row + two 'I want to' shortcuts), CANON.md (doctrine here, enumerated lists there), architecture/morfo.md (pick a part's archetype), and the component-audit minimum-package list (STUMBLES #8). docs:check 0 errors; npm run check unchanged in my files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
/**
* docs:vocabularies — generate the canonical-vocabulary appendix from the code
* consts, so agents building from the docs-book can SEE the closed sets they
* must draw from (archetypes, sema families + holds + verbs, intents, haptic
* kinds, palette scales, sizes, variants, shared strings) without any
* copy-the-list drift objection. Generated = the ONE sanctioned place these
* lists are spelled out; every other doc links here.
*
* Closes STUMBLES #1 (invisible canonical vocabularies — an agent could not
* assign archetypes / holds from the docs alone).
*
* Usage: npm run docs:vocabularies (writes the file)
* imported by scripts/docs-check.ts (freshness guard: the committed
* file must equal the generator)
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { ARCHETYPE_VOCABULARY, ARCHETYPE_DESCRIPTIONS } from '../src/uix/morfo/types';
import { PALETTE_SCALES, SIZES, SIZE_PRIMITIVE_KEYS, EIDOS_VARIANTS } from '../src/uix/eidos/lib/types';
import { SEMA_HOLDS_BY_INTENT } from '../src/uix/sema/holds';
import { SEMA_VERBS } from '../src/uix/sema/verbs';
fix(sema): los toggles recuperan voz, y el silencio deja de sintetizarse — muere el prefijo form. Tres cosas que el mismo hilo destapo, de la mas visible a la mas de fondo. 1) LOS TOGGLES SUENAN OTRA VEZ (directiva de autor 2026-08-05) switch, toggle y toggle-group llevaban SILENCIADOS desde el 2026-05-26: su pack aplicaba un tuning que restaba exactamente el gain base de la familia commit (0.3 - 0.3 = 0). Medido en navegador: pico 0.0001, el suelo de la rampa. Un silencio por defecto no se percibe como sobriedad sino como componente roto. Pasan a `commit.medium` (gain 0.1): audible, y un tercio de una pulsacion de boton (contact base 0.25) para que un flip no pese mas que una activacion. Los deltas de intent siguen montando encima (threat +0.1, fulfill +0.05). El haptico ligero no cambia. Medido despues: switch pico 0.1. Conservan el silencio, con su justificacion intacta, solo los dos donde el silencio ES la firma: tooltip (revela al pasar el raton, sonaria en cada cruce) y media-player (su propia salida ES audio — D-AP2.7, decision del hilo de audio). 2) EMITIR SILENCIO YA NO CUESTA `SoundChannel.handle` cortaba DESPUES de resolver la firma, asi que un tuning silenciador levantaba dos osciladores, un filtro y una envolvente programada para no sonar, en cada interaccion. Ahora corta antes de sintetizar cuando la ganancia resuelta es <= 0. El corte mira la ganancia RESUELTA, no el tuning, asi que un intent que la devuelve por encima de 0 (threat, fulfill) suena igual. Fijado con test propio, verificado en ROJO quitando el corto. 3) EL AFINADO SE NOMBRA POR LA FAMILIA QUE MODIFICA El catalogo mezclaba tres ejes en el primer segmento —familia (emerge.*, contact.silent), componente (tooltip.*, tabs.*) y la categoria huerfana form.*— sin regla declarada y sin guard que mirara. `form.commit.soft/subtle` entro el 2026-05-19 (09e261878, el commit que CREA sounds.ts) nombrando con verdad a siete controles de formulario; el 2026-05-26, 2d562f378 lo extendio a menus y arboles en la misma pasada que escribia la doctrina: 7 -> 29 consumidores en un commit, 50 hoy, de los que exactamente UNO era el Form. El D.6 de ese commit llego a poner el nombre correcto —commit.select— en la linea que usaba la clave incorrecta. form.commit.soft -> commit.soft form.commit.subtle -> commit.subtle form.toggle.silent -> commit.silent tooltip.silent -> emerge.silent tabs.select.soft -> commit.select.soft 131 ocurrencias en 68 ficheros, dirigido por `git ls-files` — nunca por recorrido de directorio: hay 3 worktrees vivos y build/ con 449 apariciones mas que un sed recursivo habria corrompido en silencio. La cola queda LIBRE a proposito: media-player aplica el silenciador al commit-set de un slider, no a un toggle — la cola describe la FIRMA, no el evento, asi que cerrarla contra el vocabulario de verbos fabricaria otra mentira. Por eso emerge.* no se toca pese a que `exit` no sea verbo canonico. Quedan tres silenciadores, uno por familia con base audible, con el mismo nombre: contact.silent (-0.25), commit.silent (-0.3), emerge.silent (-0.2). Eso convierte la coherencia en INVARIANTE: - `sounds-grammar.test.ts` (guard nuevo): cabeza ∈ SEMA_FAMILIES; forma {family}.{tail}; y todo {family}.silent resta EXACTAMENTE el gain base de su familia, leido de sema-map.ts —nunca copiado—. Verificado en ROJO: el guard calculo solo el -0.4 esperado de signal. - Ley escrita en sema.md § Tuning naming shapes, junto a la seccion hermana que ya legislaba los nombres de EVENTO. - vocabularies.md —"the ONE place the closed sets are spelled out"— pasa a enumerar los 14 afinados por familia; el generador los extrae del const y docs:check vigila su frescura. - D.5 gana las dos notas: el renombrado y la reversion del silencio. VERIFICACION: check 75 (linea base exacta) · sema 198/198 · docs:check 0/615 · navegador antes/despues (switch 0.0001 -> 0.1; button 0.25 sin tocar) · los 25 avisos de prettier entre lo tocado son deriva YA en HEAD, ninguno mio. Cronica NO reescrita (4 ficheros): AUDIT-blocks-2026-08-01, PLAN-audio-player, handoffs-claude-md y audit/components/date-field registran lo que ERA cierto. DESVIACION DECLARADA: sema/components/chronos.ts entra con 6 sustituciones mecanicas —chronos es de escritura excluida, pero dejarlo fuera rompia el type-check del repo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
import { SOUND_TUNINGS } from '../src/uix/sema/sounds';
feat(docs): S1 — generated canonical-vocabulary appendix (STUMBLES #1) An agent building a component from the docs-book alone could not assign archetypes / holds / haptic kinds: the closed sets live only in code consts and the corpus (correctly) forbids copying them into prose, so they were invisible from the docs. STUMBLES #1, the top stumble. Fix: docs/canon/vocabularies.md is GENERATED from the consts by 'npm run docs:vocabularies' — the ONE sanctioned place the lists are spelled out (generated = no drift objection), which every other doc links to. It covers part archetypes (with a one-line role each), sema families with their default hold/persistence + per-intent overrides, verbs by family, intents, the perceptual-duration scale, haptic kinds, the 33 palette scales, sizes, variant archetypes, and the shared common.* strings. - scripts/docs-vocabularies.ts imports the consts (robust — no fragile JSDoc/regex parsing) and formats them; the generation is an exported function so docs-check can compare. - Added ARCHETYPE_DESCRIPTIONS to morfo/types.ts (co-located with ARCHETYPE_VOCABULARY, satisfies Record<MorfoArchetype,string>) as the authoritative one-liners the appendix reads — descriptions become first-class data instead of inline union JSDoc a tool would have to parse. - docs-check gains I7: it regenerates in-memory and fails if the committed file drifts from the consts (negative-tested: corrupt -> error, regen -> green). The appendix's own counts pass I1, so I1 doubles as a second guard. - Linked from README (E2 canon row + two 'I want to' shortcuts), CANON.md (doctrine here, enumerated lists there), architecture/morfo.md (pick a part's archetype), and the component-audit minimum-package list (STUMBLES #8). docs:check 0 errors; npm run check unchanged in my files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
import { SEMA_DURATIONS } from '../src/uix/sema/durations';
import { INTENTS } from '../src/uix/intent';
import { commonLangs } from '../src/uix/langs/common';
const REPO = join(dirname(fileURLToPath(import.meta.url)), '..');
const OUT = join(REPO, 'docs/canon/vocabularies.md');
/** Parse the `kind: 'a' | 'b' | …` union off the HapticSignature. */
function hapticKinds(): string[] {
const src = readFileSync(join(REPO, 'src/uix/sema/channels.ts'), 'utf8');
const m = src.match(/kind:\s*((?:'[\w-]+'\s*\|\s*)*'[\w-]+')\s*;/);
if (!m) throw new Error('cannot find HapticSignature kind union in channels.ts');
return [...m[1].matchAll(/'([\w-]+)'/g)].map((x) => x[1]);
}
/** Collect the dotted leaf keys of commonLangs (e.g. `buttons.close`). */
function commonKeys(): string[] {
const out: string[] = [];
const walk = (node: Record<string, unknown>, prefix: string) => {
for (const [k, v] of Object.entries(node)) {
const path = prefix ? `${prefix}.${k}` : k;
if (v && typeof v === 'object' && !('es' in v) && !('en' in v)) {
walk(v as Record<string, unknown>, path);
} else {
out.push(path);
}
}
};
walk(commonLangs as Record<string, unknown>, '');
return out;
}
export function generateVocabulariesDoc(): string {
const L: string[] = [];
const desc = ARCHETYPE_DESCRIPTIONS as Record<string, string>;
const holdMs = (label: string) =>
label in SEMA_DURATIONS ? `${(SEMA_DURATIONS as Record<string, number>)[label]}ms` : label;
L.push('---');
L.push('title: Canonical vocabularies (generated)');
L.push('type: canon');
L.push('audience: human + agent');
L.push('authority: canonical — the closed sets, generated from the code consts');
L.push('status: current');
L.push('generated: npm run docs:vocabularies (do NOT edit by hand)');
L.push('---');
L.push('');
L.push('# Canonical vocabularies');
L.push('');
L.push('> **Generated from the code — do not edit.** Run `npm run docs:vocabularies`');
L.push('> to regenerate; `npm run docs:check` fails if this file drifts from the');
L.push('> consts. This is the ONE place the closed sets are spelled out (the reason');
L.push("> every other doc links here instead of copying a list that would go stale).");
L.push('> When building a component you draw part archetypes, event families/verbs,');
L.push('> intents and holds from exactly these sets (STUMBLES #1).');
L.push('');
// Archetypes
L.push(`## Part archetypes (${ARCHETYPE_VOCABULARY.length})`);
L.push('');
L.push('The cross-component classification a `part` may declare');
L.push('(`ARCHETYPE_VOCABULARY`, `src/uix/morfo/types.ts`). Omit it for a plain');
L.push('display part that pulls no shared styling.');
L.push('');
L.push('| Archetype | Role |');
L.push('| --- | --- |');
for (const a of ARCHETYPE_VOCABULARY) L.push(`| \`${a}\` | ${desc[a] || '—'} |`);
L.push('');
// Sema families + holds
const families = Object.keys(SEMA_HOLDS_BY_INTENT);
L.push(`## Sema families (${families.length}) — default hold + persistence`);
L.push('');
L.push('Each event declares a `semantic.family`; the default perceptual hold and');
L.push('persistence come from `SEMA_HOLDS_BY_INTENT` (`src/uix/sema/holds.ts`),');
L.push('overridable per event. Named holds map to `SEMA_DURATIONS`.');
L.push('');
L.push('| Family | Default hold | Persistence | Per-intent overrides |');
L.push('| --- | --- | --- | --- |');
for (const [family, intents] of Object.entries(SEMA_HOLDS_BY_INTENT)) {
const table = intents as Record<string, { hold: string; persistence: string }>;
const def = table._default;
const overrides = Object.entries(table)
.filter(([k]) => k !== '_default')
.map(([k, v]) => `${k}: ${holdMs(v.hold)}/${v.persistence}`)
.join('; ');
L.push(
`| \`${family}\` | ${holdMs(def.hold)} (\`${def.hold}\`) | ${def.persistence} | ${overrides || '—'} |`
);
}
L.push('');
// Verbs per family
L.push('## Sema verbs, by family');
L.push('');
L.push('The canonical verb set an event may use for each family (`SEMA_VERBS`,');
L.push('`src/uix/sema/verbs.ts`). A `family.verb` pairing outside this is drift.');
L.push('');
L.push('| Family | Verbs |');
L.push('| --- | --- |');
for (const [family, verbs] of Object.entries(SEMA_VERBS)) {
L.push(`| \`${family}\` | ${(verbs as string[]).map((v) => `\`${v}\``).join(' · ')} |`);
}
L.push('');
fix(sema): los toggles recuperan voz, y el silencio deja de sintetizarse — muere el prefijo form. Tres cosas que el mismo hilo destapo, de la mas visible a la mas de fondo. 1) LOS TOGGLES SUENAN OTRA VEZ (directiva de autor 2026-08-05) switch, toggle y toggle-group llevaban SILENCIADOS desde el 2026-05-26: su pack aplicaba un tuning que restaba exactamente el gain base de la familia commit (0.3 - 0.3 = 0). Medido en navegador: pico 0.0001, el suelo de la rampa. Un silencio por defecto no se percibe como sobriedad sino como componente roto. Pasan a `commit.medium` (gain 0.1): audible, y un tercio de una pulsacion de boton (contact base 0.25) para que un flip no pese mas que una activacion. Los deltas de intent siguen montando encima (threat +0.1, fulfill +0.05). El haptico ligero no cambia. Medido despues: switch pico 0.1. Conservan el silencio, con su justificacion intacta, solo los dos donde el silencio ES la firma: tooltip (revela al pasar el raton, sonaria en cada cruce) y media-player (su propia salida ES audio — D-AP2.7, decision del hilo de audio). 2) EMITIR SILENCIO YA NO CUESTA `SoundChannel.handle` cortaba DESPUES de resolver la firma, asi que un tuning silenciador levantaba dos osciladores, un filtro y una envolvente programada para no sonar, en cada interaccion. Ahora corta antes de sintetizar cuando la ganancia resuelta es <= 0. El corte mira la ganancia RESUELTA, no el tuning, asi que un intent que la devuelve por encima de 0 (threat, fulfill) suena igual. Fijado con test propio, verificado en ROJO quitando el corto. 3) EL AFINADO SE NOMBRA POR LA FAMILIA QUE MODIFICA El catalogo mezclaba tres ejes en el primer segmento —familia (emerge.*, contact.silent), componente (tooltip.*, tabs.*) y la categoria huerfana form.*— sin regla declarada y sin guard que mirara. `form.commit.soft/subtle` entro el 2026-05-19 (09e261878, el commit que CREA sounds.ts) nombrando con verdad a siete controles de formulario; el 2026-05-26, 2d562f378 lo extendio a menus y arboles en la misma pasada que escribia la doctrina: 7 -> 29 consumidores en un commit, 50 hoy, de los que exactamente UNO era el Form. El D.6 de ese commit llego a poner el nombre correcto —commit.select— en la linea que usaba la clave incorrecta. form.commit.soft -> commit.soft form.commit.subtle -> commit.subtle form.toggle.silent -> commit.silent tooltip.silent -> emerge.silent tabs.select.soft -> commit.select.soft 131 ocurrencias en 68 ficheros, dirigido por `git ls-files` — nunca por recorrido de directorio: hay 3 worktrees vivos y build/ con 449 apariciones mas que un sed recursivo habria corrompido en silencio. La cola queda LIBRE a proposito: media-player aplica el silenciador al commit-set de un slider, no a un toggle — la cola describe la FIRMA, no el evento, asi que cerrarla contra el vocabulario de verbos fabricaria otra mentira. Por eso emerge.* no se toca pese a que `exit` no sea verbo canonico. Quedan tres silenciadores, uno por familia con base audible, con el mismo nombre: contact.silent (-0.25), commit.silent (-0.3), emerge.silent (-0.2). Eso convierte la coherencia en INVARIANTE: - `sounds-grammar.test.ts` (guard nuevo): cabeza ∈ SEMA_FAMILIES; forma {family}.{tail}; y todo {family}.silent resta EXACTAMENTE el gain base de su familia, leido de sema-map.ts —nunca copiado—. Verificado en ROJO: el guard calculo solo el -0.4 esperado de signal. - Ley escrita en sema.md § Tuning naming shapes, junto a la seccion hermana que ya legislaba los nombres de EVENTO. - vocabularies.md —"the ONE place the closed sets are spelled out"— pasa a enumerar los 14 afinados por familia; el generador los extrae del const y docs:check vigila su frescura. - D.5 gana las dos notas: el renombrado y la reversion del silencio. VERIFICACION: check 75 (linea base exacta) · sema 198/198 · docs:check 0/615 · navegador antes/despues (switch 0.0001 -> 0.1; button 0.25 sin tocar) · los 25 avisos de prettier entre lo tocado son deriva YA en HEAD, ninguno mio. Cronica NO reescrita (4 ficheros): AUDIT-blocks-2026-08-01, PLAN-audio-player, handoffs-claude-md y audit/components/date-field registran lo que ERA cierto. DESVIACION DECLARADA: sema/components/chronos.ts entra con 6 sustituciones mecanicas —chronos es de escritura excluida, pero dejarlo fuera rompia el type-check del repo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
// Sound tunings
const tuningKeys = Object.keys(SOUND_TUNINGS);
L.push(`## Sound tunings (${tuningKeys.length})`);
L.push('');
L.push('Parametric deltas over a family base signature (`SOUND_TUNINGS`,');
L.push('`src/uix/sema/sounds.ts`). A key is `{family}.{tail}`: the FIRST segment is');
L.push('the sema family whose base the tuning modifies — never a component — and the');
L.push('tail describes the resulting signature, not the triggering event. Every');
L.push('`{family}.silent` subtracts exactly that family base gain, so neutral lands');
L.push('at 0 while intent deltas still surface. Enforced by');
L.push('`src/uix/sema/sounds-grammar.test.ts`.');
L.push('');
L.push('| Family | Tunings |');
L.push('| --- | --- |');
for (const family of families) {
const owned = tuningKeys.filter((k) => k.split('.')[0] === family);
if (owned.length > 0) L.push(`| \`${family}\` | ${owned.map((k) => `\`${k}\``).join(' · ')} |`);
}
L.push('');
feat(docs): S1 — generated canonical-vocabulary appendix (STUMBLES #1) An agent building a component from the docs-book alone could not assign archetypes / holds / haptic kinds: the closed sets live only in code consts and the corpus (correctly) forbids copying them into prose, so they were invisible from the docs. STUMBLES #1, the top stumble. Fix: docs/canon/vocabularies.md is GENERATED from the consts by 'npm run docs:vocabularies' — the ONE sanctioned place the lists are spelled out (generated = no drift objection), which every other doc links to. It covers part archetypes (with a one-line role each), sema families with their default hold/persistence + per-intent overrides, verbs by family, intents, the perceptual-duration scale, haptic kinds, the 33 palette scales, sizes, variant archetypes, and the shared common.* strings. - scripts/docs-vocabularies.ts imports the consts (robust — no fragile JSDoc/regex parsing) and formats them; the generation is an exported function so docs-check can compare. - Added ARCHETYPE_DESCRIPTIONS to morfo/types.ts (co-located with ARCHETYPE_VOCABULARY, satisfies Record<MorfoArchetype,string>) as the authoritative one-liners the appendix reads — descriptions become first-class data instead of inline union JSDoc a tool would have to parse. - docs-check gains I7: it regenerates in-memory and fails if the committed file drifts from the consts (negative-tested: corrupt -> error, regen -> green). The appendix's own counts pass I1, so I1 doubles as a second guard. - Linked from README (E2 canon row + two 'I want to' shortcuts), CANON.md (doctrine here, enumerated lists there), architecture/morfo.md (pick a part's archetype), and the component-audit minimum-package list (STUMBLES #8). docs:check 0 errors; npm run check unchanged in my files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
// Intents
L.push(`## Intents (${INTENTS.length})`);
L.push('');
L.push('The evaluative axis (`INTENTS`, `src/uix/intent.ts`). `commit` and `signal`');
L.push('require one; the intent policy per family is `SEMA_FAMILY_POLICY`.');
L.push('');
L.push(INTENTS.map((i) => `\`${i}\``).join(' · '));
L.push('');
// Hold durations
L.push('## Hold / perceptual durations');
L.push('');
L.push('The named perceptual scale (`SEMA_DURATIONS`, `src/uix/sema/durations.ts`);');
L.push('an event `hold` is a label from here or a raw ms number.');
L.push('');
L.push('| Label | ms |');
L.push('| --- | --- |');
for (const [label, ms] of Object.entries(SEMA_DURATIONS)) L.push(`| \`${label}\` | ${ms} |`);
L.push('');
// Haptic kinds
const kinds = hapticKinds();
L.push(`## Haptic kinds (${kinds.length})`);
L.push('');
L.push('The `kind` a `haptic` channel signature may use (`HapticSignature`,');
L.push('`src/uix/sema/channels.ts`). Haptic is opt-in (`new EngineSemantic({ haptic: true })`).');
L.push('');
L.push(kinds.map((k) => `\`${k}\``).join(' · '));
L.push('');
// Palette scales
L.push(`## Palette scales (${PALETTE_SCALES.length})`);
L.push('');
L.push('The donor scales a component `color` prop accepts under `intent="neutral"`');
L.push('(`PALETTE_SCALES`, `src/uix/eidos/lib/types.ts`) — the per-instance override');
L.push('(`<X color="teal">`). NEVER hand-count this list.');
L.push('');
L.push(PALETTE_SCALES.map((s) => `\`${s}\``).join(' · '));
L.push('');
// Sizes
L.push(`## Sizes (${SIZES.length})`);
L.push('');
L.push('The size scale (`SIZES`; the physical primitives are `SIZE_PRIMITIVE_KEYS`,');
L.push('`src/uix/eidos/lib/types.ts`). `full` is a layout semantic, not a physical');
L.push('tier. Each component exposes the subset its recipe supports.');
L.push('');
L.push(
`Physical: ${SIZE_PRIMITIVE_KEYS.map((s) => `\`${s}\``).join(' · ')} · layout: \`full\``
);
L.push('');
// Variants
L.push('## Variant archetypes');
L.push('');
L.push('The canonical variant sets per archetype (`EIDOS_VARIANTS`,');
L.push('`src/uix/eidos/lib/types.ts`). A component narrows to one set; component-only');
L.push('values live in its own `types.ts`.');
L.push('');
L.push('| Archetype | Variants |');
L.push('| --- | --- |');
for (const [archetype, values] of Object.entries(EIDOS_VARIANTS)) {
L.push(`| \`${archetype}\` | ${(values as readonly string[]).map((v) => `\`${v}\``).join(' · ')} |`);
}
L.push('');
// Common strings
const keys = commonKeys();
L.push(`## Shared strings (\`common.*\`, ${keys.length})`);
L.push('');
L.push('The shared idlangref leaves (`commonLangs`, `src/uix/langs/common.ts`),');
L.push('reachable via `v.commonRef(...)` or `#?common.{path}|Fallback` — reuse these');
L.push('instead of re-declaring a close/cancel/clear label per component.');
L.push('');
L.push(keys.map((k) => `\`${k}\``).join(' · '));
L.push('');
return L.join('\n') + '\n';
}
// Direct run → write the file.
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('docs-vocabularies.ts')) {
writeFileSync(OUT, generateVocabulariesDoc(), 'utf8');
console.log(`docs:vocabularies → ${OUT}`);
}

Powered by TurnKey Linux.