@frmwrkd/snag-core
v0.1.3
Published
Snag schemas: the single source of truth for captures, tasks and plans.
Readme
@frmwrkd/snag-core
Zod schemas and TypeScript types for Snag — the single source of truth.
Licence: Apache-2.0
Spec: docs/02-BACKEND-SPEC.md §5 (data model) and §3.1 (repo map). Locked
decisions are docs/00-MASTER-BUILD-PLAN.md §7 — do not relitigate them.
Every type in this package comes from z.infer. There are no hand-written type
duplicates, and nothing outside this package may define a schema it owns.
If you are building against this package, read these six things
1. IDs are strict. Use the minting helpers in your fixtures.
cap_, thr_, tsk_, plan_ followed by exactly 26 Crockford-base32
characters. The abbreviated ids printed in the spec — cap_01J8X,
tsk_01J9C... — are documentation shorthand and will not parse.
import { newCaptureId, newTaskUid, formatTaskLabel } from '@frmwrkd/snag-core';
const id = newCaptureId(); // cap_01K1R7Q0F8ZC4M7B9V2XE6HTPD
const uid = newTaskUid(1_700_000_000_000); // seeded — deterministic in tests
const label = formatTaskLabel(12); // SNAG-12Tasks carry both: uid (ULID) is what depends_on, blocks, plan.waves
and all sync resolve against; id (SNAG-<n>) is a human label from
next_task_id in .snag/config.json and is never reused or renumbered.
2. Severity: never compare the strings.
p0 is the most severe, so 'p1' > 'p0' is true and inverts everything.
import { rank, precedes, compareOrder, sortByOrder } from '@frmwrkd/snag-core';
rank('p0') === 3; rank('p3') === 0;precedes(a, b) is a total order over
(rank desc, blast_radius desc, effort asc, uid asc). Wave assignment
(doc 02 §8 Stage 5) depends on ties being impossible — a plain severity
comparison emits no edge for the common p2-vs-p2 case and silently lets two
tasks edit the same file inside one wave.
3. The field is v, never version.
Including map.json, whose §3.1 example prints version. §5 is the authority;
RepoMapSchema uses v and src/map.ts records the deviation. The nested
framework.version is untouched — that is the framework's own version.
4. SnagConfigSchema.parse({}) is the shipped config.
Every default from doc 02 §5.6 is baked in, so there is no second copy to
drift. Two that matter: privacy.mask_all_inputs is true (rrweb ships it
false), and plan.cluster_distance 0.18 is a cosine DISTANCE while
plan.cross_route_similarity 0.93 is a cosine SIMILARITY.
5. Statuses are snake_case, and the transition table is data.
import { TASK_TRANSITIONS, canTransition, findTransition } from '@frmwrkd/snag-core';
canTransition('open', 'claimed'); // true
canTransition('open', 'resolved'); // false — resolved is Verify-only
findTransition('verifying', 'resolved')?.verify_only; // true
findTransition('in_progress', 'resolved')?.condition;
// 'plan.verify_on_resolve === false'snagd should enforce the table rather than re-deriving it. Note that last
row: when Verify is disabled, snag_resolve legally jumps
in_progress → resolved and records verified: false.
6. Zod strips unknown keys.
Schemas are in zod's default (strip) mode, not .strict() and not
.passthrough(). A key your producer writes that the schema does not know
about is dropped silently by parse. If you need a new field, add it here
first.
JSON Schema for MCP
import { taskJsonSchema, toInlineJsonSchema } from '@frmwrkd/snag-core';
server.registerTool('snag_get', { outputSchema: taskJsonSchema() }, handler);The output is draft-2020-12 and self-contained — no $ref, no $defs, so
it validates standalone. [email protected] has no 2020-12 target, so
src/jsonschema.ts generates draft-7 and rewrites tuple items →
prefixItems and draft-4 boolean exclusive bounds → numeric ones. All seven
exported schemas compile under Ajv 2020.
Use toJsonSchema(schema, name) instead if you want the $defs-wrapped
document.
Layout
| File | Contents |
|---|---|
| src/ids.ts | ULID minting, prefixes, branded id types, SNAG-<n> labels, task filenames |
| src/severity.ts | p0…p3, rank(), effort, precedes() / compareOrder() |
| src/anchor.ts | Anchor (tiers 0–4), data-snag parse/format |
| src/relation.ts | The nine relation types, origins, inverses |
| src/capture.ts | Capture and everything under it — context, evidence, derived, redaction |
| src/task.ts | Task, the status lifecycle table, files(T) / conflict_set |
| src/plan.ts | Plan, wave and merged_from invariant checks |
| src/thread.ts | Thread — a first-class object over ordered captures |
| src/config.ts | .snag/config.json, every default from §5.6 |
| src/map.ts | .snag/map.json |
| src/jsonschema.ts | draft-2020-12 generation |
Commands
pnpm --filter @frmwrkd/snag-core typecheck # src and tests
pnpm --filter @frmwrkd/snag-core test # vitest
pnpm --filter @frmwrkd/snag-core build # tsc → dist/test/schemas.test.ts transcribes every worked example from doc 02 §5.1, §5.3,
§5.4, §5.5, §5.6 and §3.1 and asserts each one parses and round-trips
unchanged. Its header comment documents the single mechanical deviation (the
spec's elided ULIDs and "…" placeholders are expanded) and the structural
assertion that proves nothing else changed.
