Skip to content

Schemas for Task, Result, Effect and Disposition #19

Closed

Description

@don-petry

Part of the epic "Epic: slice 0, foundations. The repo holds the line, its context is tested, and the platform facts are recorded".

Why. Slice 0 ends when every schema the first slice uses has an example that validates. These four are what one Run carries.

Done when

  • Task, Result and Effect each have a schema, an example and at least one counter-example that must be rejected.
  • Disposition has the same.
  • A Result's reason is a code, never a sentence, and a counter-example shows it.
  • No schema uses a keyword the standard-library validator refuses.

Needs. Trials P-13 and P-17, which say what a Result's usage and a run record actually carry. Schemas written before them are guesses.

Design. §3.1, the catalogue; §3.5, §3.6, §3.8.

Activity

  1. changed the title [-]Schemas the first slice uses and the repo lacks: Task, Result, Effect, Disposition, Role manifest, pass record[/-] [+]Schemas for Task, Result, Effect and Disposition[/+] on Oct 11, 2026
  2. don-petry commented on Oct 18, 2026

    @don-petry
    ContributorAuthor

    Working rulings (overturnable in review)

    Solution Architect, delegated. On main at c094a8b (#170). Read with P-13's answer: CLI 2.1.302, origin/trials/p-13-cli at a0fa60b, and the redacted answer's fields. Also read trials/P-17.json, the working ruling on #161 (record 0034) and design §3.1, §3.3, §3.5 to §3.10 and §4.3.

    What this story decides. Four schemas and what each field may hold. Which fields the model writes and which the Factory writes. Three gaps in the design shape the answer.

    1. A Role's Finding has no surface yet when its Result is written. §3.7 derives a Finding's id from its source and its surface key. A Role's Finding gets its surface, a review thread or an annotation, only when apply posts it. §3.3 still reads a thread's category and severity "from that Lens's own Finding record, in its Run's result.json, joined by surface.key". So the Result cannot hold the key the join needs.
    2. Some fields in §3.5 and §3.8 claim an identity. A Disposition's by, its content_id, and a Finding's source are things a model would be asserting. Each is already known to the Factory from the Task.
    3. The validator has no oneOf, anyOf or if. P-13 proved only the keywords the validator implements (type, properties, required, additionalProperties, enum, const, pattern, items), handed to the CLI with no $ref. A list of Effects shaped {type, ...} can require no field of any one type.

    P-13 adds one fact. The answer's usage gave input_tokens 2, cache_creation_input_tokens 20475 and output_tokens 194. Its total_cost_usd was 0.167688, with num_turns, duration_ms, session_id and the model as the one key of modelUsage. Input tokens alone would count about one ten-thousandth of the input.

    1. The Result asks, and names no one. A Result holds what the Role judged, and only that. The Harness fills every field that says who the Role is, what it ran on, what Release ran it and what it cost. It takes them from the Task and from the CLI's own answer.

    • Written by the Engine: schema, run, status (completed, incomplete or refused only), reason, dispositions, findings, effects, coverage.
    • Filled by the Harness: release and usage. On an answer that does not validate, it writes the whole Result itself: status failed, reason result_invalid, empty lists, and usage. superseded is also written only by the Harness or apply.
    • No field in a Result holds a Role, an App, an actor, a content_id or a commit. The schema allows no such field, so a model cannot claim one.
    • Why: decision 0004 says a model "is able to ask, in a form a schema can refuse". A by: pr-review written by the model is the model saying who it is. apply already knows the Role from the Task. Taking it from the Result would let text the Run read decide who owns a Finding.

    2. The four schemas. Each one's own fields are below. "Code" means held by enum, const or an anchored pattern with no space. "Text" means any string. Only a field named body may be text, and no text reaches a decision (ruling 5).

    schemas/task.schema.json, task/1, a document. It is built by the Harness from the Decision, and no field comes from the model.

    Field Holds
    schema const task/1
    run ^r_[0-9A-Z]{6,26}$, the Run's identity, minted by the Controller before dispatch (0034)
    release ^factory/v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$
    role, intent ^[a-z][a-z-]*[a-z]$
    item {repo, type, number}, item as in decision.schema.json
    content_id ^sha256:[0-9a-f]{4,64}$
    findings Finding ids, ^f_[0-9a-f]{4,64}$
    texts {id, trust, source, body}: id ^[a-z]+:[A-Za-z0-9_-]+$; trust one of maintainer, role, advisory, outside; source a login, ^[A-Za-z0-9-]+(\[bot\])?$; body is text. It goes to the model as data and to nothing else
    limits turns, repair_turns, seconds: integers, at least 1 (repair_turns at least 0); cost_ceiling_usd: a number, at least 0
    tier standard or deep
    permitted_effects each {"$ref": "effect.schema.json#/$defs/type"}
    result_schema const result/1

    schemas/result.schema.json, result/1, a document. It is what run uploads as result.json.

    Field Holds
    schema, run, release As in the Task. They must equal the Task's, and apply checks that
    status completed, incomplete, failed, refused, superseded
    reason ^[a-z_]+$ or null: why a Run did not complete; null when it did
    dispositions items {"$ref": "disposition.schema.json"}
    findings raised Findings, $defs/raised, ruling 3
    effects {"$ref": "effect.schema.json"}
    coverage {"$ref": "coverage.schema.json"}; [] for an Intent that reviews nothing (0029)
    usage engine const claude-cli; engine_version ^[0-9]+\.[0-9]+\.[0-9]+$; model ^[a-z0-9.-]+$; session a UUID pattern; input_tokens, cache_creation_input_tokens, cache_read_input_tokens, output_tokens and turns, each an integer at least 0 or null; cost_usd and seconds, each a number at least 0 or null. All required. A count is null only where the CLI never answered, and then seconds is the measured duration (§3.10)

    bundle and preflight (§3.5) are left out. Slice 1's review Intent changes no code. They come with the author Intents in slice 2. A field added later is optional, or the major version goes up, because the control room reads old artifacts with new code.

    schemas/effect.schema.json, effect/1, a part. It is the Effects one Result asks for, as one list for each type. It is an object with additionalProperties: false, every property optional, each property an array of one strict shape. $defs/type is the enum of its property names. Every Effect has an id, ^e[0-9]{1,3}$, and create_check_run's external_id is built from it (§3.6).

    Type Fields
    submit_review id; comments: {finding, body}, where finding is a raised Finding's ref and body is text; body, text or null. It has no event and no commit. apply submits it as a comment on the commit the Decision admitted, so a Role's review never approves. Approval stays the act Run's (§3.10)
    create_check_run id; annotations: {finding, body}. The name and the conclusion are apply's, taken from the Task's Role and how the Run ended
    resolve_thread id; thread, ^PRRT_[A-Za-z0-9_-]+$
    comment id; on, ^(item|PRRT_[A-Za-z0-9_-]+)$; body, text

    The other types in §3.6 come with the Intents that need them: push_commits, open_pull_request, mark_ready, request_review, add_reaction. add_label, assign, enqueue_merge and dequeue_merge are the Controller's acts. They are never a Result's Effects, so effect/1 has no property for them. Why one list for each type: with no oneOf, it is the only shape in which "a resolve_thread with no thread" is something the schema can refuse. A Result that fails validation applies nothing (0004), so the schema is the guard. It also fixes the order in which apply writes.

    schemas/disposition.schema.json, disposition/1, a part. It has finding (a Finding id) and one half: proposal, {outcome, reason}, where outcome is fixed, declined, deferred or not_applicable; or decision, {result, reason}, where result is accepted or rejected. Each reason is optional, ^[a-z_]+$. There is no by and no content_id: the Run's Role and content are the Task's. This is §3.5's shape, which P-13 validated. "Exactly one half, and the half this Run's Role may make" depends on the Task's Role and the authority table, so apply checks it and the schema does not (ruling 7).

    3. A raised Finding, and where its record lives. In a Result, findings items are $defs/raised: {ref, location, category, severity, criterion, adopted_from}.

    • ref ^n[0-9]{1,3}$, local to the Result.
    • location as finding/1, its path ^[^\s]+$.
    • category and severity are finding/1's enums, copied.
    • criterion ^[A-Z]+-[0-9]+$ or null.
    • adopted_from as finding/1.
    • No id, source, surface or content_id.

    The one Effect that posts it names its ref: a submit_review comment for a Finding that gates, an annotation for one that does not (§4.3). After apply reads the posted surface back, it writes the full finding/1 into applied.json. The id is derived from the source and the surface key, as §3.7 says. The source comes from the Task's Role and its App, and content_id from the Task. The facts port joins a thread's category and severity from that record by surface.key. Why: the Finding record is written once, when all of it is known. finding/1 does not change. kernel/coverage.py's covered reads only criterion, which a raised Finding has under the same name.

    4. One definition, referenced by file. tools/jsonschema_lite.py's $ref resolves <kind>.schema.json and <kind>.schema.json#/$defs/<name>. It looks them up in a mapping of kind to schema that the caller passes: validate(value, schema, ..., schemas=None). The referenced schema becomes the root for its own #/$defs references. A reference with no mapping, or to a kind the mapping lacks, raises SchemaError: it fails closed, as an unknown keyword does. tools/check_examples.py passes every schema in schemas/. The validator still reads no file. Why: decision 0029 asks for the Result to "take coverage/1 by reference". #28's pass record must carry facts/1, at 627 lines, which a copy would double. The Harness hands the CLI a copy with the references resolved, which waits on harness/. That keeps P-13's proven form, no $ref, in front of the model.

    5. Only a body is text. In tools/check_examples.py, CODES_ONLY names task, result, effect, disposition and run-name. In those schemas, every node whose type admits a string has an enum, a const or a pattern anchored at both ends, unless it is a property named body. #28 adds its new schemas to the set. Why: this is the rule "no text from outside reaches a decision", held structurally for what one Run carries. A body goes out to GitHub or in to the model. The facts port never reads one back (§3.3).

    6. The run-name, for #161. #161 left the pattern to this story. schemas/run-name.schema.json, a part, is a string schema:
    ^sf [A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[1-9][0-9]* [a-z][a-z-]*[a-z] [a-z][a-z-]*[a-z] sha256:[0-9a-f]{4,64} r_[0-9A-Z]{6,26}$.
    The fields are repo and item, Intent, Role, the full content_id and run, each copied from the Task, with no c: or r: tag. Both values carry their own prefix. Why the full content id: the records port fills the run record's content_id from the name (0034), and the Kernel compares it whole with the facts'. A shortened one would never match. GitHub's limit on a run-name's length has not been tried. With a 64-digit content id and a 26-character run id it is about 160 characters, and P-17's were shorter. That is recorded as unverified, not a blocker.

    7. Examples and counter-examples. Each counter-example is wrong in exactly one way: the validator must give exactly one error.

    • Examples:
      • task.json: §3.5's, adjudicate-findings.
      • result.json: a review-change that raises a major Finding on a thread and a minor one as an annotation, with coverage. Its model is not a full model id, which tools/check_repo.py keeps to fixtures.
      • result.adjudication.json: §3.5's, two Dispositions and a resolve_thread.
      • result.failed.json: written by the Harness, with null counts.
      • effect.json: all four types.
      • disposition.json: an owner's decision. disposition.proposal.json: an author's proposal.
      • run-name.json.
    • invalid/task.: text-of-no-trust-class, with trust trusted; a-permitted-effect-a-result-cannot-request, with enqueue_merge; a-run-id-in-another-form, 01JA7Q; no-release.
    • invalid/result.:
      • sentence-as-reason: the issue's third criterion.
      • carries-a-commit-hash.
      • a-finding-that-names-its-source.
      • a-status-the-design-does-not-know.
      • usage-without-the-tokens-written-to-the-cache: drop cache_creation_input_tokens alone.
    • invalid/effect.: an-act-of-the-controller, enqueue_merge; a-review-that-approves, an event on submit_review; a-thread-named-in-words; a-check-run-with-a-conclusion.
    • invalid/disposition.: sentence-as-reason; names-who-decided, a by; a-result-in-words; an-outcome-the-author-cannot-propose, a proposal of accepted.
    • invalid/run-name.: the-workflows-own-name, the workflow's name: alone, as P-17 saw it listed; a-run-id-in-another-form, §3.10's old r:01JA7Q.

    "Neither half, or both" cannot be a counter-example, because the schema accepts it. It is a Boundary row waiting on harness/ (ruling 9).

    8. Tests and mutants, written first.

    • In tests/test_tools.py:
      • test_a_reference_to_another_schema_is_followed: valid and invalid through a file reference and through a #/$defs fragment. A referenced schema's own #/$defs reference resolves against that schema, not the referrer.
      • test_a_reference_to_a_schema_not_given_is_refused: SchemaError, with no mapping, and with a kind the mapping lacks.
      • test_a_string_that_is_neither_a_code_nor_a_body_is_caught: a bare string, an unanchored pattern, and a body that passes.
      • test_each_effect_type_is_requested_by_its_own_list: $defs/type's enum equals properties' keys.
    • tools/check_authority.py also holds the Result's raised-Finding category and severity enums to finding/1's, as it holds the facts' thread categories. Test: test_a_raised_finding_with_a_category_nobody_owns_is_caught.
    • Mutants, appended, each with text that appears once:
      • "a referenced schema is read against the referrer's root": the root is not switched. The first test kills it.
      • "a reference to a schema not given accepts anything". The second test kills it.
      • "a free string in a Run's schema passes": the CODES_ONLY condition becomes False. The third test kills it.
      • "a raised Finding may carry a category nobody owns". The check_authority test kills it.

    9. Record and design. A rule changes: where a Role's Finding record lives, and that a Result names no one. So this takes a new record, 0035, "A Result asks and names no one; apply writes the Finding it posted". It merges after 0033 and 0034. Its Decision is rulings 1, 3, 4 and 6.

    Its Boundaries name:

    • the counter-examples above, and the four tests;
    • waiting on harness/:
      • the Harness hands the CLI result/1 with its references resolved and the fields the Harness fills removed, status narrowed to completed, incomplete and refused, and effects narrowed to the Task's permitted_effects;
      • the Harness fills release and usage from the CLI's own answer, and writes a failed Result, result_invalid, for an answer that does not validate;
      • apply refuses a Result whose run or release is not the Task's, an Effect type the Task does not permit, an Effect id or a Finding ref given twice, a raised Finding posted other than exactly once, or one that does not gate posted on a thread;
      • apply refuses a Disposition with neither half or both, or a half its Role may not make: an author proposes and the owner of the category decides (config/authority.json);
      • apply submits a Role's review as a comment on the admitted commit, never an approval;
      • apply writes each posted Finding's finding/1 into applied.json, its source from the Task;
    • waiting on adapters/: the facts port joins a thread's category and severity from applied.json; the Controller names a Run by run-name/1, and the records port parses it.

    Rows elsewhere:

    • 0029's row "The Result's schema takes coverage/1 by reference …" changes its Check cell to schemas/result.schema.json, tools/check_examples.py.
    • 0004 gains a row: "A Result's schema has no field for a commit, a Role, an actor or a content id", naming invalid/result.carries-a-commit-hash.json and invalid/disposition.names-who-decided.json.

    Design edits, in the same change:

    • §3.3, the threads row: "in its Run's result.json" becomes "in its Run's applied.json".
    • §3.5: the Result example and who writes what. usage gains its fields, from P-13.
    • §3.6: Effects listed by type, and which ones a Result may ask for.
    • §3.7: a Role raises a Finding in its Result, and apply writes its record.
    • §3.8: a Disposition in a Result has no by and no content_id.
    • §3.10: the run-name's form, and applied.json holds the posted Findings.

    10. Out of scope.

    Can #19 and #28 run in parallel? No, not whole. #28 builds on this story: its pass record $refs facts.schema.json through ruling 4. Its role manifest refs effect.schema.json#/$defs/type. Its envelope check must classify task, result, effect, disposition and run-name.

    Needs the Maintainer? None open. The one point below was answered on 2026-10-18: option (a) taken. applied.json and outcome.json are #28's fourth criterion, and its rulings define them. Ruling 3's applied.json is that schema.

    The point as it was put: applied.json and outcome.json are artifacts of every slice-1 Run: the Kernel reads outcome.json, and with ruling 3 the facts port reads applied.json. Slice 0's exit asks for an example of every schema slice 1 uses. Neither has a story, and #19 already has four acceptance criteria.

    Recommend (a).

  3. don-petry commented on Oct 18, 2026

    @don-petry
    ContributorAuthor

    Decision number, updated: this ruling's record is 0036, not 0035. #165 (PR #176) needs a record of its own and takes 0034, the next free number, because it merges sooner. Records merge in number order: 0032 #175, 0033 P-13, 0034 #165, 0035 #161, 0036 #19, 0037 #28. #26 takes a number when it is built.

  4. don-petry commented on Oct 19, 2026

    @don-petry
    ContributorAuthor

    Decision number, updated: this story's record is 0037 (it was 0036). #26's P-39 reach trial passed and is ready to build now, so it takes 0036, ahead of the stories still being ruled or built. Order: 0034 #165 (PR #176), 0035 #161, 0036 #26, 0037 #19, 0038 #28, 0039 #174 PR B.

  5. don-petry commented on Oct 19, 2026

    @don-petry
    ContributorAuthor

    Decision numbers change rule (Maintainer, 2026-10-19). A decision record takes its number when its pull request is clean and green and about to be enqueued, as the next free number. It is no longer fixed when the rulings are posted. The number posted on this issue is provisional. The builder renames the record at enqueue if an earlier PR took that number. CI's gapless decision check still holds the order. Boundary rows in other records name tests, never numbers, so nothing else moves. This ends the renumbering, and a story with a record no longer waits behind an unrelated story that holds a lower number.

  6. don-petry commented on Oct 19, 2026

    @don-petry
    ContributorAuthor

    Decision numbers are taken at enqueue (the Maintainer, 2026-10-19, on the plan review's recommendation). A record takes the next free number when its PR is clean and green and about to be enqueued, not when its ruling is posted. Rulings say "the next free number at enqueue". Boundary rows in other records name tests, never numbers, so nothing else moves. The order posted on 2026-10-19 (0035 #161, 0036 #26, 0037 #19, 0038 #28, 0039 #174 PR B) is the expected order, not a reservation: #161 keeps 0035 as the next to enqueue, and each of the others takes the next free number in the order it is actually enqueued. A rows-only PR still merges any time.

  7. don-petry commented on Oct 19, 2026

    @don-petry
    ContributorAuthor

    Dependency (plan review, 2026-10-19). This story waits on #161's record being on main, not on its code: nothing here reads unidentified_runs (that is #28's context table). Start now in lane s; cite trials/P-13.json, which is on main (#177).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions