Contracts
operations-and-schemas.md
Contract: the planned file set of contracts/mission-status/ for 1.1
This is the planning contract between the work packages of Mission mission-status-contract-1-1-01M42XJC: the exact set of files the 1.1 slice (a working name, not a version) adds or edits under contracts/mission-status/, the schema sketches, the status and code pairing, the examples and the provisional elements. The product contract is the OpenAPI module itself once built. This note fixes names and shapes so every work package works from one agreed set; an implementer may refine a description but must keep every property, name and rule listed here, or report the change in the hand-off report; the orchestrator records it in tracer-design-decisions.md (a work package never writes under kitty-specs/: plan, "Record-keeping and write scope"). No block below is a YAML example: sketches use plain text fences so the round-trip gate of the corpus (tests/contract/test_example_round_trip.py) has nothing to collect.
Operations (three, all GET, all tagged Missions)
The new operations reuse the existing Missions tag. A new tag would change the root tags list and make the bundle comparison of FR-017 observable 3 (b) unequal (research R-7).
| operationId | Path key (root map) | Path file | Parameters | Responses |
|---|---|---|---|---|
getWorkPackageDetail | /missions/{missionId}/work-packages/{wpId}/detail | paths/missions_missionId_work-packages_wpId_detail.yaml | MissionId, WpId (both existing) | 200 WorkPackageDetail; 404 WorkPackageDetailNotFound; 500 WorkPackageDetailUnreadable; default shared Problem |
listArtifacts | /missions/{missionId}/artifacts | paths/missions_missionId_artifacts.yaml | MissionId | 200 ArtifactListing; 404 ArtifactNotFound; 500 ArtifactListingUnreadable; default shared Problem |
getArtifactContent | /missions/{missionId}/artifacts/content | paths/missions_missionId_artifacts_content.yaml | MissionId, ArtifactPath (new, query path, required) | 200 ArtifactContent; 400 ArtifactPathRefused; 404 ArtifactNotFound; 413 ArtifactTooLarge; 415 ArtifactNotText; 422 ArtifactSecretRefused; 500 ArtifactUnreadable; default shared Problem |
The three file names were computed with contract_resolver.path_file_name. The detail path is a new path item (a sub-resource), so no existing path file changes. No operation declares a security scheme (as in v1). The getArtifactContent description states the order of decision (400, 404, 413, 500, 415, 422), the 262,144-byte cap, that path is decoded once at the HTTP layer, that a symlink is never followed and that the two root status records are not artifacts. The 422 response description and the readable property description list the credential kinds by name: a GitHub token (ghp, gho, ghu, ghs, ghr prefixes and the fine-grained github_pat prefix), an AWS access key id (AKIA and ASIA prefixes) and the header line of a PEM private key (a test compares these words with SECRET_PATTERNS, FR-019).
New files (all under contracts/mission-status/)
Parameters: parameters/ArtifactPath.yaml (query parameter path, required, schema ArtifactPath).
Schemas, one file each, title equal to the file stem, closed unless stated (additionalProperties: false), every property carrying exactly one x-source or x-derived:
| Schema | Properties and rules | Provisional (AD-14) |
|---|---|---|
WorkPackageDetail | required: missionId (MissionId), wpId (WpId), subtasks (array of Subtask), dependencies (array of DependencyRef), reviewCycles (array of ReviewCycle), workspace (Workspace), ownedFiles (array of OwnedFile), artifactReferences (ArtifactReferences); every array present, possibly empty; embeds no WorkPackage | reviewCycles, workspace |
Subtask | id (RelativeReference), title (string, minLength 1, or null; human text), statusLane (StatusLane or null) | none |
DependencyRef | wpId (WpId), title (string, minLength 1; never empty; the id as fallback), statusLane (StatusLane or null) | none |
ReviewCycle | cycleNumber (integer, minimum 1), reviewedAt (RFC 3339 date-time with offset, or null), reviewer (ActorHandle, already nullable), verdict (ReviewVerdict or null), feedbackReference (string matching the pointer pattern, or null), artifactPath (ArtifactPath or null) | the schema |
Workspace | laneId (string, strict, or null), laneBranch (string matching the branch pattern, or null), planningBranch (string or null), worktreePresent (boolean) | the schema |
OwnedFile | pattern (RelativeReference), isGlob (boolean), changeState (ChangeState) | none |
ChangeState | enum changed, unchanged, unknown; description states the host dependence and the derivation | none (stable) |
ArtifactReferences | prompt and spec, each ArtifactReference or null, both required | none |
ArtifactReference | path (ArtifactPath), kind (ArtifactKind) | via kind |
ArtifactPath | string, minLength: 1, maxLength: 512, the pattern below | none |
ArtifactKind | enum of twelve values: review_cycle, work_package_prompt, spec, plan, tasks, data_model, quickstart, analysis_report, research, contract, checklist, other; the classifier order is the description | the schema |
ArtifactEntry | path (ArtifactPath), kind (ArtifactKind), sizeBytes (integer, minimum 0), modifiedAt (RFC 3339 UTC with Z, whole seconds), readable (boolean; the description lists the credential kinds) | via kind |
ArtifactListing | missionId, entries (array of ArtifactEntry, maxItems: 1000), truncated (boolean) | truncated |
ArtifactContent | all required: path (ArtifactPath), kind, mediaType (string), encoding (const: utf-8), sizeBytes, redacted (boolean), content (string, may be empty) | redacted, via kind |
ArtifactRefusalCode | enum of seven values (invalid_artifact_path, not_found, artifact_too_large, artifact_not_text, artifact_secret, artifact_unreadable, artifact_listing_unreadable) | the schema |
ArtifactRefusal | the shared Problem plus required code ($ref ArtifactRefusalCode) and one if/then per code pinning status (400, 404, 413, 415, 422, 500, 500), built like StreamRefusal; no field carries file content, a host path or the Mission directory | code |
WorkPackageDetailRefusalCode | enum not_found, source_unreadable | the schema |
WorkPackageDetailRefusal | the shared Problem plus required code ($ref WorkPackageDetailRefusalCode) and the status rule (404 for not_found, 500 for source_unreadable) | code |
ArtifactPath pattern (ECMA-262 compatible, written with the hex escape, not a unicode escape: a unicode escape typed into some editing tools is decoded into a raw NUL character):
^(?!~/)(?![A-Za-z]:)(?!\.\.?(?:/|$))[^/\\\x00]+(?:/(?!\.\.?(?:/|$))[^/\\\x00]+)*$
Responses (responses/, each application/problem+json): ArtifactPathRefused (400), ArtifactNotFound (404, used by listArtifacts and getArtifactContent), ArtifactTooLarge (413), ArtifactNotText (415), ArtifactSecretRefused (422), ArtifactUnreadable (500, content), ArtifactListingUnreadable (500, listing) all with schema ArtifactRefusal; WorkPackageDetailNotFound (404) and WorkPackageDetailUnreadable (500) with schema WorkPackageDetailRefusal. The status of each is the key it is mounted under; the schema pins the code-to-status pairing.
Environment-dependent and derived descriptions (FR-016, FR-019)
Workspace.worktreePresent, OwnedFile.changeState and ArtifactEntry.modifiedAt say in their descriptions that they depend on the serving host. ReviewCycle's description says a cycle held only on a coordination surface is not shown and that [] means none was found on the primary planning surface. OwnedFile.changeState is x-derived with the full rule of the data model and inputs git_merge_base, changed_paths and is_glob_pattern; Subtask.title and DependencyRef.title are x-derived with the extraction rule; ArtifactReferences and the entries are x-derived with the eligibility rule. Citation inputs name symbols (never line numbers); the symbols were all resolved (research R-12).
Examples (21 files under examples/, named <Schema>.<case>.yaml) and the in-test required cases
| Example | Covers |
|---|---|
WorkPackageDetail.populated | titles and lanes on subtasks and dependencies; two review cycles (one approved, one changes_requested); a lane worktree present; owned files changed and unchanged and one glob; both artifact references |
WorkPackageDetail.no-cycles | reviewCycles: [], a dependency with a null lane, a subtask with a null title |
WorkPackageDetail.unknown-change-state | owned files unknown (no worktree) including a brace pattern; worktreePresent: false |
WorkPackageDetail.null-workspace | laneId, laneBranch, planningBranch all null; empty subtasks, dependencies, ownedFiles; both references null |
WorkPackageDetail.planning-lane | laneId: lane-planning, laneBranch: null, a planningBranch |
WorkPackageDetail.unparseable-cycle | a cycle with null reviewedAt, null reviewer, null verdict, null feedbackReference and a null artifactPath |
ArtifactListing.populated | entries across several kinds with one readable: false entry |
ArtifactListing.truncated | truncated: true (a short list; a comment states a real truncated listing holds exactly 1000 entries) |
ArtifactContent.markdown | redacted: false |
ArtifactContent.redacted | redacted: true, [path] and [email] in content |
ArtifactContent.json | application/json |
ArtifactContent.empty | a zero-byte file: content: "", sizeBytes: 0 |
ArtifactRefusal.<code> (7: invalid-artifact-path, not-found, artifact-too-large, artifact-not-text, artifact-secret, artifact-unreadable, artifact-listing-unreadable) | each code with its pinned status |
WorkPackageDetailRefusal.<code> (2: not-found, source-unreadable) | each code with its pinned status |
Small leaf schemas (Subtask, DependencyRef, ReviewCycle, Workspace, OwnedFile, ArtifactReference, ArtifactEntry, the enums) carry short inline examples so that every new schema has examples (FR-019). The spec's required cases are enforced by extending tests/contract/test_mission_status_examples.py (an in-test manifest: every refusal code has an example; both redacted values occur; both readable values occur; truncated: true occurs; each ChangeState value occurs; a cycle with a null feedbackReference and one with a null reviewedAt occur), not by an entry in contracts/tools/fixtures/example_check/required_examples.json (OD-2). Every example is leak-free; a path in an example satisfies the ArtifactPath pattern; no example holds an absolute path, a host path, an e-mail address, a credential or an unredacted content.
Edits to existing files (the whole list; everything else under contracts/mission-status/ and contracts/_shared/ stays byte-identical)
openapi.yaml: info.version stays 1.0.0-SNAPSHOT (no new version, operator ruling 2026-10-05) and three path keys are added to the root map (no new tag, no change to servers or info otherwise). _index.yaml of schemas/, parameters/, responses/ and examples/: new entries only. CHANGELOG.md: the additions merge into the existing ## 1.0.0-SNAPSHOT section (no new heading), with ### Added (the three operations; the new schemas, parameter and responses; the two behaviours of the reads: invalid UTF-8 is a 415 and the root status records are no artifacts; a one-line reference to #5533 for the route mapping, naming the five route families covered and stating that the dossier overview and the snapshot export are not covered), ### Changed ("Nothing."), ### Removed ("Nothing."), ### Provisional (below) and ### Deferred to the next major version (the four v1 gaps, each with one line of reason: per-Mission staleness on MissionOverview, an actor on WorkPackageSummary, the project branch, the lane weights behind weightedPercentage; each would add a property to an existing response, which the breaking-change check treats as breaking).
The Provisional section of the 1.0.0-SNAPSHOT section names, as whole words: reviewCycles, workspace, ReviewCycle, Workspace, ArtifactKind, truncated, redacted, code, ArtifactRefusalCode, WorkPackageDetailRefusalCode, ArtifactRefusal, WorkPackageDetailRefusal, and kind (the properties that reference a provisional schema are counted by provisional_check under their own name; the final list is the output of provisional_check.py run on the finished tree, never a guess).
Order of work for the shared files
openapi.yaml, the four _index.yaml files and CHANGELOG.md are written by the contract work packages in sequence (artifact reads first, then the detail, then the CHANGELOG once the provisional list is final), never in parallel (plan, Implementation Concern Map).
tool-extension-and-reader.md
Contract: the tool extension (FR-015), the reference reader and the test map
Internal contract between the work packages of Mission mission-status-contract-1-1-01M42XJC. It fixes names, signatures and stable codes so every work package works from one agreed set. An implementer may refine a name but keeps every rule here, or report the change in the hand-off report; the orchestrator records it in tracer-design-decisions.md (a work package never writes under kitty-specs/: plan, "Record-keeping and write scope"). Plain text fences only (the corpus round-trip gate collects yaml fences of this directory).
A. The four tool files of FR-015 (the whole edit set under contracts/tools/)
fixture_builder.py
STRICT_FIELDSgainsid,laneId,laneBranch,planningBranch,pattern,feedbackReference,reviewer,kind,mediaType,changeState.titlestays out (human text). Measured:leak_scan.py --root contractsstill exits 0 on every existing file (research R-8).KINDSand_plantgain 25 kinds, every plant built from fragments at run time (no leaking text in source), each next to a clean control on the same root, as today. 22 are red-first (undetected by the unchanged scanner: run on the planning base for the ten strict-name plants, which need the newSTRICT_FIELDSentries, and for all twelve artifact-path forms, none of which the human scan reports) and 3 are regression controls (see the table; they pass on the planning base and are not counted as red-first):
| Kind family | Kinds | Planted into | Expected code |
|---|---|---|---|
| strict name (10) | strict-id, strict-laneId, strict-laneBranch, strict-planningBranch, strict-pattern, strict-feedbackReference, strict-reviewer, strict-kind, strict-mediaType, strict-changeState | a value that is a host path only in the strict class (a leading-tilde path) under that key; the control holds a clean value under the same key | HOST_PATH |
| artifact path (13) | artifact-path-empty, -too-long (513 characters), -absolute, -tilde, -drive-letter, -backslash, -nul, -line-break, -empty-segment, -trailing-slash, -dot-segment, -dotdot-segment, and artifact-path-reference (the same malformation under the key artifactPath) | entries: [{path: <malformed>}] (the key artifactPath for the last) | ARTIFACT_PATH_MALFORMED |
| content and title (3), regression controls, not red-first | content-host-path-and-email, credential-in-content, credential-in-title | a key content holding a host path and an e-mail address; a key content holding a token; a key title holding a token | HOST_PATH and EMAIL; SECRET; SECRET |
Why the third family is labelled this way (research R-15 measures which kinds the unchanged scanner already detects, which gives the split of 22 red-first kinds and 3 regression controls): a key named content or title is already human text, so a host path, an e-mail address or a token under it is found today by both passes of the unchanged scanner (run on the planning base: EMAIL and HOST_PATH, SECRET, SECRET). These kinds would still pass if the FR-015 edit were reverted. They are kept because the artifact-path edit changes how path and artifactPath are judged, and a wrong edit (one that applied the new class to every string, or masked a whole line) would blind the human scan for these keys: the three kinds fail then, so they guard the edit. The per-endpoint planted pair that is red-first lives at reader level (below).
The control of every artifact-path kind holds the legitimate odd names of AD-19: a name with a home/<x>/ directory segment, one with a space, one with an accented letter (all verified clean in both passes on the planning base), and a name with an at sign built at run time (OD-4 is decided: the scoped text pass). No committed file under contracts/, examples and committed controls included, holds an at-sign path (a YAML file cannot be built at run time); under the option not chosen (OD-4 Y) the same would hold for every file, because the raw-text pass reports it as EMAIL (verified: the e-mail pattern matches a retina-style image name, a stem, an at sign, a scale factor and an extension; a scan fixture that needs one builds it at run time from fragments); the at-sign name is proved by the run-time-built scan tests (WP02) and at reader level. A scan that flags a control name fails its own control (CONTROL_FLAGGED).
- Plant shape (changes
fixture_builder.pybeyond the kind list). Today_plantreturns one(field, value, codes)andbuildwrites a flat mapping, so it cannot put a value underentries[0].pathor give each kind its own control. The new contract:_plant(kind)returns aPlant(fragment, control_fragment, field, codes);fragmentis a nested document fragment deep-merged into the planted document (a top-levelid: <value>andentries: [{path: <malformed>}]are both expressible),control_fragmentis the clean fragment deep-merged into the control document of that kind (default empty; each artifact-path kind carries the odd names above),fieldis the dotted key path reported inBuiltFixture.field; mappings merge recursively and lists are replaced, not concatenated.buildwrites control = base control merged withcontrol_fragmentand planted = base control merged withfragment(theforbidden-propertykind keeps its empty base).BuiltFixturekeeps its four fields.run_negative_cases.build_leakis unchanged by design (it callsbuildand unlinks only the planted file, so the control stays a complete clean file);test_run_negative_cases.pymoves only through the manifest length. Tests that move:tests/contract/test_fixture_builder.py(the exactKINDSset, 33 kinds: the eight existing plus 25) and a new test that the eight existing kinds still build byte-identically: the two files of each old kind hashed withblake2b(hashlib.sha256is banned) against digests pinned at the start of IC-02 from the unmodified builder (this test passes before and after the edit by design, so it is not a red-first claim). The unchangedrun_self_test()runs every kind on everyleak_scaninvocation, so a blind scanner fails loudly (PLANTED_NOT_DETECTED). - The module docstring changes with the kinds. Reader-level planted pairs (red-first, outside
contracts/tools/):payload_leaksover a builtArtifactContentwhose body was left unredacted must reportHOST_PATHandEMAIL, and over a built detail whose subtask or dependency title holds a credential it must reportSECRET; each fails if the reader stops redacting or withholding (artifacts and detail test modules; clean twins pass).
leak_scan.py
ARTIFACT_PATH_KEYS = frozenset({"path", "artifactPath"}). In_Scanner.walk, a string under one of those keys whose ancestors hold nox-sourceorx-derivedkey is checked bymalformed_artifact_pathinstead of the strict and human host-path and e-mail patterns of the structured pass (AD-19).scan_textis a second, independent pass: it runs the human patterns (host path, e-mail andSECRET) over every non-blank raw line of every file. Under OD-4 Xscan_texttakes a second argument, the text with the artifact-path value spans blanked:artifact_path_spans(text)composes the YAML (yaml.compose, scalar start and end character indexes) and returns the span of each scalar value of a key inARTIFACT_PATH_KEYSreached without passing through anx-sourceorx-derivedkey;mask_spansblanks those characters (newlines kept, so line numbers hold); host-path and e-mail findings come from the blanked text,SECRETfindings from the original line, so a token in a path value is still reported. JSON files and a YAML file that does not parse are scanned unmasked. A test pair proves the mask is bounded: a flow mapping line holding an at-signpathand atitlewith an address reports the title and not the path; anx-sourcepath holding an address is reported; a token in a path value is reported; a contract example holding an at-sign path passes the whole scan; the module docstring ("nothing is exempt") is rewritten to name the masked span. Under OD-4 Yscan_textis unchanged and no contract file holds an at-sign path. The prototype of X was run on a scratch copy of the tools (research R-14). Underx-sourceandx-derivedthe keypathis a repository path (226 occurrences) and keeps the human scan.malformed_artifact_path(path) -> str | Noneis the one definition of the predicate on the tooling side: it returns a stable reason (empty,too_long,absolute,tilde,drive_letter,backslash,nul,line_break(any characterstr.splitlinestreats as a line break: LF, CR, VT, FF, the three information separators FS, GS and RS, NEL, U+2028, U+2029; added by WP02's review, orchestrator 2026-10-04: a line break must make the path malformed so the artifact-path mask can never hide a leak on a second line),empty_segment,trailing_slash,dot_segment,dotdot_segment) orNone. The reference reader andpayload_leaksimport it through the existing loader (tests importing tools is the allowed direction); it lives here, inside the slice file set, rather than inleak_patterns.py, which is outside it. Three-way agreement is tested: this function, a literal restatement of the spec's predicate in the test, and theArtifactPathschema pattern over the same table of cases.- New finding code
ARTIFACT_PATH_MALFORMED; the last line gainsvalues_artifact_path=Naftervalues_all=N(the existing last-line assertions oftest_leak_scan.pyare prefix and key-presence checks and keep passing; a new assertion covers the added key). No floor on the class incheck()for every root (fixture roots hold no such key); the real-contract test asserts the count is positive once the examples exist.
negative_cases.json
One case per new kind, {"id": "leak-scan-<kind>", "tool": "leak_scan.py", "build": "leak:<kind>", "plant": {"args": ["--root", "{root}"], "code": <code>}, "control": {"args": ["--root", "{root}"]}}, appended in one block. Cases that need a committed fixture are not allowed (no new file under contracts/tools/fixtures/); every other planted negative of this Mission is an in-test case under tests/contract/.
enum_pins.json
Under mission-status: ChangeState (3 values), ArtifactKind (12), ArtifactRefusalCode (7), WorkPackageDetailRefusalCode (2). enum_pin_check fails ENUM_UNREADABLE for a pinned title that is not in the module, so each pin lands in the same work package as its schema. tests/contract/test_enum_pin_check.py changes its pinned line counts: enums=3 values=19 to enums=7 values=43.
B. Reader modules (test tree, tests/contract/, none is a test module)
| Module | Holds | Imports from |
|---|---|---|
_mission_status_payloads.py (edited) | Contract wanted titles (adds WorkPackageDetail, ArtifactListing, ArtifactContent, ArtifactRefusal, WorkPackageDetailRefusal); payload_leaks artifact-path class; new FLOORS fields; tree_fingerprint with directory names; write_fixture_mission extended with a files mapping (text or bytes), lanes and tasks_md arguments; git_init and commit_all (the campsite helpers) | existing |
_mission_status_artifacts.py (new) | FileSystem seam and REAL_FS; resolve_mission(repo_root, mission_id); is_eligible; classify; media_type_of; redact; has_credential; read_content(...); list_artifacts(...); artifact_references(...); modified_at(...); ArtifactOutcome(status, body) | _mission_status_payloads (MissionSource, load_source), leak_scan (the predicate), leak_patterns |
_mission_status_detail.py (new) | HostCapability, NO_WORKTREES; work_package_universe; subtask_titles; dependency_refs; review_cycles; workspace_of; matches_owned_file; change_states; build_work_package_detail(...); DetailOutcome(status, body) | _mission_status_artifacts (eligibility), _mission_status_payloads (Projector, load_source, STATUS_LANES) |
Entry points return an outcome (a status and a body), never raise for a refusal: the three operations are testable as the service answers them. Public readers only (wp_task_files, read_authored_wp_frontmatter, reconstruct_wp_view, read_lanes_json, lane_branch_name, resolve_workspace_for_wp, build_review_cycle_pointer, ReviewCycleArtifact.from_file, is_glob_pattern, git_merge_base, kernel.git.changed_paths, get_current_branch); the only private names in the test tree are the two matcher copies, imported by the parity test alone.
C. Test map (X-pure: three new modules, the plan's recommendation and the layout it is written for; X: the same plus the live module row; Y: fallback with no new module)
The live-baseline row below exists under X only: under X-pure there is no committed live test, the comparison against the contract tree on main is the W-7 recorded command (no tag exists or is needed, operator ruling 2026-10-05), and ACK-1 is void; the three pure rows, the registrations (three modules, six edits; X: four modules, eight edits) and the lane binding hold for the X-family (X or X-pure).
| Concern | X home (new test module, run by tests (contract tools)) | Y home (existing module) |
|---|---|---|
| Artifact reader: eligibility, containment, listing, content, boundary, encoding, credentials, redaction, faults, mutations | tests/contract/test_mission_status_artifacts.py | test_mission_status_payloads.py (router corpus job) |
| Detail reader: universe, subtask titles, dependencies, cycles, workspace matrix, change state with real git, matcher parity, mutations | tests/contract/test_mission_status_detail.py | test_mission_status_payloads.py |
1.1 proof (the additive proof; "1.1" is the working name, not a version): unchanged version, additive diff, bundle comparison, scope function (name-status entries; allowed set as data that carries the admitted statuses), terminology scan, CHANGELOG section (1.0.0-SNAPSHOT), provisional names, credential-kind descriptions, refusal examples | tests/contract/test_mission_status_contract_1_1.py (green on every clone) | test_mission_status_examples.py |
| Live baseline test (VOID under the operator ruling 2026-10-05: no tag exists, so no such test; kept as the record) (fails, never skips, while the tag is missing; the only accepted red until the tag, and red in any clone without the tag; ACK-1, X only; retired by its named owner at the 1.1.0 release or by the next contract-changing Mission) | tests/contract/test_mission_status_baseline_live.py (its own module, markers contract and corpus only, so the module above is green and the nightly does not select it) | none: no committed live test; the comparison against the tag is a recorded command at W-7 (a live id in the examples module would inherit its fast marker) |
| Corpus reality check: floors, independent counts, readable invariant, refusal codes, verdict and cycle floors, fingerprint | tests/contract/test_mission_status_reality.py (edited; router corpus job) | same |
| Tool extension tests | test_leak_scan.py, test_fixture_builder.py, test_enum_pin_check.py, test_run_negative_cases.py (edited) | same |
| Examples, required cases, path map | test_mission_status_examples.py (edited: EXPECTED_PATH_KEYS becomes eight paths, the "five paths" test, MIN_EXAMPLES, the required cases) | same |
Under the X-family the artifacts module and the 1.1 proof module carry the markers of their neighbours (contract, fast, corpus): they read no tag (none exists), no history and start no subprocess, so they are safe on the nightly's shallow checkout, which is verified in a git clone --depth 1 scratch run. The detail module carries contract, corpus and git_repo and not fast: it builds real git repositories and worktrees, and pytest.ini defines fast as pure logic with no subprocess or git; without fast the nightly fast or unit run does not select it and it runs in the router job tests (contract tools) (the reality module's fast marker beside its git init is a precedent not copied under the X-family). The live module carries contract and corpus only, never fast or unit (plan, ACK-1 b: the nightly selects fast or unit over tests/contract on a tag-less shallow checkout, and a fast live test would be red there on every run). Each of the three pure modules (four under X) needs two registrations outside the spec's slice file set (operator decision OD-1; six edits in all, eight under X, written by the work packages that share those two files, in dependency order, one lane by construction; plan, "Lanes"): one --deselect tests/contract/<module> in the built-in-corpus-suite command of .github/workflows/packs.yml and one row in _CORPUS_MARKED_MODULES of tests/architectural/test_ci_corpus_trigger_completeness.py. Both were proven necessary (research R-6) and sufficient (200 passed, 1 skipped on the gate files). Under Y no registration changes and the tests of the new modules join the serial router corpus job; the modelled cost (research R-6) fits NFR-001 either way. Y cost on markers: the detail concern's real-git change-state tests land in test_mission_status_payloads.py, whose module marker includes fast, so they inherit it (the reality module's precedent) unless that module's marker is changed, which removes its pure tests from the nightly selection; a module-level marker cannot be split per test (tracer-tooling-friction F-19). This is a cost of Y stated in plan OD-1, so the rule above is the X-family rule and the test map's "nothing else changes" holds for the X-family only. Either registration edit alone also selects the architectural-heavy battery on the pull request (plan, OD-1).
D. Floors in code and pinned fixtures
FLOORS gains (tasks phase: added by WP08, where the reality check consumes them, not by WP05): detail_work_packages 3000, status_log_only 26, subtask_ids 14000, table_rows 12000, checkbox_rows 8300, owned_file_entries 13000, dependency_entries 3100, review_cycle_files 1200, missions_with_cycles 210, unparseable_cycles 150, verdict_cycles 290, artifact_entries 14000, readable_true 14000, redacted_files 970, missions_without_lanes 72 (each pinned below the research R-5 measurement by the stated margin rule). The existing three floors stay. A new fixture tests/contract/fixtures/mission_status_artifacts_expected.json lists, by name, the two 413 files, the two not-UTF-8 files and the three NUL files with their codes, the work package with no prompt file, and the Mission with duplicate work package ids; each named entry must still be present and behave as stated (a subset check; an unnamed new anomaly is printed, never a failure). Planted 422 coverage is a fixture Mission built at run time, since the corpus holds none. The readable invariant is checked against an independent oracle, not against the reader's own function (plan D-P8): the reality module classifies every eligible file from its bytes alone (size over 262,144 gives 413; a NUL byte, then a strict UTF-8 decode failure, gives 415; a SECRET_PATTERNS match in the decoded text gives 422; otherwise 200; in that order), and asserts the expected status and the expected readable for each file against the listing entry and against the content read. The equality readable == (content read answers 200) stays only as a wiring guard and is labelled so in the test.
E. Stable test-failure messages
Every new corpus-level assertion states its examined and discovered counts in its failure text and fails when either is zero. A fault-injection test fails with the injected fault did not fire when its counter is zero. A mutation test fails with the mutation was not killed: <name> when the planted defect leaves its target test green.