Schemas: Task, Result, Effect and Disposition (#19)
Why
Slice 1's Runs need the contract of decision 0004 as schemas a validator can refuse: the Task the Harness builds, the Result the Engine returns, the Effects a Result may ask for and the Disposition on a Finding. The working rulings on the issue (the architect's, with the Maintainer's comments) are the spec; where the issue or the design said otherwise, the rulings were followed and the design amended in the same change.
What each schema holds, and what the model may not write
| Schema | Holds | The model may not write |
|---|---|---|
task/1 | run, release, role, intent, item, content id, Finding ids, texts ({id, trust, source, body}), limits, tier, permitted Effects, result schema. Built by the Harness from the Decision | Anything: no field comes from the model. Only a text's body is text, and it goes to the model as data |
result/1 | status, reason (a code or null), dispositions, raised Findings ({ref, location, category, severity, criterion, adopted_from}), effects, coverage (by reference to coverage/1), usage | release and usage (the Harness fills them); failed and superseded (the Harness or apply); a Role, an App, an actor, a content_id, a commit, or a Finding's id, source or surface: the schema has no such field |
effect/1 | One list for each type: submit_review (comments by raised-Finding ref, body), create_check_run (annotations), resolve_thread (thread id), comment (on the item or a thread). $defs/type is the enum the Task's permitted_effects uses | A review event or commit (a Role's review never approves); a check run's name or conclusion; add_label, assign, enqueue_merge or dequeue_merge, which are the Controller's acts |
disposition/1 | finding and one half: proposal {outcome, reason} or decision {result, reason} | by or content_id: both are the Task's. The schema accepts neither half and both; apply checks that (waiting on harness/) |
run-name/1 | sf <repo>#<n> <intent> <role> <content_id> <run>, the whole content id and the Run id with their own prefixes, as #161 left it to this story |
Held by tools/check_examples.py: in these five schemas every string is a code (enum, const or a pattern anchored at both ends) unless the property is named body, through references too. tools/jsonschema_lite.py now resolves <kind>.schema.json and <kind>.schema.json#/$defs/<name> in a mapping the caller passes, the referenced schema being the root of its own references; a reference with no mapping is a SchemaError, and the validator still reads no file. tools/check_authority.py holds a raised Finding's category and severity to finding/1's.
Where the rulings depart from the issue and the design, and the choices made
- A Role's Finding record lives in its Run's
applied.json, written byapplyafter it posts the surface, not inresult.json(design §3.3, §3.7): the Result cannot hold a surface key that does not exist yet. - A Disposition in a Result has no
byand nocontent_id(§3.8); a Result has nobundleorpreflightuntil the author Intents (§3.5). - The run-name carries the whole content id and the Run id as
sha256:…andr_…, notc:andr:(§3.10). GitHub's bound on a run-name's length is unverified (about 160 characters at most). - Shapes the rulings left open, taken in closed form and recorded in the decision:
usagerequires every field;modelandsessionare strings even for a Run the CLI never answered, because the Harness names both before the call;effectsin a failed Result is{}; asubmit_reviewrequiresbody, text or null. - The decision record is written as 0038, the next free number now; it is renamed at enqueue if another PR takes it first.
Reviews
A fresh-context Claude review, a security pass (subagents) and Muse Wonder Full read the first head, 11071c9; the Kernel and the simulator are untouched, so AGY was not run. Every finding was verified and folded into one fix round, fecd592:
- The first head failed the context check (the record named the Run's artifacts as paths; three rules over the line limit). Fixed.
- "Anchored at both ends" was judged by a pattern's first and last characters, so
^a$|b$and^a\$passed as codes (Claude, Muse, security). A pattern is now anchored by its structure, and each anchor, the alternative and the escaped dollar have a mutant; the test was widened so a single-anchor mutant dies (Muse's major). - A
$refcycle recursed without bound, and a keyword beside a$refwas silently dropped (Muse, security). Both are aSchemaErrornow, with tests and mutants. - The codes-only walk passed over a reference it could not follow (Muse). It reports one now.
usage.secondswas nullable against the record's closed form (Muse). It is a number now; a counter-example holds it.location.pathand an adopted surface's key admitted control characters; the Task'sitem.repoadmitted what the run-name refuses (security). Tightened.- Rows added for
applyand the Harness, waiting onharness/(security, Muse): an Effect's thread is on the Task's item; a Disposition is on a listed Finding, given once; status and reason are coherent; a text's trust class comes from the facts' actor.
CodeRabbit then found that the codes-only walk passed over a reference cycle, or a keyword beside a $ref, in a branch no example takes (the validator reads a reference only where a value reaches it); f561b9a holds both, with a test case and a mutant each. The first CI run put three mutant shards past their 25 minutes: a mutant that names no test runs the whole suite, with the simulation, before it is read as killed, and the thirteen named none. d9be3c8 names each one's test, as the hook mutants do, and makes the codes-only test ask the walk in memory with one front-door run. Each of the thirteen new mutants was applied by hand to a scratch copy and its named test seen to fail; CI's shards are the final word.
Verify
make verify passed on a fresh git archive export with PATH=/usr/bin:/bin SIM_SEEDS=40.
Closes #19
🤖 Generated with Claude Code