`IntentOptionalFamily` era `Exclude<SemaFamily, IntentRequiredFamily>`: una
derivacion de mundo abierto —«todo lo que no es required»— que se tragaba el
tercer valor en silencio. Una familia marcada `'forbidden'` caia en el cubo
OPCIONAL, donde el intent esta PERMITIDO, asi que el dia que el libro endurezca
`contact` (lo anticipa el propio docblock) se editaria una linea del const
creyendo cerrada la puerta y `contact + threat` seguiria compilando.
Los tres cubos se derivan ahora POSITIVAMENTE, uno por valor del eje, y detras
va la rama `{ family: IntentForbiddenFamily; intent?: never }` en `SemaEvent` y
en `MorfoEventSemantic`. Coste hoy: CERO, medido — `IntentForbiddenFamily` es
`never`, asi que la rama esta deshabitada y no toca la puerta de compilacion de
los 172 morfos. Lo que cambia es que la promesa del canon («editing the const
reshapes the discriminated unions») pasa a ser cierta.
Runtime: `isSemaEvent` gana la condicion simetrica. Y aparecio una arista que la
ficha del audit no vio — leer `SEMA_FAMILY_POLICY[f].intentRequirement` directo
ESTRECHA a `'required' | 'optional'` (no hay familia forbidden hoy), asi que la
comparacion no compila: «no overlap». Anotar el `const` no basta, el flujo lo
vuelve a estrechar. De ahi `intentRequirementOf(family)`, cuyo tipo de retorno ES
el eje; queda documentado en su docblock para que nadie lo deshaga.
⚠️ Desviacion DECLARADA de la receta firmada: NO se toca `validation.ts`. La
receta pedia la condicion simetrica «en los dos guardianes», pero medido antes de
escribir nada, la rama de politica que ya existe alli es INALCANZABLE:
`validateSemaEvent({family:'commit'})` lanza el mensaje generico «is not a valid
canonical semantic event», porque `isSemaEvent` rechaza aguas arriba. Anadir la
simetrica seria anadir mas codigo muerto. Va como hallazgo aparte.
Test nuevo `intent-policy.test.ts`: el probe de tipos con una replica ENDURECIDA
del const (tres `@ts-expect-error`, uno de ellos el que ayer se quedaba sin
consumir), la particion de los tres cubos contra el const, y el guard de runtime
derivado del const — vacuo hoy A PROPOSITO, vivo el dia que una familia pase a
`'forbidden'` sin que nadie tenga que acordarse.
Verificado por METODO, no por cifra: base aislada con stash = 69 errores, con mis
cambios 69, diff VACIO. sema+morfo 537 ✓ (532 + 5 nuevos) · contracts 6 fallos =
los 6 ajenos de la base · prettier limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
parent
cb1fd54d10
commit
1a174d5a6e
@ -0,0 +1,118 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isSemaEvent } from './event';
|
||||
import { validateSemaEvent } from './validation';
|
||||
import {
|
||||
intentRequirementOf,
|
||||
SEMA_FAMILY_POLICY,
|
||||
type IntentForbiddenFamily,
|
||||
type IntentOptionalFamily,
|
||||
type IntentRequiredFamily,
|
||||
type SemaFamily
|
||||
} from './types';
|
||||
|
||||
const requirementOf = (family: string) => intentRequirementOf(family as SemaFamily);
|
||||
|
||||
/**
|
||||
* The intent policy has THREE values and, until 2026-08-13, two of them were
|
||||
* implemented. `IntentOptionalFamily` was `Exclude<SemaFamily,
|
||||
* IntentRequiredFamily>` — an open-world derivation ("everything that is not
|
||||
* required") that quietly filed a `'forbidden'` family under OPTIONAL, where
|
||||
* intent is allowed. The runtime mirrored the hole: both guards only ever
|
||||
* compared against `'required'` (audit S-33).
|
||||
*
|
||||
* No family uses `'forbidden'` today (`CANON.md` §4 keeps it reserved), so the
|
||||
* only way to test the shape is to derive the buckets from a HARDENED replica
|
||||
* of the const and check what the compiler does with them — which is exactly
|
||||
* what the fix is about: the const is the source, and editing it must reshape
|
||||
* the unions by itself.
|
||||
*/
|
||||
describe('intent policy — the three buckets', () => {
|
||||
// ── Type level: the derivation shape, against a hardened policy ──────────
|
||||
//
|
||||
// These probes fail at `npm run check`, not in this runner: an unused
|
||||
// `@ts-expect-error` is a compile error (TS2578). Reverting the positive
|
||||
// derivation to `Exclude` makes the first one stop erroring, and the
|
||||
// directive above it goes unused → check turns red.
|
||||
|
||||
const HARDENED = {
|
||||
contact: { intentRequirement: 'forbidden' },
|
||||
commit: { intentRequirement: 'required' },
|
||||
signal: { intentRequirement: 'required' },
|
||||
handle: { intentRequirement: 'optional' },
|
||||
emerge: { intentRequirement: 'optional' },
|
||||
shift: { intentRequirement: 'optional' },
|
||||
sustain: { intentRequirement: 'optional' },
|
||||
delegate: { intentRequirement: 'optional' }
|
||||
} as const satisfies Record<SemaFamily, { intentRequirement: string }>;
|
||||
|
||||
type HardRequired = {
|
||||
[K in SemaFamily]: (typeof HARDENED)[K]['intentRequirement'] extends 'required' ? K : never;
|
||||
}[SemaFamily];
|
||||
type HardOptional = {
|
||||
[K in SemaFamily]: (typeof HARDENED)[K]['intentRequirement'] extends 'optional' ? K : never;
|
||||
}[SemaFamily];
|
||||
type HardForbidden = {
|
||||
[K in SemaFamily]: (typeof HARDENED)[K]['intentRequirement'] extends 'forbidden' ? K : never;
|
||||
}[SemaFamily];
|
||||
|
||||
type HardEvent =
|
||||
| { family: HardOptional; intent?: string }
|
||||
| { family: HardRequired; intent: string }
|
||||
| { family: HardForbidden; intent?: never };
|
||||
|
||||
it('a forbidden family cannot carry intent, and stays usable without it', () => {
|
||||
// @ts-expect-error contact is forbidden in HARDENED — intent must not compile
|
||||
const withIntent: HardEvent = { family: 'contact', intent: 'threat' };
|
||||
const bare: HardEvent = { family: 'contact' };
|
||||
// @ts-expect-error a required family still cannot drop its intent
|
||||
const missing: HardEvent = { family: 'commit' };
|
||||
|
||||
expect([withIntent, bare, missing]).toHaveLength(3);
|
||||
});
|
||||
|
||||
// ── The real types, as they stand ────────────────────────────────────────
|
||||
|
||||
it('the three buckets partition the families, and forbidden is empty today', () => {
|
||||
const required: IntentRequiredFamily[] = ['commit', 'signal'];
|
||||
const optional: IntentOptionalFamily[] = [
|
||||
'contact',
|
||||
'handle',
|
||||
'emerge',
|
||||
'shift',
|
||||
'sustain',
|
||||
'delegate'
|
||||
];
|
||||
// `IntentForbiddenFamily` is `never`: an empty array is the only value
|
||||
// this type accepts, which is the assertion.
|
||||
const forbidden: IntentForbiddenFamily[] = [];
|
||||
|
||||
expect([...required, ...optional, ...forbidden].sort()).toEqual(
|
||||
Object.keys(SEMA_FAMILY_POLICY).sort()
|
||||
);
|
||||
expect(Object.keys(SEMA_FAMILY_POLICY).filter((f) => requirementOf(f) === 'forbidden')).toEqual(
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
// ── Runtime: derived from the const, so it activates by itself ───────────
|
||||
|
||||
it('the runtime guard rejects intent on any family the policy forbids', () => {
|
||||
const forbidden = Object.keys(SEMA_FAMILY_POLICY).filter(
|
||||
(family) => requirementOf(family) === 'forbidden'
|
||||
);
|
||||
|
||||
// Vacuous today ON PURPOSE — `forbidden` is empty until the book hardens
|
||||
// a family, and then this covers it without anyone remembering to.
|
||||
for (const family of forbidden) {
|
||||
expect(isSemaEvent({ family })).toBe(true);
|
||||
expect(isSemaEvent({ family, intent: 'threat' })).toBe(false);
|
||||
expect(() => validateSemaEvent({ family } as never)).not.toThrow();
|
||||
expect(() => validateSemaEvent({ family, intent: 'threat' } as never)).toThrow();
|
||||
}
|
||||
|
||||
// The live half of the same gate, for contrast: `required` still bites.
|
||||
expect(isSemaEvent({ family: 'commit' })).toBe(false);
|
||||
expect(isSemaEvent({ family: 'commit', intent: 'affirm' })).toBe(true);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in new issue