feat(morfo,sema,contracts): M1(i) — el contrato dice QUIEN dispara cada evento (D.2)

El morfo no podia decir quien emite un evento declarado: todo se presumia del
runtime, y la verdad de las excepciones vivia fuera del contrato — en la lista
de deuda del guard D9 y en las excepciones del censo de packs. Las tablas de
eventos de los README prometian percepcion que no ocurre (la ficha M1, P1).

Ejecuta la decision firmada D.2 (IMPLEMENTATION_CONTRACT):

- `emission: 'runtime' | 'host' | 'external' | 'declared-only'` en MorfoEvent +
  literal en el schema sium. Ausente = 'runtime': las ~250 declaraciones
  existentes no se tocan y la presuncion sigue siendo la norma.
- El guard D9 exime por DECLARACION en vez de por lista: solo los 'runtime'
  exigen emisor. Y si un evento eximido sigue en INERT_EVENT_DEBT, el guard
  FALLA con «FIXED — remove it», para que la deuda no sobreviva a su
  resolucion.
- El pack-census gana el chequeo simetrico: una regla cuyo alcance son SOLO
  eventos 'declared-only' afina una percepcion que jamas estampara — muerta
  por definicion, sin lista de excepciones. 'host' NO cuenta como muerto: el
  anfitrion dispara por el runtime y la regla casa normal. Fixture con los dos
  casos (positivo y negativo con hermano runtime al alcance).
- `morfo.md` §Step 5.5 lleva el apendice tecnico en los terminos que D.2
  exige: la tabla de los cuatro valores × sus guards, y la nota doctrinal de
  por que NO es doctrina del libro — marcar un evento inerte como
  declared-only para callar al guard es ensanchar la deuda con otro nombre.

La sonda temporal (flag sobre tooltip.emerge-present → correr → revertir)
destapo un agujero real y lo cerro: el flag entre `name` y `semantic` hacia
INVISIBLE el evento al regex del guard — ni censado ni exento. El regex tolera
ahora la linea opcional, y la sonda termino dando la conducta disenada exacta:
exencion + exigencia de limpiar la deuda.

NADIE estrena el flag: los 7 de INERT_EVENT_DEBT quedan intactos y son la
parte (ii) — siete decisiones perceptuales del autor, una a una (la de
tooltip: ¿el componente mas ubicuo merece firma, o silencio declarado?). La
instancia original de la ficha (los 5 pickers con close inerte) esta muerta
desde la normalizacion de nombres.

Verificado: suites 945 ✓ / 6 ajenos · docs 0/624 · check 69 = base aislada,
diff VACIO · prettier: mis dos limpios siguen limpios, pack-census formateado
(regresion mia), contracts y morfo.md ya fallaban en HEAD.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent 846d201693
commit 1c4303b10f

@ -805,6 +805,28 @@ per-emission axis, which the morfo CAN declare a default for.
For a comprehensive worked example see the toggle and dialog morfos.
**`emission` — who fires the event (technical appendix; D.2, signed).** By
default a declared event is presumed emitted by this repo's providers
(`emission: 'runtime'`, the norm — the field is simply absent), and the D9
contract guard fails when none does. Three declared exceptions exist, and they
are metadata about the EMITTER, never a license to declare perception that
does not happen:
| Value | Who fires it | Guards |
| --- | --- | --- |
| `'runtime'` (default, absent) | a soma provider / eidos view via `runtime.trigger` | D9 requires a live emitter |
| `'host'` | the embedding application, through the runtime | D9 exempt; pack rules stay LIVE (the stamp happens) |
| `'external'` | an outside system consumes the declaration (analytics, external a11y) | D9 exempt; nothing stamps here |
| `'declared-only'` | nobody — contract surface for testing / docs | D9 exempt; a pack rule tuning it is DEAD and the pack census rejects it |
Doctrinal note (the reason this is an appendix and not book doctrine, per
D.2): presented wrong, this reads as "the system promises events that do not
exist". It is the opposite — the flag makes the emitter explicit so the
README event tables stop promising perception that never occurs, and the
guard can tell a declared surface from an abandoned one. Marking an inert
event `'declared-only'` to silence the guard is the same move as widening its
debt list, and the review treats it the same way.
### Step 6 — Wire the provider
In `src/uix/soma/components/{kebab}/{kebab}-provider.svelte.ts`:

@ -219,8 +219,21 @@ por los dos caminos.
consumidor en produccion (¿se gana el sitio?), y la rama `required` de
`validation.ts:39-46` es codigo muerto (inalcanzable tras `isSemaEvent`).
3. ~~**S-30/S-38**~~ EJECUTADO · ~~**S-19**~~ EJECUTADO PARCIAL (firma alineada; encender el canal sigue abierto, ver §3).
4. **§3.5 fable** — `M1 emission` (aceptada y nunca implementada; cerraría los
waivers de tooltip y gradient-picker) · ~~`M4`~~ **EJECUTADO 2026-08-13, con
4. **§3.5 fable** — ~~`M1 emission`~~ **PARTE (i) EJECUTADA 2026-08-13**: el
flag `emission: 'runtime' | 'host' | 'external' | 'declared-only'` de D.2
existe en `MorfoEvent` + schema (ausente = `'runtime'`, las ~250
declaraciones intactas), el guard D9 exime por declaracion (y si el evento
eximido esta en la deuda, EXIGE limpiarla — visto con sonda temporal sobre
tooltip), el pack-census rechaza reglas sobre eventos `declared-only`
(fixture 2 casos; `host` NO es muerto), y `morfo.md` §Step 5.5 lleva el
apendice tecnico con la nota doctrinal de D.2. La sonda destapo y cerro un
agujero: el flag entre `name` y `semantic` hacia INVISIBLE el evento al
regex del guard — ahora lo tolera. ⚠️ **QUEDA la parte (ii): las 7
resoluciones de INERT_EVENT_DEBT, una a una con el autor** (tooltip ×3:
¿firma o silencio declarado? · virtual-list/grid ×3 · gradient-picker
commit-reset = S-14). NADIE estrena el flag hasta esas decisiones. La
instancia original de la ficha (5 pickers con close inerte) esta MUERTA
desde la normalizacion de nombres · ~~`M4`~~ **EJECUTADO 2026-08-13, con
el DIAGNOSTICO CORREGIDO dos veces**: la premisa de la ficha era falsa (hay
12 declaraciones con enum de 3+, censadas: aura·checkbox·image·meter·
progress·tooltip) y su arreglo («exigir exactamente 2 valores») las habria

@ -376,10 +376,17 @@ function collectInertEventViolations(): string[] {
for (const file of listSourceFiles(morfoDir)) {
const component = basename(file, '.ts');
const source = readFileSync(file, 'utf8');
const declared = [...source.matchAll(/name:\s*'([a-z]+-[a-z0-9-]+)',\s*\n\s*semantic:/g)].map(
(m) => m[1]
);
if (declared.length === 0) continue;
// `semantic:` following `name:` is what discriminates an EVENT from a
// part (parts have `name:` too). The optional middle line is `emission`
// (D.2) — without it in the pattern, an event carrying the flag between
// the two anchors would fall out of the census entirely: invisible, not
// exempt. Measured with the probe that added the flag to a real event.
const matches = [
...source.matchAll(
/name:\s*'([a-z]+-[a-z0-9-]+)',\s*\n(?:\s*emission:\s*'[a-z-]+',\s*\n)?\s*semantic:/g
)
];
if (matches.length === 0) continue;
const dir = join(somaDir, component);
if (!existsSync(dir)) continue;
@ -388,8 +395,20 @@ function collectInertEventViolations(): string[] {
.map((f) => readFileSync(f, 'utf8'))
.join('\n') + shared;
for (const name of declared) {
for (const [i, match] of matches.entries()) {
const name = match[1];
const id = `${component}.${name}`;
// D.2: a declared `emission` other than 'runtime' says WHO fires the
// event instead of this repo's providers — declared exemption, not
// debt. The chunk runs to the next event declaration, so the flag is
// read from the event it belongs to.
const chunk = source.slice(match.index, matches[i + 1]?.index);
const emission =
/emission:\s*'(runtime|host|external|declared-only)'/.exec(chunk)?.[1] ?? 'runtime';
if (emission !== 'runtime') {
if (INERT_EVENT_DEBT.has(id)) fixed.push(id);
continue;
}
const emitted = blob.includes(`'${name}'`) || blob.includes(`"${name}"`);
if (emitted) {
if (INERT_EVENT_DEBT.has(id)) fixed.push(id);

@ -320,6 +320,10 @@ const a11ySemanticSchema = object({
const eventSchema = object({
name: string(),
semantic: eventSemanticSchema,
// D.2 — who fires the event. Absent = 'runtime' (the norm).
emission: optional(
union(literal('runtime'), literal('host'), literal('external'), literal('declared-only'))
),
a11ySemantic: optional(a11ySemanticSchema),
mode: optional(union(literal('blocking'), literal('advisory'))),
regime: optional(

@ -637,9 +637,34 @@ export interface MorfoA11ySemantic {
* contract: a component can declare not only that it emits public state,
* but also what semantic event that state transition means.
*/
/**
* Who fires a declared event (D.2, signed — IMPLEMENTATION_CONTRACT, kept out
* of the book's body on purpose):
*
* - `'runtime'` — a soma provider / eidos view calls `runtime.trigger`. The
* DEFAULT when the field is absent, and the norm: ~250 declarations.
* - `'host'` — the embedding application fires it through the runtime (the
* contract declares it; the emitter lives outside this repo).
* - `'external'` — an outside system consumes the declaration (analytics,
* external a11y tooling); nothing stamps in this runtime.
* - `'declared-only'` — contract surface with no emitter anywhere: testing
* and documentation surface. NOT a license to declare perception that
* never happens — the D9 guard exempts only what is explicitly marked,
* and a sema pack rule tuning a `declared-only` event is a dead rule the
* pack census rejects.
*/
export type MorfoEventEmission = 'runtime' | 'host' | 'external' | 'declared-only';
export interface MorfoEvent {
name: string;
semantic: MorfoEventSemantic;
/**
* Who fires this event — see {@link MorfoEventEmission}. Absent means
* `'runtime'`: an event is presumed emitted by this repo's providers, and
* the D9 guard fails when none does. Declare the exception, never assume
* it (D.2).
*/
emission?: MorfoEventEmission;
/**
* Accessibility commitments the event must honor (book §9.1). When
* omitted, the runtime applies no extra a11y behavior beyond the

@ -207,6 +207,49 @@ function stampPartsOf(event: MorfoEvent): readonly string[] {
return [...canonical, ...allowed, ...fallback];
}
/**
* True when every event the matchers can reach on `morfo` is declared-only —
* the rule tunes a perception that can never stamp (D.2 / M1). One reachable
* event with any other emission keeps the rule alive: 'runtime' obviously,
* and 'host' too (the embedding app fires through the runtime, so the
* selector matches normally).
*/
function declaredOnlyReachable(
morfo: Morfo,
matchers: Partial<Pick<ParsedSelector, 'eventName' | 'eventNamePrefix' | 'eventFamily'>>
): boolean {
const reachable = candidateEvents(morfo, {
marker: undefined,
eventName: undefined,
eventNamePrefix: undefined,
eventFamily: undefined,
eventIntent: undefined,
unmodelledEventAttrs: [],
...matchers
} as ParsedSelector);
return reachable.length > 0 && reachable.every((event) => event.emission === 'declared-only');
}
function collectDeclaredOnlyTunings(
rules: ReadonlyArray<{
id: string;
parsed: ParsedSelector;
packName: string;
file: string;
}>
): string[] {
const offenders: string[] = [];
for (const { id, parsed, packName, file } of rules) {
if (parsed.marker === undefined) continue;
const resolved = resolveMarker(parsed.marker, packName);
if (!resolved) continue;
if (declaredOnlyReachable(resolved.morfo, parsed)) {
offenders.push(`${file} (${id}): every event in reach is declared-only`);
}
}
return offenders;
}
/** Events of `morfo` a rule's matchers can reach. */
function candidateEvents(morfo: Morfo, parsed: ParsedSelector): readonly MorfoEvent[] {
return eventsOf(morfo).filter((event) => {
@ -539,6 +582,54 @@ describe('pack census — every cascade rule can fire, and fires where the morfo
).toEqual([]);
});
// D.2 / M1: `emission` says WHO fires an event. `'declared-only'` means
// nobody does — contract surface for testing / docs — so a pack rule whose
// reachable events are ALL declared-only tunes a perception that can never
// stamp: dead by definition, no exception list. `'host'` is NOT dead (the
// embedding app fires through the runtime and the rule matches normally).
it('tunes no event whose emission is declared-only', () => {
const offenders = collectDeclaredOnlyTunings(
RULES.map(({ id, parsed, pack }) => ({
id,
parsed,
packName: pack.pack.name,
file: pack.file
}))
);
expect(
offenders,
'a rule for a declared-only event tunes a perception that can never stamp — ' +
'wire the emitter, or drop the rule'
).toEqual([]);
});
it('the declared-only check bites (fixture)', () => {
// The check has no live offender (nobody uses the flag yet), so its
// teeth are proven on a fixture: a rule reaching one declared-only
// event must be reported; the same rule with a runtime sibling in
// reach must not.
const morfo = {
name: 'M1Fixture',
kebab: 'm1-fixture',
scope: ['soma', 'sema'],
events: [
{
name: 'signal-alert-probe',
emission: 'declared-only',
semantic: { family: 'signal', intent: 'risk', target: { target: 'provider' } }
},
{
name: 'signal-warn-probe',
semantic: { family: 'signal', intent: 'risk', target: { target: 'provider' } }
}
],
parts: []
} as unknown as Morfo;
expect(declaredOnlyReachable(morfo, { eventName: 'signal-alert-probe' })).toBe(true);
expect(declaredOnlyReachable(morfo, { eventFamily: 'signal' })).toBe(false);
});
it('re-exports every pack from the barrel (the canon says "import from the barrel")', () => {
// S-36: 71 pack files, 68 re-exports. `calendar`, `gradient-builder` and
// `gradient-picker` were missing, so every consumer following the canon

Loading…
Cancel
Save

Powered by TurnKey Linux.