A minimal fault substrate: native throw, family-scoped kinds, no Result monad.
  • TypeScript 91.5%
  • Shell 8.5%
Find a file
PerishFire 1ded128db4
All checks were successful
guard / guard (push) Successful in 21s
harness: step onto 0.4.0, the gate unpinned (#10)
2026-07-21 11:50:23 +00:00
.forgejo guard: the gate is global, not pinned per repo (#9) 2026-07-21 11:30:45 +00:00
.runseal harness: step onto 0.4.0, the gate unpinned (#10) 2026-07-21 11:50:23 +00:00
docs birth: seat the fault substrate 2026-07-20 15:14:50 +08:00
lib verb: first, and the shield family owns foreign and exhausted (#2) 2026-07-20 08:14:26 +00:00
tests verb: first, and the shield family owns foreign and exhausted (#2) 2026-07-20 08:14:26 +00:00
.gitignore birth: seat the fault substrate 2026-07-20 15:14:50 +08:00
AGENTS.md birth: seat the fault substrate 2026-07-20 15:14:50 +08:00
deno.json verb: first, and the shield family owns foreign and exhausted (#2) 2026-07-20 08:14:26 +00:00
negentropy.toml birth: seat the fault substrate 2026-07-20 15:14:50 +08:00
README.md birth: seat the fault substrate 2026-07-20 15:14:50 +08:00
runseal.toml birth: seat the fault substrate 2026-07-20 15:14:50 +08:00

shield

A minimal fault substrate for TypeScript. Native throw, family-scoped kinds, no Result monad, no runtime dependencies.

A business domain declares its failure vocabulary once:

export const fault = family("land", {
  dirty: z.object({ branch: z.string() }),
  missing: z.object({ path: z.string() }),
});

The declaration is the single source of truth: kinds and their payload shapes flow into both the runtime factories and the type of every handler. shield consumes the shape and never validates — validation, if you want it, is the business's own call.

Throw at the disease:

shield.assert(status.clean, () => fault.dirty({ branch }));

Recover locally, or dispose at the boundary:

const text = await io.fault.attempt(() => io.read(path), { missing: () => "" });

await run(main).catch(land.fault.consume({
  dirty: (f) => io.fail(`dirty on ${f.meta.branch}`),
  missing: (f) => io.fail(`no path ${f.meta.path}`),
  ahead: (f) => io.fail(`behind by ${f.meta.by}`),
}));

consume is exhaustive: every kind is handled or the compiler stops you. run absorbs every non-fault into a foreign fault, so a handler always faces a fault, and a bug still crashes loudly unless you route it on purpose.

See docs/constitution.md for the full design.