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

264 lines
11 KiB

/**
* 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,
* directions, 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';
import { SOUNDS } from '../src/uix/sema/sound-names';
import { SEMA_DURATIONS } from '../src/uix/sema/durations';
import { SEMA_DIRECTIONS } from '../src/uix/sema/types';
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('');
// Sound catalogue
const soundNames = Object.keys(SOUNDS);
L.push(`## Sounds (${soundNames.length})`);
L.push('');
L.push('THE catalogue (`SOUNDS`, `src/uix/sema/sound-names.ts`). A component names');
L.push('one of these and writes nothing else: the type of a pack rule accepts a name');
L.push('or `SILENT`, and nothing more. Whether a name resolves to a synthesised');
L.push('recipe or to a `.wav` is decided here, never at the component.');
L.push('');
L.push('A name is applied over the family base and BEFORE the intent deltas, so the');
L.push('evaluative profile always survives it — which is what makes the old D.7 / S-07');
L.push('class of defect impossible rather than merely forbidden.');
L.push('');
L.push('Silence is not a name: it is `SILENT`, one canonical value for the whole');
L.push('system (`$uix/sema`). Declared on a channel slice, the resolver honours it by');
L.push('dropping that channel.');
L.push('');
L.push(soundNames.map((n) => `\`${n}\``).join(' · '));
L.push('');
// 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('');
// Directions
L.push(`## Directions (${SEMA_DIRECTIONS.length})`);
L.push('');
L.push('The sense of a traversal (`SEMA_DIRECTIONS`, `src/uix/sema/types.ts`),');
L.push('projected as `data-event-direction`. Per emission like the intent, and');
L.push('unlike it undeclarable on the EVENT: one `shift-navigate` is the previous');
L.push('month and the next one is the following month, so only the caller knows');
L.push('(`TriggerOptions.direction`). Optional — most occurrences have no sense to');
L.push('declare, and an invented one is worse than none.');
L.push('');
L.push('A SENSE, never an axis: eidos maps `forward` onto the inline end and');
L.push('`backward` onto the inline start, so RTL flips through `:dir(rtl)` and');
L.push('nothing upstream knows about it. Distinct from `SoundContour`');
L.push('(`ascending` / `descending`, `src/uix/sema/sounds.ts`), which shapes a pitch,');
L.push('and from `Morfo.direction`, which names the parts that carry the `dir` stamp');
L.push('(the RTL contract — see `docs/canon/direction-contract.md`).');
L.push('');
L.push(SEMA_DIRECTIONS.map((d) => `\`${d}\``).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.