Directiva del autor, sin matices: «todo el sistema de sonido es una puta
mierda... la unica solucion es evento sonido, evento.intent = sonido, y
punto y nada de mierdas de que si el intent modifica nada».
Tenia razon, y lo que lo destapo fue oirlo: los dieciseis nombres del
catalogo anterior sonaban practicamente igual. Medido despues: cinco de
ellos eran LA MISMA nota de 700 Hz a cinco volumenes, y las variantes de
direccion se separaban 20 Hz. El modelo de modulacion producia teoria
bonita y mush audible.
LA REGLA ENTERA, y no hay mas:
nombre = per-emit ?? cascada ?? pack ?? morfo ?? familia[verbo] ?? familia.default
sonido = pack[`${nombre}.${intent}`] ?? pack[nombre] ?? nada
Dos busquedas. El intent SELECCIONA un sonido entero; si no existe la
variante se desprecia y suena la base; si no hay base, silencio. Un
`threat` no es un `tick` con mas aspereza: es otro sonido —mas grave, mas
rasposo, descendente— y no se pueden confundir.
EL TIER DEL VERBO es lo que vacia los packs. La tabla de familia se
indexa por verbo (`emerge.close → 'close'`), asi que ni un solo overlay
escribe su cierre. De ~165 reglas de sonido quedan 30, en 16 de los 71
packs — y las 135 borradas no decian mas que el defecto. Para que el
verbo llegue, ahora VIAJA en la señal: se calculaba y se tiraba con un
`void` (hallazgo S-40), el tipo prometia que se emitia y era falso.
CATALOGO NUEVO: 10 bases + 6 variantes, cada una un sonido diseñado
entero. Medido en el grafo de audio real: de 240 Hz (`tick.loss`) a
1900 Hz (`step`), casi tres octavas, y las variantes evaluativas traen su
propio modulador de aspereza — texturas distintas, no un tono movido.
100% sintesis a proposito: el pack por defecto no lleva binarios, funciona
sin red y no puede dar 404. Los .wav quedan como pack de EJEMPLO.
EL GUARD QUE FALTABA: `distinguishability` exige que dos entradas se
separen en al menos DOS ejes perceptuales. Nacio en rojo contra mi propio
catalogo —ocho pares casi gemelos— y dirigio el diseño hasta separarlos.
Una instantanea no lo habria cazado: cada valor era exactamente el que la
tabla decia; lo que faltaba era una RELACION entre entradas.
GESTOS POR REPETICION (idea del autor). Un arrastre ya emite a su ritmo,
asi que toca `step` (18 ms) por emision: la velocidad del gesto ES la del
trinquete, como una rueda fisica. Mueren los tres resolvers que
sintetizaban tono desde posicion y velocidad —la ultima aritmetica del
sistema— y `handle` queda EXENTA de la memoria de frecuencia, que si no
estrangularia el trinquete al primer arrastre. Perdida firmada: el ritmo
lleva la velocidad, pero la posicion ya no mapea a altura.
NADIE ESCRIBE PARAMETROS, ni la app. `SemaSignatureOverride.sound` y
`SemaCascadeRule.sound` aceptan un nombre o SILENT. La rendija de autoria
inline «para la personalizacion» sobraba: un producto REGISTRA su sonido
(nombre + definicion, una vez) y lo nombra.
⚠️ BUG PROPIO durante la cirugia: al reemplazar el bloque del sonido borre
tambien la aplicacion de deltas de intent, y el HAPTICO dejo de recibirlos.
Lo cazaron sus tests. Restaurado — el haptico sigue modulando porque su
vocabulario es categorico y sus deltas cambian el KIND, que tambien es
seleccion.
MUERE: los deltas de sonido del intent, las bases de sonido de familia, la
capa 1.5, `op:'add'` para sonido, `mergeSoundOverride`, y
`d7-intent-survives.test.ts` — vigilaba una inversion que ya no puede
existir.
VERIFICADO: sema+morfo+soma+sound 1108/1108 · docs:check 0/618 · la pagina
`/uix/docs/sound` reescrita y medida en navegador (commit+threat →
tick.threat; commit+affirm → tick, el intent se desprecia; emerge+close →
close sin una linea de pack; sustain → silencio). Los 7 fallos de la suite
completa son los ajenos y preexistentes de contracts.test.
⚠️ NO SE HA OIDO. Todo esta medido en el grafo de audio, no con un oido.
Si `open` suena a lo que debe sonar un panel abriendose es tu decision, y
se afina en un fichero.
⚠️ El estudio `/temas/sema` compila pero edita firmas crudas: su modelo es
el viejo. Queda por reconvertir.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
parent
09f8d76e02
commit
86fa9a1b77
@ -1,67 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveSignature } from './resolver';
|
||||
import { SEMA_MAP } from './sema-map';
|
||||
import { colorPickerSema } from './components/color-picker';
|
||||
import { gradientPickerSema } from './components/gradient-picker';
|
||||
import { proofOfHumanSema } from './components/proof-of-human';
|
||||
|
||||
/**
|
||||
* D.7 guard — a pack rule may add CHARACTER, never replace the intent's
|
||||
* evaluative profile.
|
||||
*
|
||||
* These four packs used to canonize samples on `commit` events
|
||||
* (`sound('handle.release.soft')`, `sound('handle.snap.chime')`,
|
||||
* `sound('notification.ping')`, `sound('alert.error')`). `sound()` returns a
|
||||
* COMPLETE signature and the cascade applies it in `replace` mode, so the
|
||||
* sample flattened exactly what D.7 exists to protect: changing the intent
|
||||
* changed nothing audible. Corrected 2026-08-06.
|
||||
*
|
||||
* The axes the intent owns are `pitch` / `contour` / `gain` / `roughness`; a
|
||||
* rule composes over them with `{ op: 'add' }` or stays out of them.
|
||||
*/
|
||||
describe('D.7 — the intent survives the pack', () => {
|
||||
/** Matches only the rule whose selector names THIS event — as the DOM would. */
|
||||
function targetFor(event: string): HTMLElement {
|
||||
const el = {
|
||||
matches: (sel: string) => sel.includes(`"${event}"`),
|
||||
closest: (sel: string) => (sel.includes(`"${event}"`) ? el : null)
|
||||
} as unknown as HTMLElement;
|
||||
return el as HTMLElement;
|
||||
}
|
||||
|
||||
function resolve(
|
||||
pack: { cascade: readonly unknown[] },
|
||||
name: string,
|
||||
intent?: 'fulfill' | 'risk'
|
||||
) {
|
||||
return resolveSignature(
|
||||
{ target: targetFor(name), name, family: 'commit', ...(intent ? { intent } : {}) },
|
||||
{ map: SEMA_MAP, cascade: [...pack.cascade] as never }
|
||||
).sound;
|
||||
}
|
||||
|
||||
it.each([
|
||||
['color-picker', colorPickerSema, 'commit-set'],
|
||||
['gradient-picker', gradientPickerSema, 'commit-save'],
|
||||
['proof-of-human', proofOfHumanSema, 'commit-confirm']
|
||||
])('%s · %s keeps the intent audible', (_name, pack, event) => {
|
||||
const neutral = resolve(pack as never, event)!;
|
||||
const fulfill = resolve(pack as never, event, 'fulfill')!;
|
||||
const risk = resolve(pack as never, event, 'risk')!;
|
||||
|
||||
// `fulfill` adds +300 pitch and +0.05 gain; `risk` adds +0.2 roughness.
|
||||
expect(fulfill.pitch).toBeGreaterThan(neutral.pitch);
|
||||
expect(fulfill.gain).toBeGreaterThan(neutral.gain);
|
||||
expect(risk.roughness).toBeGreaterThan(neutral.roughness);
|
||||
});
|
||||
|
||||
it('keeps the character the samples carried (a brighter settle)', () => {
|
||||
// commit base centroid is 1800; the rule lifts it where `handle.release.soft`
|
||||
// used to sit, without touching a single axis the intent owns.
|
||||
const neutral = resolve(colorPickerSema as never, 'commit-set')!;
|
||||
expect(neutral.centroid).toBeGreaterThan(SEMA_MAP.families.commit.base.sound!.centroid);
|
||||
});
|
||||
});
|
||||
@ -1,29 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { SEMA_DURATIONS, resolveSemaDuration } from './durations'
|
||||
import { SEMA_DURATIONS, resolveSemaDuration } from './durations';
|
||||
|
||||
describe('SEMA_DURATIONS', () => {
|
||||
it('exposes the canonical perceptual scale (ms, ascending)', () => {
|
||||
expect(SEMA_DURATIONS.subliminal).toBe(50)
|
||||
expect(SEMA_DURATIONS.glimpse).toBe(120)
|
||||
expect(SEMA_DURATIONS.brief).toBe(240)
|
||||
expect(SEMA_DURATIONS.noticed).toBe(600)
|
||||
expect(SEMA_DURATIONS.insistent).toBe(1200)
|
||||
expect(SEMA_DURATIONS.persistent).toBe(3000)
|
||||
})
|
||||
})
|
||||
expect(SEMA_DURATIONS.subliminal).toBe(50);
|
||||
expect(SEMA_DURATIONS.glimpse).toBe(120);
|
||||
expect(SEMA_DURATIONS.brief).toBe(240);
|
||||
expect(SEMA_DURATIONS.noticed).toBe(600);
|
||||
expect(SEMA_DURATIONS.insistent).toBe(1200);
|
||||
expect(SEMA_DURATIONS.persistent).toBe(3000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveSemaDuration', () => {
|
||||
it('returns the literal ms for a number', () => {
|
||||
expect(resolveSemaDuration(420)).toBe(420)
|
||||
})
|
||||
expect(resolveSemaDuration(420)).toBe(420);
|
||||
});
|
||||
|
||||
it('looks up a label', () => {
|
||||
expect(resolveSemaDuration('brief')).toBe(240)
|
||||
expect(resolveSemaDuration('noticed')).toBe(600)
|
||||
})
|
||||
expect(resolveSemaDuration('brief')).toBe(240);
|
||||
expect(resolveSemaDuration('noticed')).toBe(600);
|
||||
});
|
||||
|
||||
it('returns undefined when no spec is provided (lets caller fall back)', () => {
|
||||
expect(resolveSemaDuration(undefined)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
expect(resolveSemaDuration(undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1 +1 @@
|
||||
export * from './exports'
|
||||
export * from './exports';
|
||||
|
||||
@ -1,52 +1,49 @@
|
||||
import { isSemaEvent, isSemaEventLabel, isIntent, isIntentBinding } from './event'
|
||||
import { SemaInvariantError } from './errors'
|
||||
import { SEMA_FAMILY_POLICY, type SemaActionEvent, type IntentBinding } from './types'
|
||||
import { isSemaEvent, isSemaEventLabel, isIntent, isIntentBinding } from './event';
|
||||
import { SemaInvariantError } from './errors';
|
||||
import { SEMA_FAMILY_POLICY, type SemaActionEvent, type IntentBinding } from './types';
|
||||
|
||||
export { SemaInvariantError } from './errors'
|
||||
export { SemaInvariantError } from './errors';
|
||||
|
||||
export function validateIntentBinding(binding: IntentBinding, ctx = 'sema.intent'): void {
|
||||
if (!isIntentBinding(binding)) {
|
||||
throw new SemaInvariantError(`${ctx} is not a valid IntentBinding`)
|
||||
throw new SemaInvariantError(`${ctx} is not a valid IntentBinding`);
|
||||
}
|
||||
|
||||
const supported = binding.supported ?? []
|
||||
if (supported.length === 0) return
|
||||
const supported = binding.supported ?? [];
|
||||
if (supported.length === 0) return;
|
||||
|
||||
for (const intent of supported) {
|
||||
if (!isIntent(intent)) {
|
||||
throw new SemaInvariantError(`${ctx}: intent "${String(intent)}" is not a valid Intent`)
|
||||
throw new SemaInvariantError(`${ctx}: intent "${String(intent)}" is not a valid Intent`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!supported.includes(binding.default)) {
|
||||
throw new SemaInvariantError(
|
||||
`${ctx}: default intent "${binding.default}" must be included in supported intents`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateSemaEvent(event: SemaActionEvent, ctx = 'sema.event'): void {
|
||||
if (isSemaEventLabel(event)) return
|
||||
if (isSemaEventLabel(event)) return;
|
||||
|
||||
if (!isSemaEvent(event)) {
|
||||
throw new SemaInvariantError(`${ctx} is not a valid canonical semantic event`)
|
||||
throw new SemaInvariantError(`${ctx} is not a valid canonical semantic event`);
|
||||
}
|
||||
|
||||
const intent = 'intent' in event ? event.intent : undefined
|
||||
const intent = 'intent' in event ? event.intent : undefined;
|
||||
|
||||
// Policy enforcement: families marked `intentRequirement: 'required'` MUST
|
||||
// declare intent.
|
||||
if (
|
||||
intent === undefined &&
|
||||
SEMA_FAMILY_POLICY[event.family].intentRequirement === 'required'
|
||||
) {
|
||||
if (intent === undefined && SEMA_FAMILY_POLICY[event.family].intentRequirement === 'required') {
|
||||
throw new SemaInvariantError(
|
||||
`${ctx}: family "${event.family}" requires intent (intentRequirement: 'required')`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Bindings need structural validation; literals + absence are fine.
|
||||
if (intent === undefined || typeof intent === 'string') return
|
||||
if (intent === undefined || typeof intent === 'string') return;
|
||||
|
||||
validateIntentBinding(intent, `${ctx}.intent`)
|
||||
validateIntentBinding(intent, `${ctx}.intent`);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue