diff --git a/design/TESTSPEC_MODEL.md b/design/TESTSPEC_MODEL.md new file mode 100644 index 0000000..452c6cc --- /dev/null +++ b/design/TESTSPEC_MODEL.md @@ -0,0 +1,415 @@ +# Test Spec Model — Structure Analysis & a Type-Based Proposal + +**Status:** analysis / proposal (2026-06). Nothing here is implemented yet. +This document analyses the current `build/test/*.jsonic` model and proposes a +more coherent, *type-based* structure that uses aontu's unification features +instead of leaving the entry schema implicit in the TypeScript runner. + +> Scope note. The compiled `build/test/test.json` is the behavioural contract +> consumed by all 21 ports (see [`AGENTS.md`](../AGENTS.md)). **Any restructure +> must leave the emitted `test.json` byte-equivalent** — otherwise it is a +> behavioural change to every port, not a refactor. §5 (Migration) is built +> around that constraint. + + +## 1. How the model is built today + +The corpus is an aontu/`@voxgig/model` program, not hand-written JSON: + +``` +build/test/*.jsonic # aontu source (the model) + │ voxgig-model test/test.jsonic (build/package.json: "test-model") + ▼ +build/test/test.json # 390 KB compiled output — the actual contract + │ read by typescript/test/runner.ts → resolveSpec() + ▼ +every port's test runner # ts, py, go, rust, c, … all read test.json +``` + +`test.jsonic` is the entry point. It wires the per-function files together and +seeds one template: + +```jsonic +struct: &: { # '&' = a template unified into EVERY child of `struct` + name: key() # each function gets name = its own key + set: [] # …and a default empty `set` +} + +struct: minor: @"minor.jsonic" # '@' = import/include +struct: getpath: @"getpath.jsonic" +struct: validate: @"validate.jsonic" +… +primary: check: { DEF: { … }, basic: { set: [ … ] } } +``` + +So aontu *is* already doing type-ish work here — the single `struct.&` +template is a for-all-children rule that stamps `name`/`set` onto every +function. That mechanism is the lever this proposal pulls on; today it is +used exactly once. + + +## 2. The model has three nesting levels + +``` +struct +└── e.g. getpath, merge, validate, minor.isnode … (one per .jsonic, or one per minor fn) + └── e.g. basic, edge, operators, child … ({ set:[…], + ad-hoc fixtures }) + └── set[] list of test ENTRIES (the leaf units) +``` + +* **Function** — one file per public function (`getpath.jsonic`, `merge.jsonic`, + …), except `minor.jsonic` which packs ~30 small functions (`isnode`, `ismap`, + `clone`, `slice`, …) as siblings. +* **Group** — a named bucket of entries (`basic`, `relative`, `handler`, + `operators`, `child`, `exact`, …). A group is `{ set: [...] }`, sometimes with + extra sibling keys used as **fixtures** (see §3.3) and optionally a `DEF` + block (see §3.4). +* **Entry** — the leaf. A single map describing one call: inputs, expected + output, and metadata. + + +## 3. The entry "type" exists — but only in the runner + +There is no declared schema for an entry anywhere in the model. Its shape is +defined *operationally* by `typescript/test/runner.ts` (`resolveArgs`, +`checkResult`, `handleError`, `resolveEntry`). Reverse-engineered, an entry is: + +| Field | Role | Notes | +|----------|------|-------| +| `in` | input (single arg) | cloned, passed as the sole argument — the common case (1319 uses) | +| `args` | input (arg vector) | explicit positional args; mutually exclusive with `in`/`ctx` (15 uses) | +| `ctx` | input (context map) | wrapped via `makeContext`, gets `client`/`utility` attached (2 uses) | +| `out` | expectation | deep-equal against result; `null` → `__NULL__` sentinel (1192 uses) | +| `err` | expectation | `true` = any error, or string = substring/`/regex/` match of message (59 uses) | +| `match` | expectation | structural matcher: regex strings, `__UNDEF__`, `__EXISTS__` (15 uses) | +| `id` | metadata | doc anchor, format `"/#