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 (alpha-0.1-dir-prefs09e261878, el commit que CREA sounds.ts) nombrando con verdad a siete controles de formulario; el 2026-05-26,2d562f378lo 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>
parent
05add971c7
commit
d05ecc1aa8
@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Census guard — the naming grammar of `SOUND_TUNINGS`.
|
||||
*
|
||||
* WHY THIS EXISTS. A tuning is a parametric delta over a FAMILY's base
|
||||
* signature. Its name must say which family it modifies, because that is the
|
||||
* only axis that is simultaneously true and mechanically checkable: the eight
|
||||
* families are a closed set (`SEMA_FAMILIES`), a component name is not.
|
||||
*
|
||||
* THE DRIFT THAT PAID FOR IT. `form.commit.soft` / `form.commit.subtle`
|
||||
* entered on 2026-05-19 (`09e261878`, the commit that created `sounds.ts`)
|
||||
* naming seven form controls — checkbox, radio-group, stepper, tags-input,
|
||||
* tag-group, file-upload, editable — truthfully. One week later
|
||||
* `2d562f378` (2026-05-26) added `form.toggle.silent` for switch/toggle AND
|
||||
* applied `form.commit.subtle` to menubar, navigation-menu, dropdown-menu,
|
||||
* context-menu, tree-view and tree-grid. Consumers went 7 → 29 in a single
|
||||
* commit, and by 2026-08-05 they were 50 — of which exactly ONE was the Form
|
||||
* component. The doctrine written in that same commit (D.6) even spelled the
|
||||
* right name, `commit.select`, on the very line that used the wrong key.
|
||||
*
|
||||
* Nothing caught it for two and a half months because nothing looked:
|
||||
* `SoundTuningName = keyof typeof SOUND_TUNINGS` closes the set of keys but
|
||||
* says nothing about their SHAPE, and no script referenced the catalogue.
|
||||
* Renamed on 2026-08-05 (`form.*` → `commit.*`, `tooltip.silent` →
|
||||
* `emerge.silent`, `tabs.select.soft` → `commit.select.soft`).
|
||||
*
|
||||
* WHAT IS NOT CHECKED, deliberately: the tail. `commit.silent` is applied to
|
||||
* a slider's `commit-set` in `media-player.ts` as legitimately as to a
|
||||
* switch's `commit-toggle` — the tail describes the resulting SIGNATURE, not
|
||||
* the triggering event, so closing it against the verb vocabulary would
|
||||
* manufacture a lie. There is no closed set for it in the codebase, and
|
||||
* inventing one here would be exactly the copied-canon this guard exists to
|
||||
* prevent.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { SEMA_FAMILIES } from './event';
|
||||
import { SEMA_MAP } from './sema-map';
|
||||
import { SOUND_TUNINGS } from './sounds';
|
||||
import type { SemaFamily } from './types';
|
||||
|
||||
/**
|
||||
* Keys allowed to break the grammar, each with the reason it earned the pass.
|
||||
* Growing this list is a decision, not a convenience.
|
||||
*/
|
||||
const EXCEPTIONS: Record<string, string> = {};
|
||||
|
||||
const TUNING_KEYS = Object.keys(SOUND_TUNINGS);
|
||||
|
||||
/** The family base gain, read from the map — never copied into this file. */
|
||||
function baseGain(family: SemaFamily): number | undefined {
|
||||
return SEMA_MAP.families[family]?.base?.sound?.gain;
|
||||
}
|
||||
|
||||
describe('SOUND_TUNINGS naming grammar', () => {
|
||||
it('finds the catalogue (sanity: the census is not scanning an empty const)', () => {
|
||||
expect(TUNING_KEYS.length).toBeGreaterThan(8);
|
||||
});
|
||||
|
||||
it('heads every key with a sema family', () => {
|
||||
const families = new Set<string>(SEMA_FAMILIES);
|
||||
const offenders = TUNING_KEYS.filter((key) => key in EXCEPTIONS === false).filter(
|
||||
(key) => !families.has(key.split('.')[0])
|
||||
);
|
||||
expect(
|
||||
offenders,
|
||||
`a tuning is a delta on a FAMILY — head these keys with one of ${[...families].join(' | ')}`
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('gives every key a tail describing the signature', () => {
|
||||
const malformed = TUNING_KEYS.filter((key) => {
|
||||
const segments = key.split('.');
|
||||
return segments.length < 2 || segments.some((s) => !/^[a-z][a-z-]*$/.test(s));
|
||||
});
|
||||
expect(malformed, 'shape is `{family}.{tail}`, lowercase segments').toEqual([]);
|
||||
});
|
||||
|
||||
it('makes every silencer subtract EXACTLY its family base gain', () => {
|
||||
const silencers = TUNING_KEYS.filter((key) => key.endsWith('.silent'));
|
||||
// The silence trick only exists because there is a base to cancel.
|
||||
expect(silencers.length, 'the catalogue lost its silencers').toBeGreaterThan(0);
|
||||
|
||||
const wrong: string[] = [];
|
||||
for (const key of silencers) {
|
||||
const family = key.split('.')[0] as SemaFamily;
|
||||
const base = baseGain(family);
|
||||
const gain = (SOUND_TUNINGS as Record<string, { gain?: unknown }>)[key].gain;
|
||||
if (base === undefined) {
|
||||
wrong.push(`${key}: family '${family}' declares no base.sound.gain to cancel`);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
typeof gain !== 'object' ||
|
||||
gain === null ||
|
||||
(gain as { op?: string }).op !== 'add' ||
|
||||
(gain as { value?: number }).value !== -base
|
||||
) {
|
||||
wrong.push(`${key}: expected { op: 'add', value: ${-base} }, got ${JSON.stringify(gain)}`);
|
||||
}
|
||||
}
|
||||
expect(
|
||||
wrong,
|
||||
'a `.silent` tuning must land the family at gain 0 so intent deltas still surface'
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps the exception list honest (every entry still names a live key)', () => {
|
||||
const stale = Object.keys(EXCEPTIONS).filter((key) => !TUNING_KEYS.includes(key));
|
||||
expect(stale, 'these exceptions excuse keys that no longer exist — delete them').toEqual([]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in new issue