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

294 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 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 {
LOGICAL_POSITIONS,
PALETTE_SCALES,
POSITIONS,
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 readonly 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('');
// Placement grids — TWO of them, and the pair is the doctrine.
L.push(`## Placement grids (2 × ${POSITIONS.length})`);
L.push('');
L.push('The 3×3 grids a placement prop draws from (`POSITIONS` /');
L.push('`LOGICAL_POSITIONS`, `src/uix/eidos/lib/types.ts`). Narrow with');
L.push('`Extract<…>`; add component-only values (`static`) by union. NEVER');
L.push('re-declare a grid — four components did until 2026-08-15 and were kept');
L.push('in step by hand.');
L.push('');
L.push('**Choosing is a BEHAVIOUR decision, not a naming one**: does the');
L.push('placement have to flip for a right-to-left reader? A strip pinned to');
L.push('`bottom-end` belongs on the trailing edge in both directions; a panel');
L.push('that opens to the physical right because that is where the space is does');
L.push('not. This pair settles EID-3, which recorded the physical exception in');
L.push('July 2026 and left its doctrine pending.');
L.push('');
L.push('| Grid | Mirrors in RTL | Values | Used by |');
L.push('| --- | --- | --- | --- |');
L.push(
`| \`Position\` (physical) | no — \`left\` is the screen's left | ${POSITIONS.map((p) => `\`${p}\``).join(' · ')} | Dialog · Drawer · Toast · floating anchors |`
);
L.push(
`| \`LogicalPosition\` | yes — \`start\`/\`end\` follow the direction | ${LOGICAL_POSITIONS.map((p) => `\`${p}\``).join(' · ')} | Affix + its consumers (Fab · MenuDial) · OnionMenu · Avatar badge |`
);
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.