Mission Specification: Mission Status contract v1
Mission Branch: issue-5558-mission-status-contract-v1 Created: 2026-10-01 Status: Draft Input: Issue #5558 - "Mission Status contract v1: contracts/mission-status/openapi.yaml (split layout, Node-free tooling, CI)". Part of #5528; takes over the transport question from the closed #460; implements the content specified in #5529 (overview) and #957 (detail). Design: the maintainer design comment on #5528 (comment 5937187090).
Summary
The Mission Status Read API is built contract-first. A versioned OpenAPI 3.1 document is the seam between a future read service and the dashboard and Desktop screens, so both sides can be built in parallel against one file. This Mission delivers that file, the conventions that let it scale past one module, and the CI that keeps it honest:
- a split, multi-file contract under
contracts/(_shared/plusmission-status/), covering a read-only v1 slice of five resources; - Node-free validate, bundle, lint and breaking-change checks in a new path-filtered workflow;
- a reality-check test that proves the contract fits this repository's own Missions as today's readers see them;
- a release workflow that publishes the bundled document and its checksum, with a dry run proven in PR 4;
- CODEOWNERS for
contracts/.
It adds a contract and CI only. No Python product code under src/ changes, no status event schema changes, and no CLI behaviour changes.
Terminology (binding for this spec and for the contract)
- Mission, never "feature" (charter Terminology Canon). Contract schema names, examples and descriptions follow the same rule.
- Status lane: one of the work-package lifecycle states of the status domain (glossary term
lane: planned, claimed, in_progress, for_review, in_review, approved, done, blocked, canceled; plus the non-display values genesis and uninitialized). This is the only meaning the contract gives the word lane. The canonical list of the nine values is thelaneentry of.kittify/glossaries/spec_kitty_core.yaml; theLaneentry indocs/context/orchestration.mdstill lists seven values and is marked applicable to 1.x and 2.x. That is known drift, cited here and not fixed by this Mission. - Code lane, repo-root lane, execution lane: the glossary (
docs/context/topology.md) defines Code lane (a lane that resolves to a.worktrees/lane worktree and a lane branch; onlylanesandlanes_with_coordMissions have them) and Repo-root lane (the one-lane manifest of asingle_branchMission). Execution lane is kept only as the umbrella phrase for what a Mission'slanes.jsonrecords across both (docs/architecture/execution-lanes.md); it is not itself a glossary term. None of these is part of v1 and none appears in the contract's schemas. Where the contract, README or an example must name one, it says code lane or repo-root lane, and uses execution lane only as the umbrella. This spec never uses the bare word "lane" for either. - Stream cursor: the event-log tail position
{offset, invariant}of the CLI's tail reader (a byte offset on a line boundary plus a SHA-256 of the last consumed line). Page cursor: the opaque pagination token in_shared/. The two are different schemas with different names; the bare word "cursor" is not used in contract field names. - Lifecycle status: the Mission-level value (active, planned, done, draft, discarded). Mission phase (contract field
phases): a step of the Mission workflow (specify, plan, tasks, implement, review). It is not the glossary termphase(.kittify/glossaries/spec_kitty_core.yaml: a grouping container for mission primitives, not the enforcement unit); the contract and the README say so whereverphasesis described, and the field keeps the namephasesbecause #5558 uses it. Phase label: the optional free-textphasegrouping field in a work package's frontmatter (contract fieldphaseLabel). Three different things with three different names, none of them the glossaryphase. - Bundle: the single resolved
openapi.yamlproduced from a module's split files by CI. It is a build product and is never committed. - Provisional: an element marked
x-provisionalthat may change without a major version bump until decided on #5528.
Clarifications
The following are recorded decisions for this Mission. They bind the spec, the plan and the implementation; a reviewer checks against them rather than re-opening them, with one stated exception: the realised form of CL-2's third bullet and of CL-4's dry-run placement diverges from the literal reading, and is recorded below as the flagged deviations DEV-1 and DEV-2, which need maintainer confirmation (OQ-4) and are not settled decisions. They come from two sources that are kept apart here. CL-1 to CL-4 are maintainer decisions made for this Mission about its process and its CI placement. They are recorded in this spec and are not quotes of #5558 or of the #5528 design comment, which do not contain them; the PR bodies repeat them so the maintainers can confirm them when the design is acknowledged (CL-1), and PR 1 lists DEV-1 and DEV-2 as explicit confirmation items (they are also asked on #5528). CL-1 and C-001 are amended by the operator ruling of 2026-10-02 (DD-21): the single Mission PR is replaced by six stacked seam PRs. CL-5 to CL-9 restate the design in #5558 and in the #5528 design comment, with one exception: the CODEOWNERS handles in CL-6 are also a maintainer decision made for this Mission. Everything the spec author derived from the charter, the code and the data (D-1 to D-19 below, including the silent-success rule D-15, the CI gate analysis D-16 and the release dry-run placement D-17) is a derived decision and open to reviewer challenge.
CL-1 - Build now; every PR of the stack stays a draft until the design is acknowledged (maintainer decision for this Mission, shape amended by the operator ruling of 2026-10-02, DD-21)
The Mission is built now. By the operator ruling of 2026-10-02 (DD-21, which supersedes the earlier "single Mission PR" wording of this clarification and of C-001) it is delivered as six stacked seam PRs: (1) WP01 and WP02; (2) WP03 to WP05 (the contract content, ending at preview point p0, the UI early-start unit); (3) WP06 and WP07; (4) WP08 and WP09; (5) WP10; (6) WP11 and WP12. PR 1 targets main; PR n (n = 2 to 6) targets the head branch of seam n-1 (plan, "PR stack"). Every PR is opened as a draft and all six stay drafts until a maintainer acknowledges the design on #5528 (one acknowledgement covers the stack). Until then none is marked ready for review and none is labelled ready-for-squad. Each PR body links the acknowledgement once it exists. Reading rule for this spec: where it says "the PR", "the PR body" or "green in the PR", it means the PR of the seam that carries that evidence, as assigned by the per-PR evidence table in the plan ("PR stack"); the stack as a whole is meant only where the text says "the stack". This is how the contributing guide's "large changes are agreed first" rule is satisfied for a change that introduces a new top-level tree, a new toolchain and a new release stream.
CL-2 - The reality-check test, its marker, and its CI placement (maintainer decision for this Mission)
- The reality-check test is
tests/contract/test_mission_status_reality.py(a new path) and carriespytest.mark.corpusas a real marker application (a module-levelpytestmarkentry or a decorator), not a prose mention. contracts/**is added to the CI router'scorpuspath filter in.github/workflows/ci-router.yml, so a contracts-only change selects the router's marker-selected corpus job.- The new path-filtered contracts workflow ties into the reality check without running it twice per change. This is the maintainer decision, read literally: the contracts workflow also exercises the reality check against the contract it builds, and the reality check never runs twice per change.
- DEV-1 (flagged deviation from the literal reading; maintainer confirmation requested, non-blocking, OQ-4). The form this spec builds is not the literal one. The contracts workflow proves, with a non-pytest resolver-parity script, that the reality check's resolution of the contract equals the CI bundle, and the reality check itself runs once, in the router's
tests-corpusjob (D-16). The intent (the reality check is exercised against the contract the contracts workflow builds, and never runs twice) is kept; a pytest run inside the contracts workflow is not built. The alternative is a literal pytest run of the reality check in the contracts workflow, which would need a priced ledger row, aWORKFLOW_FILESrow and a deselection fromtests-corpus(D-16). If the maintainer picks the literal reading, these change: FR-017 (no-pytest bullet), FR-019 (last two bullets), FR-020, FR-025 (guard-tests row and runner table), D-16, D-19, OQ-1, SC-007 and SC-008, and C-003. The PR body carries DEV-1 as a confirmation request, in these words.
How these three decisions are realised against the repository's architectural gates, and which job is the single blocking home, is derived in D-16 and specified in FR-017 and FR-020.
CL-3 - Release tag namespace (maintainer decision for this Mission)
The release tag is contract-mission-status-v1.0.0: no leading v, so the CLI's v.. release machinery never sees it. This was verified on this checkout: .github/workflows/release.yml has a single tag trigger, v.. (plus a manual dispatch that takes an existing release tag); scripts/release/validate_release.py (discover_release_tags) keeps only tags that start with v and match the release version pattern; src/specify_cli/release/changelog.py lists tags with the pattern v*. Non-v tags already exist in the repository (crossrepo-bugfix-pack-2026-02-24, saas-hardening-2026-02-24). The contract release workflow's tag trigger must not match any CLI release tag, and the CLI release trigger must not match a contract tag; this is asserted (FR-022).
CL-4 - Release workflow ships in the stack (PR 4); the tag is pushed after merge (maintainer decision for this Mission)
The PR ships the release workflow with a dry run that builds the bundle and its sha256 as a workflow artifact, and that dry run is green in PR 4 (the seam that carries the release workflow and the dry-run job). After merge, a maintainer pushes the tag contract-mission-status-v1.0.0. That tag push is recorded as a close-out step of this Mission (in the PR body and the Mission close-out record), not as a follow-up issue. Because GitHub resolves a manually dispatched workflow from the default branch, the dry run's evidence in the PR comes from the release-dry-run job of the contracts workflow, a pull_request run on the PR branch that executes the same contracts/tools/ release script as the release workflow (D-17, FR-022). The release workflow itself has no pull_request trigger (DEV-2).
CL-5 - Contract-first, split layout, conventions
- OpenAPI 3.1, contract-first. Humans edit split files; CI produces the bundle; the bundle is never committed.
- Layout:
contracts/README.md(conventions);contracts/_shared/(cross-module pieces only:Problemasapplication/problem+json, the page cursor,PageInfo;schemas/,parameters/,responses/, each with an_index.yamlregistry);contracts/mission-status/with a rootopenapi.yaml(info, servers, tags, and a path-to-file map only),paths/(one file per path item, named after the path with/turned into_and{param}kept, for examplemissions_{missionId}.yaml),schemas/(one file per schema, named like the schema, plus_index.yaml),parameters/,responses/,examples/, andCHANGELOG.md. - Each module is one document with its own
info.versionand the/api/v1prefix. Later modules (for example write surfaces) are sibling folders versioned independently. $refs between files are relative file paths; there are no hand-written~1JSON pointers; the root maps paths to files.contracts/fixtures/(the existing handoff fixtures read bytests/contract/test_handoff_fixtures.py) is left untouched.
CL-6 - Node-free tooling and CI shape
- Validate and bundle: a small Gradle build under
contracts/using the openapi-generator Gradle plugin (JVM), which reads multi-file specs; it validates each module and bundles it with theopenapi-yamlgenerator. - Lint: vacuum (a Go binary) with a Spectral-format ruleset kept in
contracts/. - Breaking changes: oasdiff (a Go binary) compares each module's bundle with its last release; a breaking change fails unless the major version moves.
- Tool versions are pinned, downloaded artefacts are checksum-verified, and third-party GitHub Actions are pinned to full commit SHAs (the repository's existing workflows do the same).
- The workflow is path-filtered on
contracts/**and separate from the Python suites, so the Python suites' critical path is unchanged. CODEOWNERSforcontracts/names@stijn-dejonghand@MOES-Media(#5558 says only "CODEOWNERS forcontracts/"; the two handles are a maintainer decision made for this Mission). No.github/CODEOWNERSexists today; it is a new file. The charter records that nothing on GitHub enforces review, so this is advisory routing, and the PR body says so.- No Node toolchain is introduced anywhere.
CL-7 - The v1 slice
Read-only resources, all under /api/v1: GET /project, GET /missions, GET /missions/{missionId}, GET /missions/{missionId}/work-packages/{wpId}, and GET /events (text/event-stream, one schema per event's data, documented as a convention because OpenAPI has no native event-stream schema).
CL-8 - Data decisions
- Raw nine-status-lane counts, with no grouping. Board columns are a documented consumer mapping, not part of the contract.
- Identity is the ULID
missionId.displayNumberis display-only (numbers repeat). - Lifecycle status uses today's five values:
active,planned,done,draft,discarded. - Topology has five values:
lanes,single_branch,coord,lanes_with_coord,unknown. - Staleness and next action are marked
x-provisionaluntil decided on #5528. - Mission phases: specify, plan and tasks come from artifacts and lifecycle events; implement and review are derived from status lanes and are marked as derived.
- No absolute paths (
feedback_path,record_path,prompt_path,feature_dir) and no e-mail addresses in any payload or example. Actors are agent, profile and role handles. - Every v1 schema field cites its source in the status domain.
CL-9 - Scope exclusions
Out of v1: artifact reads (#5533) and write surfaces (#5231). Out of this Mission: the read service implementation and the dashboard and Desktop screens (separate Missions against this contract).
Flagged deviations (maintainer confirmation requested, OQ-4)
- DEV-1 is recorded in CL-2 (third and fourth bullets): the reality check runs once in
tests-corpus, and the contracts workflow proves resolver parity, instead of running pytest itself. - DEV-2 (CL-4 read literally). Read literally, CL-4 has the release workflow carry its own dry run that is green in the PR. The form built here keeps the release workflow to a tag push and a
workflow_dispatch(with a dry-run mode that never publishes), and puts the PR-green dry run in the contracts workflow as therelease-dry-runjob, which runs the same release script. The artifact CL-4 asks for (the bundle and its sha256, built, verified and uploaded, green in the PR) is produced; only the workflow file that holds the PR-triggered job differs. The reason is recorded in D-17. If the maintainer picks the literal reading, these change: FR-017 (fleet-verdict bullet: a secondPR_WORKFLOWSentry, a secondworkflow_runname, and an edit toscripts/ci/fleet_main.py), FR-022, D-16 item 6, D-17, R-12, C-003 and SC-008.
Derived decisions (spec author; reviewable)
These follow from the grounded data shapes, from the code and charter as read on this checkout, and from the decisions above. A reviewer may overturn any of them; if so, the affected FRs change with them.
1. tests/architectural/test_ci_corpus_trigger_completeness.py pins the set of corpus-marked test modules to a hand-maintained registry (_CORPUS_MARKED_MODULES) with equality in both directions, so every new tests/contract module that carries the marker (the reality check and any helper unit-test module) needs its own registry row in the same change. The marker is detected by a single-line regular expression: pytestmark = ...pytest.mark.corpus must be on one line (as in tests/contract/test_example_round_trip.py), or the marker is a decorator; a multi-line pytestmark = [ list is not detected. The file's _CORPUS_GLOBS and _CORPUS_DATA_ROOTS are documentation of the corpus path set, not a mirror of the router: today _CORPUS_GLOBS holds .kittify/release/downstream-verified.json, which the router filter does not, and no test compares the two (test_every_corpus_data_root_is_covered_by_a_trigger_glob compares the roots only with the file's own globs). This Mission does not edit them: the gate needs only the registry rows, and open PR #5557 rewrites this same file so that _CORPUS_GLOBS is read from the live router (load_router().filters['corpus']), so a documentation edit would be a pure conflict risk with no gate behind it. A third transcription, non_src_router_groups.corpus in tests/release/ci_retirement_scrub.json, is not gated either and is left unedited. 2. tests/architectural/test_no_duplicate_suite_execution.py classifies a whole workflow as change-triggered when it has a pull_request trigger or a push with branch or path filters; only a workflow listed in NON_CHANGE_TRIGGERED_WORKFLOWS (or a tags-only push) is exempt, so a workflow_dispatch or tag-push job inside a workflow that also has a pull_request trigger still counts as per-change. Each (workflow, job) that reaches pytest needs a row in AUTHORIZED_PER_CHANGE_SUITE_JOBS; under the charter's Standing Order 5 such a row is priced debt that needs an owner, a rationale and an exit condition, and the empty-allowlist shape is preferred. 3. tests/architectural/test_workflow_coherence.py requires the set of workflows that invoke pytest to equal WORKFLOW_FILES in tests/architectural/_gate_coverage.py, and requires every router filter glob to match at least one tracked path (contracts/ does once contracts/ holds files). 4. .github/ci-module-registry.yml lists tests/contract as a registry-excluded directory whose only per-PR home is the marker-selected router corpus job and records that claiming it in a module row "would double-run it". The registry is not edited. 5. Two per-change jobs already select the corpus marker: tests-corpus in ci-router.yml (-m "corpus and not windows_ci", 10 minute timeout) and built-in-corpus-suite in packs.yml (the same expression over the whole tree with --cov=src/doctrine, 20 minute timeout, forced on for every push to main and in full mode, and on for pull requests touching its built_in filter paths such as packs/built-in/ and tests/doctrine/*). Both are authorised in the ledger and neither has a JVM. The reality-check test is therefore collected by both on a push to main and on a mixed diff, by existing design, and must pass in both. 6. Per-workflow gates the new workflow files meet: tests/ci/test_fork_guard.py (a workflow with a push, schedule or workflow_run trigger needs the canonical fork guard on every root job unless it is in EXEMPT_WORKFLOWS, today only release.yml); the fleet verdict (applicable_workflows in scripts/ci/fleet_verdict.py collects every workflow with a pull_request trigger and raises ValueError when that set differs from PR_WORKFLOWS; tests/ci/test_fleet_verdict.py pins PR_WORKFLOWS and pins the workflow_run list of ci-fleet-verdict.yml to the name: of every member; main_workflows in scripts/ci/fleet_main.py reads only PR_WORKFLOWS members and raises on any push trigger key other than branches and paths). Only a workflow with a pull_request trigger is a member, so the contracts workflow (which has one) is added to PR_WORKFLOWS and to that workflow_run list with the listed edits of FR-017, and the release workflow (tag push and workflow_dispatch only, D-17) is not a member and needs none of the fleet edits, in particular none to scripts/ci/fleet_main.py; and test_reusable_workflow_ceiling_respected in tests/architectural/test_module_shard_registry.py (all .yml files under .github/workflows/ against a ceiling of 20; 17 exist at this base).
- D-1 Status-lane field names. Contract properties that carry a status lane use the stem
statusLane(statusLane,statusLaneCounts,fromStatusLane,toStatusLane) and the enum schema isStatusLane, so a reader of the contract cannot confuse a status lane with an execution lane. Thelanestem is not used. - D-2 Closed response schemas. Response object schemas are closed (
additionalProperties: false, orunevaluatedProperties: falsewhere composed). Documented open maps are the only exceptions (for example subtask id to status lane). Without this the reality check would pass on payloads carrying any junk, which is the vacuous-pass failure this Mission exists to avoid. Adding a response property is still a non-breaking, minor-version change. - D-3 Citation extensions. Every property of every resource schema carries
x-source(repo-relative file path plus symbol, optionally a line number) or, where the value is computed from several sources or by a rule no existing reader implements,x-derived.x-derivedis a structured value,{rule: <non-empty prose>, inputs: [<non-empty list>]}, where each input is either a code citation of the same form asx-source(repo-relative path plus symbol) or a contract-field reference (a property path in this module that must exist). The citation check resolves everyx-derivedinput exactly as it resolves anx-source, and fails anx-derivedthat is free text, has an emptyrule, or has an empty or missinginputslist (FR-010). Line numbers rot; the path and symbol are what the citation check resolves (FR-010). A cited path must exist in the checkout the PR targets. A derivation whose only implementation lived in the dashboard code removed by #5545 is thereforex-derivedwith its rule written in the schema description and its inputs cited (contract fields or surviving status-domain symbols), and carries nox-sourceto the removed file. - D-4
wpTotaldefinition.wpTotalis the number of work packages in the reduced status snapshot (len(snapshot.work_packages)): event-sourced, not a count oftasks/WP*.mdfiles. Measured on this checkout (539 Missions withmeta.json, scaffold of this Mission included): 3125 work package files; 2936 snapshot work packages whenmaterialize_snapshotreads each Mission's own directory; and 52 Missions where the two sets differ (45 with fewer snapshot work packages than files, 7 with more). All of these figures are counted in own-directory mode (each Mission's ownkitty-specs/<slug>/directory), a pure function of the checked-out tree, so they are identical in a developer clone and in CI; they stay indicative until re-measured from atests-corpusjob log at implement start (FR-019). The reducer folds status transitions and annotations only, so a work package that has a file and perhaps aWPCreatedlifecycle row but no transition events (or a Mission with no log at all) is not in the snapshot. That is a pre-existing reader limit which v1 pins as behaviour; it is not hidden: FR-019 lists and counts the disagreeing Missions,wpTotalmay differ from the length of the detailworkPackagesarray, and the PR reports the limit under the charter's pre-existing failure rule (C-009), citing a public upstream issue for it (filed by the implementer if none exists). - D-5 Lifecycle derivation.
lifecycleStatusis derived fromstatusLaneCounts,wpTotal,discardedAtandacceptedAtby an explicit rule written in the schema description (x-derived), taken from the dashboard derivation being replaced:discardedwhendiscardedAtis set;draftwhenwpTotalis 0 (an unreadable event log also reads asdraft, with zero counts, and the reality check lists any such Mission);activewhen any work package is claimed, in_progress, for_review, in_review or approved;plannedwhen none of those holds and any work package is planned or blocked; otherwise (every work package done or canceled)activeuntilacceptedAtis set anddoneafter. v1 pins today's quirks (a Mission whose only remaining work packages areblockedreads asplanned; all-canceledwithacceptedAtset reads asdone). The dashboard function counted five board columns taken fromtasks/WP*.mdfiles with a non-genesis lane (claimed and in_progress as doing, for_review and in_review as for_review, blocked as planned, canceled as done), not snapshot work packages, so the rule above equals the old behaviour only where the file set and the snapshot set agree. The pinned expected values in FR-019 sample only such Missions; the disagreeing Missions are a listed, counted difference, never a silent one. - D-6 Mission-level fields without a reader are excluded.
openDecisionCount(no reader exists),retrospective(the reducer matches anevent_nameenvelope that does not occur in the corpus, so it is effectively never populated, and its record path is an absolute host path),done evidence(free-text commands and file lists),claim.shellPid(host-local), and anypromptPathare not in v1. - D-7 Provisional semantics. Changes to an
x-provisionalelement are recorded in the moduleCHANGELOG.mdunder a "Provisional" heading and do not by themselves require a major version bump. Removingx-provisionalis a minor-version change, also recorded. The breaking-change check still runs over provisional elements and reports them separately; it never excludes them silently. - D-8 Event stream scope. Every event's
datacarriesmissionIdand the stream cursor of that Mission's own log. A stream cursor is valid for exactly one physical event log (the primary and the coordination copy of a Mission's log are different files with different offsets). The event stream therefore never forwards raw log rows: it emits named, projected kinds only, by the allow-list projection in FR-007, and drops every other row kind. v1 has exactly three kinds: a status-lane transition kind, a Mission lifecycle marker kind, and a log-truncated (resync) kind. Annotation, decision-point, retrospective, reviewer-self-approval and unknown rows are not exposed (free-text notes and absolute record paths), and work packagehistoryexposes transitions only (FR-006), so the rule is the same in both places. - D-9 Event projection is mandatory. The CLI's tail reader passes every JSON row through (status transitions, annotations, lifecycle rows, retrospective rows), and the corpus contains absolute paths and e-mail addresses in exactly those rows. The contract's event schemas therefore define the projected shape, and the leak rules (FR-012) apply to event data.
- D-10 Actor handles. #5558 and CL-8 say actors are agent, profile and role handles, so an actor projects to
{tool, role, profile}, wheretoolis the agent handle (the key the status domain stores it under). The stored actor can also carry amodelkey; it is not part of the actor projection (a fourth handle would widen the exposed surface beyond the decision). Themodelof a work package's authored plan and resolved assignment is a separate field (authored.model,assignment.model); it is subject to the same handle pattern, the same null-on-mismatch rule and the same leak scan, and the model sentinel__resolved_model_absent__projects to null. Each handle is a nullableActorHandlestring constrained to a conservative pattern (letters, digits,.,_,:,-; no@, no/, no whitespace; at most 128 characters). The mapping from the stored forms is fixed: (a) a structured actor (the four-key dict the status domain stores) is copied field by field fortool,roleandprofilethrough the handle filter, each becoming null when it is null or fails the pattern; (b) a plain string that matches the pattern becomestool, withroleandprofilenull, and colon-encoded forms such asclaude:sonnet:implementer:implementerare not split; (c) any other string (an e-mail address, anything with@,/or whitespace, or longer than 128 characters) becomes all-null. The reality check asserts an e-mail actor projects to all-null anduserorfinalize-tasksproject totoolwith the others null, andexamples/holds one example per source form. - D-11
topology. A Mission with no stored topology (49 of 539 on this checkout), or with a stored value outside the four known shapes, projects tounknown. It does not inherit the runtime's "unstamped means lanes" default, because that would state something the metadata does not say. - D-12 First-release baseline. For the breaking-change check, "no previous release tag exists" is a distinct, loudly reported state, allowed only for the module's initial version; see FR-016.
- D-13
missionTypeis an open string. Mission types can come from organisation packs, so the contract does not close the set to the three observed built-in values. A typeless Mission isnull. - D-14 Free text, field classes and the leak scan. String values fall into three classes, and the leak scan (FR-012) treats each explicitly. (1) Authored markdown (
promptMarkdown, returned only on request) passes through as authored; it is excluded from the scan of corpus-derived payloads and says so in its schema description, but it is scanned inexamples/, which we author. (2) Strict fields (identifiers, handles and path-like values:missionId,mid8,slug,wpId,targetBranch,mergeTargetBranch,missionType, everyActorHandle,eventType,dependencies,ownedFiles,requirementRefs, subtask ids,trackerRefs): a value that fails the strict patterns projects to null where the field is nullable, and otherwise is a projection error that fails the reality check (an identifier cannot be redacted). (3) Human text fields (title,friendlyName,phaseLabel, everyreason,cancellation.reason, reviewreference,nextAction): real titles and names legitimately contain/tmpor start with~/, so these fields are scanned with the narrower "real host path" pattern and the e-mail pattern, pass through unchanged when clean, and when they fail have only the matching substring replaced by the fixed token[path]or[email](the field stays non-null). Every redaction in the corpus is printed with its Mission and field, never silent. The two real cases~/.kittify Runtime Centralization(a friendly name) and/tmp burn-down: sync(a work package title) are named positive-pass controls of the human text class. - D-15 Silent success is forbidden. Every check this Mission adds (validate, bundle, lint, breaking-change diff, citation check, leak scan, layout check, example and event-mapping checks, enum pinning, pin and checksum verification, resolver parity, no-pytest scan, reality check, release dry run, and every guard test) must fail loudly when it cannot do its job: missing tool, empty corpus, missing release tag, checksum mismatch, zero inputs. A check that passes because it examined nothing is a defect. Authority: the charter's Standing Order 5 (non-vacuous gates: a concrete floor plus a self-mutation test;
architectural-gate-non-vacuity) and the acceptance-criteria non-vacuity tactic. FR-021 and the matrix in FR-025 make it testable. - D-16 CI gate analysis and placement. Read from this checkout, and binding on the plan through FR-017 and FR-020:
Chosen shape. The invariant is that the reality-check test body runs exactly once per change event class that matters, accounting for the packs job (item 5): on a pull_request diff that touches contracts/** and no path of the packs built_in filter, it executes in exactly one blocking per-change job, the router's tests-corpus; on a push to main, and on a diff that also matches the packs built_in filter, built-in-corpus-suite runs it as well, by existing design. The home is tests-corpus, selected by the one added router glob, so neither the router marker expression nor ci-module-registry.yml changes (C-003, SC-008). Neither new workflow invokes pytest on any event: the contracts workflow runs validate, bundle, lint, breaking-change and negative-test jobs plus the non-pytest contracts/tools/ check scripts (D-19) and a resolver-parity script that proves the reality check's resolution of the contract equals the CI bundle (FR-019, OQ-1; the script has its own planted-violation proof, FR-021, FR-025); the release dry run is a job of the contracts workflow, which is change-triggered as a whole and is counted as such, which is a further reason it runs no pytest; the release workflow has only a tags-only push and workflow_dispatch, which test_no_duplicate_suite_execution.py exempts as publication and manual events, and it runs no pytest either. Neither workflow therefore needs a ledger row or a WORKFLOW_FILES row. This is the realisation of "the contracts workflow also runs the reality check without running it twice" (CL-2): a literal pytest execution inside the contracts workflow on a pull request (DEV-1) would be a second per-change execution and would need a priced ledger row, which the "without running it twice" decision rules out. If a maintainer later wants one, it needs the owner, rationale and exit condition above, a WORKFLOW_FILES row and a deselection from tests-corpus, and is outside this Mission.
Where the dry run lives, and the alternative weighed (DEV-2). Two placements were weighed against CL-4 ("a dry run that builds the bundle and its sha256 as an artifact, green in the PR"). (A) A pull_request trigger on the release workflow: it makes the publishing workflow a PR_WORKFLOWS member, so it needs a second PR_WORKFLOWS entry, a second workflow_run name, an edit to production CI code (scripts/ci/fleet_main.py, whose main_workflows raises on any push key other than branches and paths, so the release workflow's tags key needs a skip) and a tags-only test case, and it puts a pull-request trigger on a workflow that publishes. (B, chosen) The release workflow is a tag push plus workflow_dispatch only (with a dry-run mode that never publishes; a manually dispatched workflow resolves from the default branch, so it cannot be the PR evidence); the PR-green dry run is the release-dry-run job of the contracts workflow, which is already a PR_WORKFLOWS member, and both workflows call the same release_check.py and one shared build wrapper under contracts/tools/, so they differ in triggers and publish steps only. B is a strict subset of A's shared-CI edits: no second PR_WORKFLOWS entry or workflow_run name, no fleet_main.py edit, one fewer test edit. A PR-green dry run needs a pull_request-triggered job somewhere; B places it in the one workflow that already has the trigger. Accepted cost: the PR proves the script and the build, not the release workflow's own YAML, whose tag-rule, namespace and publish-step guards (FR-022) are tested separately and whose publication step is proven only after merge (R-12). Planted fixtures. The planted dry-run fixture module (a module whose CHANGELOG.md lacks the version heading, and one with a mismatched version) lives under contracts/tools/fixtures/, outside module discovery, so the real dry run, the layout, validate and lint checks never see it. The negative-test job points release_check.py at that fixture root with the module-root parameter and asserts the failure message, with a clean control module on the same fixture root. The other planted fixtures of the contracts workflow (FR-016's baseline and candidate pair, the layout, citation and leak cases) live in the same place; the text-level leak scan's allowlist of planted-fixture directories (FR-012) is this directory. contracts/fixtures/ is not used (C-004).
- D-17 Release dry run on a pull request. A
pull_requestevent has no tag, so the dry run does not take one as input:contracts/tools/release_check.py(a D-19 script, with a module-root parameter) loops over every module discovered undercontracts/(the direct subdirectories ofcontracts/holding a rootopenapi.yaml;_shared,fixtures,gradleandtoolsare not modules), derives the candidate tagcontract-<module>-v<info.version>from each module'sinfo.version, applies the tag rules to that candidate, and builds, checksums and verifies the bundle. On a tag push the pushed tag is the input and must equal the derived tag of the module it names. Publication is reachable only on a push of a tag ref (FR-022). - D-18 Event stream request shape.
GET /eventstakes an optionalmissionIdquery parameter. A resume cursor requiresmissionIdand is carried as the query parameterstreamCursorin the string form<offset>:<invariant>(decimal byte offset, a colon, the 64 hex characters); the same string is the SSEid:field of every event and is also accepted in theLast-Event-IDrequest header, so a browser's automatic reconnect resumes. The JSON object{offset, invariant}remains the form insidedata. An unscoped stream (nomissionId) is live-only: it never replays, and a cursor sent withoutmissionIdis refused with aProblem. A new connection with no cursor is live-only from the moment of connection. Ordering is per Mission only (log order); there is no cross-Mission order guarantee, and alog-truncatedevent for one Mission may interleave with events of others. The stream sends a heartbeat: an SSE comment line (: heartbeat) every 30 seconds, which carries no data and is not an event. Onlog-truncateda consumer discards that Mission's stored cursor, re-reads the Mission detail, and resumes from the detail'sstreamCursor(the event's own cursor is the reset one: offset 0 with the empty digest). A consumer treats an unknown enum value inside a known event kind as an unknown value and does not fail; the contract's schemas stay closed, so a producer must not emit one. - D-19 Runner for every check. The new workflows run no pytest (D-16), so every Python-implemented check has a named runner that is not a second pytest execution of the corpus. Contract-owned checks are plain Python scripts under
contracts/tools/(new path; not a module, because a module is a directory with a rootopenapi.yaml, and the layout check ignorestools/), run by the contracts workflow aspython contracts/tools/<script>.pyusing only the standard library and the repository's already-locked dependencies (PyYAML and jsonschema are declared inpyproject.toml). They never import or invoke pytest, and they never import fromtests/. The shared contract resolver is a library of that directory,contracts/tools/contract_resolver.py: the reality check and the resolver-parity script both import it, so there is one resolution authority (FR-019). The no-pytest scan ofcontracts/tools/is itself acontracts/tools/script (no_pytest_scan.py, with a unit-test module intests/contract/), so it runs on exactly the edits it guards: atests/ci/placement would not, because the module-matrixcirow selects onlyscripts/ci/and.github/workflows/, and an edit confined tocontracts/tools/selects neither. It detects an import, apython -minvocation or a subprocess or shell string that reaches pytest, building its own search strings from fragments so it does not flag itself, and it scans every script in the directory including itself. The contracts workflow'scontracts/path filter selects it on every edit undercontracts/, README and CHANGELOG included, so each check runs on exactly the edits it guards. Each script has a unit-test module undertests/contract/that carries the single-linecorpusmarker and a registry row (D-16 item 1); the router selects it through the addedcontracts/glob andtests-corpusruns it, once. On a push tomainbuilt-in-corpus-suitecollects it too by existing design (D-16 item 5), so it is fast and needs no JVM. The contracts workflow's negative-test job runs each script against committed planted fixtures and asserts the failure message; that is the class A CI evidence of FR-021. Tests that guard the workflow files,scripts/ci/and the fleet machinery stay intests/ci/, where the module-matrixcirow selects them on every edit to.github/workflows/**(the workflow-file half of the no-pytest guard included). Nothing is placed intests/ci/that must run on an edit confined tocontracts/. The per-check assignment is the runner table after the FR-025 matrix.
User Scenarios & Testing (mandatory)
User Story 1 - Build a screen or a service against one pinned, trustworthy file (Priority: P1)
A developer building the dashboard or Desktop screens, and a developer building the read service, each work against the same versioned contract. They take the bundled openapi.yaml from a release, verify its sha256, and generate or mock from it. Every field they see names where it comes from in the status domain, and fields that are not settled are marked provisional.
Why this priority: it is the reason the Mission exists; both sides can only proceed in parallel if the file is trustworthy and pinnable.
Independent Test: build the bundle from a clean checkout, verify that it contains the five v1 paths, that every property of every resource schema carries a source citation, that every provisional element carries x-provisional, and that every cited path resolves in the checkout.
Acceptance Scenarios:
1. Given a clean checkout, When the contract build runs, Then one bundled openapi.yaml is produced containing exactly the five v1 path items and no unresolved reference. 2. Given a resource schema property with no citation, When the citation check runs, Then it fails naming the property (and a property citing a path that does not exist also fails). 3. Given the bundle, When a consumer reads GET /missions and GET /missions/{missionId}, Then the status-lane vocabulary is the raw nine values, lifecycle status has five values and topology has five values, with no board-column grouping anywhere.
User Story 2 - A maintainer is protected from drift and accidental breaking changes (Priority: P1)
A maintainer opens a PR that touches contracts/**. A dedicated workflow validates, bundles, lints and breaking-change-checks every module, without touching the Python suites' critical path. Each check has been shown to fail on a planted violation, so a green check means something.
Why this priority: an unguarded contract drifts from reality and from its consumers; a guard that cannot fail is worse than none.
Independent Test: for each of validate, bundle, lint and breaking-change diff, a planted violation makes that check (and only that check's job) fail with a message naming the cause; the same fixture without the violation passes.
Acceptance Scenarios:
1. Given a module whose path file references a schema file that does not exist, When validate runs, Then it fails and names the unresolved reference. 2. Given a response schema that violates a ruleset rule, When lint runs, Then it fails; the clean fixture passes under the same ruleset (positive control). 3. Given a baseline bundle and a candidate that removes a response property or an enum value, When the breaking-change check runs without a major version bump, Then it fails; with a major bump it passes. 4. Given a PR that touches only contracts/, When CI runs, Then** the contracts workflow runs and none of the Python module-shard jobs is newly selected by this Mission's edits beyond the corpus job named in CL-2.
User Story 3 - The contract is proven against this repository's own Missions (Priority: P1)
Before anything is built on the contract, a test builds the overview and detail payloads for every Mission in this repository's kitty-specs/ with today's readers and validates each against the contract. A contract that does not match the status ledger fails here.
Why this priority: a contract written from prototypes or memory will not match the stored data; this is the gate that makes the "grounded in what the domain actually stores" claim checkable.
Independent Test: run tests/contract/test_mission_status_reality.py; it validates at least the floor number of Missions and every work package of those Missions, with zero exclusions, and fails on a planted mismatch.
Acceptance Scenarios:
1. Given the repository's committed Missions, When the reality check runs, Then every Mission's overview and detail payload, and every work package's detail payload, validates against the contract. 2. Given a planted payload carrying an e-mail address, an absolute path, an unknown status lane, or an extra property, When the same validation runs, Then it fails (and an identical clean payload passes). 3. Given a Mission with no stored topology, no event log, a null display number or a null status lane, When its payload is built, Then it validates using the documented nullable or unknown forms rather than being skipped.
User Story 4 - Cut a release consumers can pin (Priority: P2)
A maintainer pushes the tag contract-mission-status-v1.0.0 after merge. The release workflow publishes the bundled openapi.yaml and its sha256, and consumers pin by checksum. Before merge, a dry run of the same release script, as a job of the contracts workflow, builds the same two files as an artifact in the PR.
Why this priority: the pin is what consumers rely on, but it cannot be exercised for real until after merge, so the PR proves everything except publication.
Independent Test: the dry run's artifact contains a non-empty bundle and a checksum file, and sha256sum -c over them succeeds; a corrupted checksum makes the dry run fail.
Acceptance Scenarios:
1. Given the PR, When the contracts workflow's release-dry-run job runs, Then it uploads the bundle and its sha256 as an artifact, verifies them, and does not publish anything. 2. Given a tag whose version differs from the module's info.version, When the release workflow runs, Then it fails before publishing. 3. Given the contract tag and the CLI release tags, When the workflow trigger patterns are compared, Then neither matches the other's tags.
User Story 5 - Future modules follow the same conventions, and the right people review (Priority: P2)
A contributor adding a second module (for example write surfaces) reads contracts/README.md, copies the layout, and the same workflow covers it. CODEOWNERS routes contracts/ changes to the contract owners.
Why this priority: the conventions are the long-term value; this Mission only needs to make them discoverable and enforced.
Independent Test: add a throwaway second module folder following the README and confirm the workflow picks it up with no workflow edit; remove its root file and confirm the discovery check fails rather than skipping.
Acceptance Scenarios:
1. Given contracts/README.md, When a reader looks for naming, $ref, versioning and provisional conventions, Then each is stated with an example. 2. Given .github/CODEOWNERS, When it is parsed, Then contracts/ is owned by both named handles.
Edge Cases
- Empty or shrunken corpus. The reality check requires a floor of at least 500 Missions with
meta.json(539 measured on this checkout, scaffold of this Mission included). Zero Missions, or fewer than the floor, fails loudly; it never passes vacuously. It also fails if it validated zero work packages, or if a status lane, topology value or lifecycle value that must be exercised never occurs and no fixture covers it. - Values the corpus cannot exercise.
discarded_atoccurs in zero committed Missions, sodiscardedcannot be proven from the corpus. A fixture-built payload (positive control) proves the branch, and the examples validate. - No previous release tag. The first release has no baseline. The breaking-change check reports that state explicitly (FR-016), allows it only for the initial module version, and fails in every other case, including a shallow checkout that cannot see tags.
- Field absent from real data.
topologyis absent on about 9 percent of Missions (49 of 539);mission_numberis null on about half (255 of 539);mid8resolves to an empty string when there is nomission_id; a work package with no events has a null status lane; some Missions have no event log on the primary checkout (21 of 539; coordination or single-branch Missions among them) and read asdraft; resolved-binding fields exist for only about a quarter of work packages. Each projects to a documented nullable, empty orunknownform. A required property absent from real data is a contract defect to fix, never a reason to skip the Mission. - Unknown enum values. A status lane, lifecycle status or topology value from real data outside the contract's enum fails the reality check loudly (the contract lags the domain); it is neither coerced silently nor dropped. Topology is the one deliberate exception: unstamped and unrecognised values map to
unknown(D-11). - Sentinels and variants in real data. The model sentinel
__resolved_model_absent__projects to null; frontmatteragentvalues can be dicts, plain strings or colon-encoded strings; observedexecution_modevalues include a quoted"code_change"; actors can be an e-mail address,user,finalize-tasks,migration. The projection normalises each explicitly and documents it; it does not skip the record. - Duplicate display numbers. Display numbers repeat (41, 45, 65, 80 and 83 repeat in the corpus).
displayNumberis never used as an identifier, and the reality check assertsmissionIdis unique across the corpus. - Missions without a usable ULID. The dashboard's
legacy:<slug>andorphan:<slug>pseudo-keys are dormant in this corpus and are not part of v1. A Mission directory with no valid ULIDmission_idis not representable; the reality check fails on one rather than inventing an id. - Absolute paths and e-mail in source rows. About 383 event lines contain an absolute path (
feedback_pathon 271 events,record_pathon retrospective rows) and e-mail addresses occur inMissionCreatedpayload.actorandaccepted_by. None may reach a payload, an event or an example. - Readers that write. Some existing read paths write to disk (the dashboard's encoding auto-fix; a past query-only
nextcall that rewrote a trackedstatus.json). The reality check proves the readers it uses left every tracked Mission artifact byte-identical. - Router reshaping while this PR is open. An open CI-rework PR (#5557) re-homes the corpus job (a blocking router job running named node ids, with the marker-selected remainder run elsewhere). After rebase the test must still be selected by exactly one blocking job on a contracts-only pull request (D-16); this is re-verified, not assumed.
- Dangling citations. PR #5545, which removes the CLI-bundled dashboard that many catalogue citations name, has merged on upstream
mainafter this branch's base. A citation that stops resolving fails the citation check loudly after rebase. Derivations that existed only in that dashboard code become contract-ownedx-derivedrules (D-3, FR-010), and the plan rebases onto currentmainbefore implementation starts. - Unknown or pre-existing red tests. The reality check is judged against a recorded baseline (see Risks), so a pre-existing red elsewhere on the base is neither blamed on nor hidden by this Mission.
- Mid-flight Missions. See Reflexivity below.
Reflexivity: Missions in flight
This Mission adds a contract tree, CI workflows, one CODEOWNERS file and tests. It does not change src/, the status event schema, meta.json handling, any CLI command, or any migration. Preserved surfaces, unchanged and not re-described by the new contract, with no new write path to any of them: the vendored spec_kitty_events upstream contract (specify_cli/core/upstream_contract.json, loaded by tests/contract/conftest.py), the orchestrator-api command surface, the doctrine schemas and the mission step contracts. Missions already in flight are unaffected in how they run. One coupling is new and is stated plainly. The router's corpus path filter already includes kitty-specs//spec.md, plan.md, kitty-specs//tasks/, kitty-specs//contracts/ and acceptance-matrix.json (it deliberately excludes only bare status.events.jsonl and a bare kitty-specs/, so routine status churn does not trigger the corpus job). Every Mission work-package PR in this repository therefore already selects the corpus job, and after this Mission that job runs the whole-corpus reality check against the committed data of every other Mission, including Missions mid-flight with partial frontmatter or a mission_number sentinel (the identity reader raises on a non-numeric string). A data defect in an unrelated in-flight Mission can block a different Mission's PR, and this Mission's own scaffold directory is part of that corpus. Triage of such a failure follows the baseline, attribution and blast-radius rules in Risks (R-3, R-4).
Requirements (mandatory)
<!-- Legend (tactic acceptance-criteria-non-vacuity): delivery [build] = new behaviour. The last column of the FR table names the evidence a do-nothing change cannot produce, and the success criteria it feeds (the full SC-to-FR map is in the Traceability section). The Acceptance Criteria that follow name how each fails. -->
Functional Requirements
| ID | Title | User Story | Priority | Status | Delivery | Evidence a no-op cannot produce (SC) |
|---|---|---|---|---|---|---|
| FR-001 | Contracts layout and README conventions | As a contributor, I want one documented layout and naming scheme so that every later module is built the same way. | High | Open | [build] | layout check fails on each planted violation; README and CHANGELOG structural check (SC-001, SC-008) |
| FR-002 | Shared building blocks | As a module author, I want Problem, the page cursor and PageInfo (plus shared parameters and responses) defined once so that modules do not diverge. | High | Open | [build] | lint fails a planted error response with another content type; shared pieces listed in _index.yaml (SC-001) |
| FR-003 | GET /project resource | As a screen developer, I want the project name and Mission count so that I can render a project header. | High | Open | [build] | reality check asserts missionCount equals the number of overview records built (SC-002) |
| FR-004 | GET /missions overview resource | As a screen developer, I want one overview record per Mission, paginated and deterministically ordered, so that I can render the Mission list without reading any work package. | High | Open | [build] | assertion that the built list is strictly ordered by the stated sort key; builder test with tasks/WP*.md reads trapped (SC-001, SC-002) |
| FR-005 | GET /missions/{missionId} detail resource | As a screen developer, I want a Mission's phases and work package summaries so that I can render a Mission page. | High | Open | [build] | phase-basis validation with a planted bad entry; unit tests for every phase-rule branch (SC-002) |
| FR-006 | GET /missions/{missionId}/work-packages/{wpId} resource | As a screen developer, I want one work package's authored plan, resolved state, review evidence, readiness and history, with the prompt body only on request, so that I can render a work package page. | High | Open | [build] | snapshot-equality assertion with a positive and a negative control (SC-002) |
| FR-007 | GET /events event stream resource (projection: D-8, D-9) | As a screen developer, I want a documented change stream with one schema per event kind so that I can update views without polling. | High | Open | [build] | event-name-to-schema mapping check with planted name and schema; examples validate (SC-001) |
| FR-008 | Fixed v1 vocabularies | As a consumer, I want the status-lane, lifecycle, topology and phase vocabularies fixed and ungrouped so that my own mapping is stable. | High | Open | [build] | enum-pinning check plus breaking-change failure on a changed enum (SC-001, SC-002) |
| FR-009 | Identity and display rules | As a consumer, I want a ULID missionId as the only identifier and displayNumber as display-only so that repeated numbers cannot collide. | High | Open | [build] | reality check fails on a duplicate missionId and passes on duplicate display numbers (SC-002) |
| FR-010 | Source citation on every field | As a reviewer, I want each v1 field to cite where the status domain stores or derives it so that I can check the contract against the code. | High | Open | [build] | citation check by AST symbol resolution with cited == total (SC-004) |
| FR-011 | Provisional marking | As a consumer, I want unsettled fields marked x-provisional so that I do not build on them as if they were stable. | High | Open | [build] | provisional-marker check fails on a missing or undescribed marker (SC-004) |
| FR-012 | No host paths, no e-mail addresses (event data: D-9) | As a maintainer of a public repository, or of a private one, I want no absolute path and no e-mail address in any payload, event or example so that the API cannot leak local identity. | High | Open | [build] | leak scan and text-level scan with planted values of each kind and floors (SC-005) |
| FR-013 | Closed schemas and validating examples | As a consumer and as the reality check, I want closed response schemas and examples that validate so that a payload with a wrong or extra field fails. | High | Open | [build] | example validation and orphan-example check on planted violations (SC-001) |
| FR-014 | Validate and bundle check | As a maintainer, I want every module validated and bundled to one document in CI so that structural errors and unresolved references fail the PR. | High | Open | [build] | validate and bundle planted failures; byte-identical double build (SC-001, SC-003) |
| FR-015 | Lint check | As a maintainer, I want a Spectral-format ruleset enforced by vacuum so that conventions are machine-checked. | High | Open | [build] | lint planted-violation fixtures with a passing clean control (SC-003) |
| FR-016 | Breaking-change check (first-release baseline: D-12) | As a consumer, I want a breaking change to fail CI unless the major version moves so that I can pin a major version safely. | High | Open | [build] | baseline and candidate fixture pair, plus the loud no-baseline state (SC-003) |
| FR-017 | Contracts workflow shape | As a maintainer, I want a path-filtered workflow separate from the Python suites so that contract work does not slow the Python critical path. | High | Open | [build] | guard test over the new workflow files (no Node, path filter, fork guard, no pytest) (SC-007, SC-008) |
| FR-018 | Pinned and verified tooling | As a maintainer, I want pinned tool versions, verified checksums and SHA-pinned actions so that the build is reproducible and supply-chain safe. | High | Open | [build] | SHA-pin guard, checksum tamper fixtures, Gradle dependency verification (SC-003) |
| FR-019 | Reality-check test | As a maintainer, I want the contract validated against payloads built from this repository's own Missions with today's readers so that a contract that does not match the ledger fails before anything is built on it. | High | Open | [build] | floors, controls, read-only hash and snapshot-equality assertions (SC-002) |
| FR-020 | CI placement of the reality check | As a maintainer, I want the reality check selected by exactly one blocking per-change job and the architectural gates kept green so that it neither goes unrun nor runs twice. | High | Open | [build] | collect-only run, a named green job, and the named architectural gate files (SC-007, SC-008) |
| FR-021 | Planted-violation proof for every check | As a reviewer, I want each check shown failing on a planted violation, with a clean positive control, so that a green check is evidence. | High | Open | [build] | linked failing runs with asserted failure messages (SC-003) |
| FR-022 | Release workflow, dry run and tag namespace | As a consumer, I want a release that publishes the bundle and its checksum under a tag the CLI release machinery cannot see, and a maintainer wants that proven in the PR except for publication. | High | Open | [build] | dry-run artifact with sha256sum -c; publish-step and namespace guards (SC-006, SC-009) |
| FR-023 | CODEOWNERS for contracts/ | As a maintainer, I want contract changes routed to the contract owners so that the contract has accountable reviewers. | Medium | Open | [build] | CODEOWNERS check; PR 3 (WP07) states review is advisory (SC-010) |
| FR-024 | Module versioning and CHANGELOG | As a consumer, I want a module info.version, a CHANGELOG.md and stated versioning rules so that I can tell what changed between pins. | Medium | Open | [build] | CHANGELOG structural check; breaking-change job fails on a bundle change without a version change (SC-003) |
| FR-025 | Loud failure when a check cannot do its job | As a maintainer, I want every check to fail loudly rather than pass vacuously so that a green run always means the check examined something. | High | Open | [build] | one planted-violation test per matrix row, each asserting a minimum input count (SC-003) |
Acceptance Criteria
Each criterion names the observable that changes and how it fails. "Planted" means a fixture or throwaway change committed only to demonstrate failure (see FR-021 for where the evidence lives).
FR-001 - Layout and conventions
- A layout check over
contracts/(acontracts/tools/script, D-19) fails (naming the offending path) when: a file under a module'spaths/is not named after a path item per the/to_,{param}kept rule; the rootopenapi.yamlmaps a path to a file that does not exist; apaths/file is referenced by no root entry (orphan); a schema file's name differs from the schema it defines; an_index.yamllists a file that does not exist or omits one that does; a$refis a URL, an absolute path, or contains a~1pointer;_shared/is referenced for a module-specific piece; or a bundled document (a file containing the resolved, inlined component set of a module) is tracked undercontracts/. Each is a planted violation with a clean positive control on the same fixture. contracts/README.mdstates, with one example each: the layout, the path-file naming rule, relative$refrules,_shared/admission criteria, per-moduleinfo.versionand the/api/v1prefix, thex-source,x-derivedandx-provisionalextensions, the status lane versus code lane and repo-root lane terminology (with execution lane as the umbrella phrase), the workflow phase versus glossaryphasedistinction, that theTopologyenum is the storedmeta.jsonshape and is unrelated toMissionStatus.topologyin the status aggregate (a two-value legacy or coordination vocabulary outside this contract), and the rule that the bundle is a build product. A Node-free structural check (acontracts/tools/script run by the contracts workflow, D-19, so that an edit confined to either file triggers it) overcontracts/README.mdandcontracts/mission-status/CHANGELOG.mdfails when a required heading is absent (one README heading per topic above; the CHANGELOG headings for added, changed, removed and provisional, and a heading for the module's currentinfo.version), and fails when it read zero headings; a planted fixture with a heading removed fails and the clean fixture passes. The repository's.markdownlint-cli2.jsoncis enforced only by the router's existing advisorymarkdownlintjob (npx, ending in|| true, so it cannot fail); this Mission neither relies on that job as evidence nor adds a Node markdownlint step (C-005), and the README says markdown lint of these files is advisory.git diff --statfor the PR shows no change undercontracts/fixtures/.
FR-002 - Shared building blocks
contracts/_shared/definesProblem(media typeapplication/problem+json), a page cursor schema,PageInfo, and the shared parameters and responses the five resources use, each listed in its_index.yaml. Every non-2xx response in the module references the sharedProblemresponse. A planted path file declaring an error response with a different content type fails lint.- The page cursor is opaque to consumers and has no schema relationship to the stream cursor.
FR-003 - GET /project
- Returns
name(the project slug, cited to.kittify/config.yamlproject.slugandProjectIdentity.project_sluginsrc/specify_cli/identity/project.py) andmissionCount(x-derived: the number of overview records, that is, of Missions the service lists). There is no separate display name in the domain, and the response has no project path, no worktree path and no tokens. - The reality check asserts
missionCountequals the number of overview records it built. The Mission registry of the removed dashboard (build_mission_registry) counted pseudo-key orphans, scanned.worktreescopies and collapsed duplicate ids, so it is at most a cross-check on the clean corpus and never the definition.
FR-004 - GET /missions
- Each record carries the fields in the Contract Field Catalogue (Mission overview). A response schema missing any catalogue field, or carrying one not in the catalogue, fails the reality check (closed schema, D-2).
- Ordering is a documented total order: the operation description names the sort key
createdAtdescending, ties broken bymissionIdascending (a total key, becausemissionIdis unique). The reality check asserts that the overview list it built is strictly ordered by that key with no ties. "Two requests return the same order" is not testable in a Mission that implements no service, so it is not claimed; the total key is the checkable form. The list is paginated with the shared page cursor; the first page, a middle page and an exhausted cursor each have an example. - Overview payloads are built without reading any work package file (
tasks/WP.md): the overview-builder test runs with reads of those files trapped (patchedopenandPath.read_textraising on any path matchingtasks/WP.md) and fails if the trap is hit;wpTotalis the snapshot's work package count (D-4).
FR-005 - Mission detail
- Returns the overview fields plus
phasesandworkPackages.phasesalways contains exactly the five Mission phases (specify, plan, tasks, implement, review); each entry carries astatus(pending, in_progress, complete) and abasisthat isartifact,lifecycle_eventorderived_from_status_lanes. A phase entry without abasis, or an implement or review entry claiming an artifact basis, fails validation. - The phase rules are contract-owned (
x-derived, written in the schema description), because no existing reader computes them: the dashboard's workflow reader returned four keys and set implement to in_progress whenever the tasks directory existed, never complete, and had no review key. Specify, plan and tasks:completewith basisartifactwhenspec.md,plan.mdortasks.mdrespectively exists in the Mission directory; elsecompletewith basislifecycle_eventwhen the matchingSpecifyCompleted,PlanCompletedorTasksCompletedlifecycle event exists; elsein_progress(basislifecycle_event) when the matching...Startedevent exists; elsepending(basisartifact). Implement and review (basisderived_from_status_lanes, counts fromstatusLaneCounts, evaluated in this order). Implement:completewhenwpTotalis above 0 and every work package is for_review, in_review, approved, done or canceled; elsependingwhen no work package has left planned or blocked (including zero work packages); elsein_progress. Review:completewhenwpTotalis above 0 and every work package is done or canceled; elsependingwhen no work package is for_review, in_review, approved or done; elsein_progress. A unit test with planted lane-count fixtures covers every branch. workPackagesentries are the file-backed work packages of the Mission (each joined with its snapshot state; a null status lane when it has no events), as summaries (identifier, title, phase label, status lane, dependencies, readiness, subtask progress, last transition time), and never include the prompt body. A work package present in the snapshot with no file has no authored plan and is not representable; it is counted in the disagreement list of FR-019, not dropped silently.
FR-006 - Work package
- Returns the fields in the Contract Field Catalogue (Work package). The prompt body (
promptMarkdown) is returned only when the request asks for it with an explicit query parameter, and an example proves both the absent and the present case. No property namedpromptPathorprompt_pathexists. - Resolved state is read from the status snapshot, never from stale runtime keys left in WP frontmatter (
agent,shell_pid,lane,assignee,review_status). Schema validation cannot tell a snapshot value from a stale frontmatter value, so the reality check asserts equality instead: for every work package it builds, the payload'sstatusLane,assignmentandactorequal the values derived from an independent read ofmaterialize_snapshotfor that Mission (the assertion re-reads the snapshot; it does not reuse the helper's intermediate). Positive control: a fixture work package whose frontmatterlane,agentandassigneedeliberately differ from its event log must project the snapshot values. Negative control: the same payload patched with the frontmatter value fails the equality assertion. - Work package
historylists status transitions only (kindistransition); annotation and lifecycle rows are not exposed there (D-8), andreasonis a human text field (D-14). - The review block exposes the review reference (
review-cycle://...or the legacy free text reference), the verdict (approvedorchanges_requested) and the reviewer handle; it never exposesfeedback_path.
FR-007 - Event stream
events.yamldeclares a200response with content typetext/event-stream, documents the stream convention in prose (event name,id, resume, heartbeat, per-Mission ordering, consumer actions; D-18), and maps each event name to one data schema. The mapping is checked: every event name referenced in the description has a schema, every schema in the mapping is referenced by exactly one name, and the check fails when it finds zero event names. A planted event name with no schema, or a schema with no name, fails the check.- Request shape (D-18): optional
missionIdquery parameter; optionalstreamCursorquery parameter (<offset>:<invariant>), which requiresmissionId; theLast-Event-IDheader carries the same string; an unscoped stream is live-only and heartbeats are SSE comment lines every 30 seconds. - Exactly three kinds are defined (D-8):
status-transition,mission-lifecycleandlog-truncated(resync, withreasonlimited tosize_shrinkandcontent_mismatch). Every event's data carriesmissionIdand a stream cursor. The projection is an allow-list:
Source row in status.events.jsonl | Stream kind | missionId | at | eventId |
|---|---|---|---|---|
status transition (a row with to_lane) | status-transition | the Mission's meta.json mission_id | the row's at | the row's event_id |
lifecycle row (envelope timestamp, aggregate_id, event_type, payload) whose event_type is one of MissionCreated, SpecifyStarted, SpecifyCompleted, PlanStarted, PlanCompleted, TasksStarted, TasksCompleted | mission-lifecycle | the same meta.json identity | the row's timestamp | the row's event_id |
anything else: annotation rows, WPCreated, ReviewerSelfApproval, DecisionPointOpened and DecisionPointResolved, RetrospectiveCaptured and RetrospectiveCaptureFailed (which uses type), and every unknown kind | dropped, never forwarded |
missionId never comes from aggregate_id: it is the Mission ULID only for MissionCreated, the slug for several other lifecycle rows and the work package id for WPCreated. The reality check projects every committed log, validates each projected event, asserts at least one event of each of the two row-derived kinds, prints the count of dropped rows with each dropped event type, and a planted log row of an unknown kind is dropped, not forwarded.
- Resume: the request carries a stream cursor with
missionId; the documented refusal reasons are exactlynegative,out_of_range,misalignedandcontent_mismatch(the reason values of the tail reader'sResumeRefused), each mapped to aProblemresponse, plus the refusal of a cursor sent withoutmissionId. An example exists for each. Onlog-truncatedthe consumer discards the Mission's cursor and re-reads the Mission detail (D-18). - Raw log rows never appear (D-9): a planted event example carrying an absolute path or e-mail fails the leak scan (FR-012).
FR-008 - Vocabularies
StatusLanelists exactly the nine display values: planned, claimed, in_progress, for_review, in_review, approved, done, blocked, canceled.statusLaneCountshas exactly those nine keys, each required, none grouped, and neithergenesisnoruninitializedis a key (a Mission's event-less work package reads as a nullstatusLane).LifecycleStatusis exactly active, planned, done, draft, discarded.Topologyis exactly lanes, single_branch, coord, lanes_with_coord, unknown. Adding or removing a value in any of the three fails the breaking-change check without a version move and fails an enum-pinning check in the reality check's fixtures.- The README documents the board-column mapping as a consumer convention and the contract contains no
columnsor board grouping schema.
FR-009 - Identity
missionIdis a 26 character Crockford ULID (pattern^[0-9A-HJKMNP-TV-Z]{26}$, the pattern of the status domain's identifier), and is the only identifier in every path parameter that selects a Mission.displayNumberis nullable, described as display-only, and is not accepted as a path parameter anywhere.mid8is nullable (null when there is no Mission id), never an empty string.- The reality check fails if two corpus Missions share a
missionId, and passes when Missions share adisplayNumber.
FR-010 - Citations
- A citation check (a
contracts/tools/script, D-19) fails when a property of a resource schema has neitherx-sourcenorx-derived, when anx-derivedis not the structured form of D-3, has an emptyrule, or has an empty or missinginputslist, when anx-derivedinput does not resolve (a code input by the rules below; a contract-field input as a property path that must exist in the bundle), when a cited file does not exist in the checkout, or when a cited symbol does not resolve in the cited file. Resolution is by defined name, not by text match: in a.pyfile the symbol must be adef, aclass, or an assignment target (module or class level) found by parsing the file's syntax tree, and a name that appears only in a comment, a string or as a substring of another name does not resolve; in a YAML or JSON file the symbol is a key path that must exist. Line numbers are optional and are not checked. The check fails by naming the property and the unresolved citation. - Traversal: every property at any depth under every resource schema of the bundle (nested objects, array
items,allOf,oneOfandanyOfbranches,additionalPropertiesvalue schemas, and properties reached only through$ref) is checked. The check computes the total property count itself, prints it beside the number of properties carryingx-source, the number carryingx-derivedand the number ofx-derivedinputs resolved, and fails unless thex-sourceandx-derivedcounts sum to the total and the total is above zero. A planted nested property without a citation fails, a planted symbol that occurs only in a comment fails, a plantedx-derivedwith no inputs fails, and a plantedx-derivedinput naming a missing symbol fails. Whether a resolving citation is the right one is a review judgment no check can make; to bound reuse of one real citation, the check reports any citation used by more than five properties, and the PR body lists them together with everyx-derivedproperty, its rule and its inputs, so the set of properties exempt from code grounding is visible and cannot grow unnoticed. - Where a citation can name a status-domain reader (
src/specify_cli/status/,src/specify_cli/mission_metadata.py,src/mission_runtime/), it does so. A derivation that existed only in the dashboard code (lifecycle derivation, workflow phase, next action, mission count) is stated as anx-derivedrule in the schema description (D-3, D-5, FR-005), with nox-sourceto code that the base no longer contains, so the contract stands on its own.
FR-011 - Provisional
- Staleness (per work package; the domain's
StaleState: fresh, stale or not applicable, plus reason and minutes since last commit, which needs git and worktrees and so is not computable from events alone) and next action (a nullable prose string; the derivation returns one of two prose strings or null, the review-then-accept string or the consolidate string, both of which embed the Mission slug) carryx-provisional: truewith a description naming the open decision on #5528. A check fails if either lacks the marker, and fails if any element carriesx-provisionalwithout a description that names what is undecided. - Provisional fields are nullable. Because the reality check cannot compute staleness (it has no worktrees), its payloads carry null there;
examples/carries populated provisional values that validate, so the non-null branches are exercised.
FR-012 - No leaks
- No property in the contract tree is named
feedback_path,record_path,prompt_path,feature_dir,worktreepath,project_path, or camel-case forms of those (feedbackPath,recordPath,promptPath,featureDir), and no property carries an absolute path. A planted property with any of those names fails a name check. - A value scan runs over every example and every payload the reality check builds, per field class (D-14), with these patterns (Python regular expressions). Strict fields: a value is a host path if it matches
^(?:/|~/|[A-Za-z]:[\\/]), or(?:^|/)(?:home|Users)/[^/\s]+/, or[A-Za-z]:\\Users\\. Human text fields: a value contains a host path if it matches(?<![\w.~-])/(?:home|Users|root)/[^\s/]+, or(?<![\w.~-])/(?:tmp|var/tmp|var/folders)/\S, or[A-Za-z]:\\Users\\[^\s\\]+(so a leading~/and a bare/tmpword are not host paths there). All string fields except authoredpromptMarkdownin corpus-derived payloads: a value is an e-mail address if it matches[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+. These patterns were run over the corpus's work package titles and phase labels, owned files, dependencies, requirement references, subtasks, friendly names, slugs and target branches (6231 values at measurement) with zero hits. Behaviour on a hit follows D-14 (null, projection error, or substring redaction by field class). The scan has same-fixture positive controls (a planted value of each kind in each class is detected), the two named real pass controls of D-14, and floors per class (it fails when it scanned zero values in a class). - Actor values are
ActorHandlestrings (D-10). The reality check's corpus contains source actors that are e-mail addresses or free text; the check asserts they projected to all-null handles, so the redaction is exercised, not assumed. - A text-level scan runs the human-text and e-mail patterns over every file under
contracts/(descriptions,x-sourceandx-derivedstrings, README, CHANGELOG, examples including thepromptMarkdownof examples, which we author) and over the new tests' fixture files, with an explicit allowlist limited to the directory of deliberately planted fixtures (each planted file carries a marker line and is itself asserted to be detected). It fails when it scanned zero files. Test source files are not scanned (they hold the patterns); planted values in test code are assembled at run time from fragments. - Residual risk, stated: a handle-shaped string can still be a person's account name (the status domain has recorded a user's OS or git user name in actor-like fields). The contract cannot prove a handle is not a person's name; the shape rule and the value scan reduce, not eliminate, that risk (see Risks).
FR-013 - Closed schemas and examples
- Response object schemas are closed (D-2).
examples/holds at least one validating example per resource and per event kind, plus the provisional-populated and discarded-Mission examples. A validate step fails when an example does not validate against the schema it is attached to, and whenexamples/contains a file no schema or operation references.
FR-014 - Validate and bundle
- The workflow validates every module found under
contracts/(a module is a directory with a rootopenapi.yaml) and bundles each to oneopenapi.yaml. It reports the number of modules validated and fails when that number is zero. - The bundle contains the five v1 path items and no remaining relative file
$ref. A planted dangling reference, a planted schema that breaks OpenAPI 3.1, and a planted module whose root file is missing each fail the job. - Bundling is deterministic: two consecutive builds of the same checkout produce byte-identical files (the sha256 of both is compared and the job fails on a difference).
- The bundle is uploaded as a workflow artifact and is never committed (a tracked bundle fails FR-001's check).
FR-015 - Lint
- The ruleset is a Spectral-format file kept under
contracts/. It enforces at least:operationIdpresent and unique, every operation tagged, every non-2xx response referencingProblem, every schema property described,x-sourceorx-derivedon resource schema properties (FR-010), no property with a forbidden name (FR-012), enum values in a fixed case convention, and closed response schemas (D-2). Each rule has a planted-violation fixture and the clean fixture passes under the same ruleset. - Lint fails when the ruleset file is missing or empty, when vacuum loads zero rules, or when it lints zero files.
FR-016 - Breaking-change check
- For each module the check compares the freshly built bundle with the bundle of the module's last release (the most recent tag matching
contract-<module>-v<semver>), rebuilt or downloaded as a verified release asset. A breaking change (removed path, removed or renamed response property, narrowed enum, newly required request parameter, changed type) fails unless the candidate'sinfo.versionhas a higher major version than the baseline. A non-breaking addition passes with a minor or patch bump and fails ifinfo.versiondid not change at all while the bundle did (CHANGELOG discipline, FR-024). - First release (D-12): when no tag matching the module's pattern exists, the job prints an explicit "no baseline" notice and records it in the job summary; this is allowed only when
info.versionis the module's initial version and the module'sCHANGELOG.mdcarries the initial entry, and fails otherwise. The job also fails (it does not report "no baseline") when it cannot list tags reliably, for example in a shallow checkout (git rev-parse --is-shallow-repositoryis true) or when tag listing errors. - Provisional elements are diffed and reported in their own section (D-7); they are never dropped from the comparison.
- Because no real release exists yet, the negative proof of this check runs against a committed pair of small baseline and candidate fixtures, under
contracts/tools/fixtures/(outside module discovery, D-17), with a clean positive control.
FR-017 - Workflow shape
- A new workflow file (new path, for example
.github/workflows/contracts.yml) triggers on pull requests and on pushes tomain, both filtered to exactly three paths (thepushtrigger carriesbranches: [main]; thepull_requesttrigger carries nobrancheskey, because PRs 2 to 6 of the stack target seam branches and abranches: [main]filter would never fire on them; the same absence keeps the fleet-verdict applicability test, which defaults a missingbranchesto every base, true for them; DD-7, DD-21):contracts/**,.github/CODEOWNERS(so the CODEOWNERS check of FR-023 runs on the edits it guards) and its own file. It has least-privilegepermissions, shares no job with the Python suites, and holds the validate, bundle, lint, breaking-change, negative-test andrelease-dry-runjobs (FR-022). The release workflow (FR-022) has only a tag push andworkflow_dispatch: a tag push cannot be path-filtered, and it has nopull_requestor branch-push trigger. A guard test over the two new workflow files fails when (i) the contracts workflow has apull_requestorpushtrigger without exactly that path set, or itspull_requesttrigger carries abrancheskey, or itspushtrigger lacksbranches: [main], (ii) the release workflow has anypull_requesttrigger or any push trigger other than tags, or (iii) either file invokes a Node toolchain (node,npm,npx,yarn,pnpm,setup-node). The release workflow's tag glob is checked by the namespace guard and its publication by the publish-step guard (FR-022), not by the path-filter rule. Like every guard test (D-15) it first asserts it parsed at least one workflow file and one job. - The Mission's edits to the Python suites' selection are limited to the one corpus glob in the router (FR-020), plus the registry rows that the corpus-trigger gate requires (D-16: the gate requires the registry rows; nothing reads the marker description, so
pytest.iniis not edited, and the file's glob and data-root documentation is not edited, because #5557 rewrites it). The listed fleet-verdict edits below are the only other edits to shared CI. No existing job'sneeds, matrix or marker expression changes, andci-module-registry.ymlis untouched. - Neither new workflow invokes pytest, directly or through
make, a script or a composite action, on any event (D-16). The guard has two halves, each placed where it runs on the edits it protects (D-19): a guard test intests/ci/parses both workflow files and fails if any step reaches pytest (selected by thecirow on any workflow edit), and thecontracts/tools/no_pytest_scan.pycheck, run by the contracts workflow, fails if any script undercontracts/tools/imports or invokes pytest (selected bycontracts/**). No ledger row and noWORKFLOW_FILESrow is therefore added; if a plan change ever adds one, FR-020's ledger rules (owner, rationale, exit condition) apply and the change is called out in the PR. - Fork guard: both new workflows have a
pushtrigger (branch and paths for the contracts workflow, tags for the release workflow), sotests/ci/test_fork_guard.pytreats them as automated workflows. Every root job (noneeds) carries the canonical guard as the first top-level&&conjunct of itsif:((github.repository == 'spec-kitty/spec-kitty' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch')), so pull-request and manual runs stay live on forks. No entry is added toEXEMPT_WORKFLOWS(onlyrelease.ymlis exempt). The named file is run and passes. - Fleet verdict (explicit, minimal, listed edits; the alternative that needs more is weighed in D-17).
applicable_workflowsinscripts/ci/fleet_verdict.pyadds every workflow that has apull_requesttrigger to the inventory it checks and raisesValueError("PR workflow inventory changed") when that set differs fromPR_WORKFLOWS;tests/ci/test_fleet_verdict.pypinsPR_WORKFLOWS(test_all_existing_pr_workflows_are_registered,test_new_pr_workflow_fails_closed) and pins theworkflow_runlist ofci-fleet-verdict.ymlto thename:of every member plus the aggregate (test_reporter_trigger_covers_every_registered_workflow_and_reruns). Only the contracts workflow has apull_requesttrigger, so only it is added; leaving it out would fail those tests and, once merged, would make the fleet verdict computation raise for every PR. The release workflow has nopull_requesttrigger, so it is not in the inventory,main_workflowsinscripts/ci/fleet_main.pynever reads it, and that file is not edited. This Mission makes exactly these edits to the fleet machinery and no others: (a) the contracts workflow file name is added toPR_WORKFLOWS(scripts/ci/fleet_verdict.py); (b) itsname:is added to theworkflow_runlist ofci-fleet-verdict.yml; (c)tests/ci/test_fleet_verdict.pyis updated: its workflow-id fixture gains the new entry, and its existing expected sets additionally exclude the path-filtered contracts workflow (thepyproject.tomldiff expectsPR_WORKFLOWSminus the contracts workflow; thedocs/example.mddiff keeps its existing exclusion ofrelease-readiness.ymlandcheck-spec-kitty-events-alignment.ymland also excludes the contracts workflow; a case with acontracts/path expects it); (d)tests/ci/test_fleet_main.py: the contracts workflow has a push trigger withbranchesandpaths, whichmain_workflowsclassifies as a conditional main gate, so the conditional-gate expectations of that file (test_absent_conditional_gate_is_explicit_and_its_failure_is_observedlists the unobserved conditional gates) gain it; the plan reads the exact lines. Eachpull_requesttrigger uses only the keysapplicable_workflowssupports (types,branches,paths); the contracts workflow'spull_requesttrigger usespathsonly (nobranches), so it stays applicable to a stacked PR whose base is a seam branch (a missingbranchesdefaults to every base) and theci-router.ymlpull_request trigger, which also has nobranchesfilter, behaves the same way. Consequence: a red contracts check on a PR that touches a path in its filter appears in the fleet PR verdict comment; the contracts workflow's push trigger is path-filtered, so it is a conditional main gate. The contracts checks are still not inrouter-gate. C-003 and SC-008 list these edits, and the PR body repeats them.tests/ci/test_fleet_verdict.pyandtests/ci/test_fleet_main.pyare run as named gate files (FR-020). - Workflow count: 17 workflow files exist at this base and the two new ones (the contracts workflow and the release workflow) make 19, within the ceiling of 20 pinned by
test_reusable_workflow_ceiling_respected. The plan records the count and re-verifies it after rebasing on currentmain, because other CI changes (#5557 is open) may consume the remaining slot.
FR-018 - Pinned and verified tooling
- Gradle (distribution checksum verified through
distributionSha256Sum), the openapi-generator plugin, vacuum and oasdiff each have an exact version; downloaded binaries and distributions have a recorded sha256 in a committed manifest, and the workflow verifies each checksum before the binary or distribution is executed. A mismatching or missing checksum fails the job before execution (planted: an altered checksum fails). Everyuses:in the new workflow files is pinned to a full 40 character commit SHA with a version comment; a guard test fails on a floating tag or branch ref (planted: an unpinneduses:is detected, the pinned one passes). - The plugin and its transitive JVM dependencies are resolved under Gradle dependency verification: a committed
verification-metadata.xml(undercontracts/gradle/) records a sha256 for every resolved artefact, and the build runs with strict verification, so an unverified artefact, a missing metadata file or a tampered checksum fails the build naming the artefact (planted: a copy of the metadata with one altered checksum fails). A missing JVM, a missing Gradle distribution and a plugin or repository resolution failure each fail the job naming the cause (FR-025). - No tool is installed through an unpinned package-manager command in the workflow. Downloads use HTTPS only.
FR-019 - Reality check
tests/contract/test_mission_status_reality.pybuilds, for every Mission directory underkitty-specs/that has ameta.json, the overview payload and the detail payload, and for every work package in that Mission the work package payload. It reads each Mission through the status domain's coordination-aware read surface (MissionStatus.loadand its resolved read directory, ADR D-3: the API composes the existing read models and never rebuilds paths) and then uses the existing public readers:materialize_snapshotfor the status snapshot (the read-only function;materializealso writesstatus.jsoninto the Mission directory, which this check must never call; on this checkout it rewrites tracked snapshots and creates missing ones), themeta.jsonidentity reader,resolve_mid8,compute_weighted_progress,reconstruct_wp_view(WPView),dependency_readiness_for_wpand the tail reader for the stream cursor.MissionStatus.load(specify_cli/status/aggregate.py) reads the coordination copy when the Mission's coordination branch exists and raisesCoordinationBranchDeletedwhen it does not, so which status source a Mission is read from depends on the branches present in the clone: a developer clone with local coordination branches and a CI clone can differ. On this checkout 36 Missions cannot be loaded that way (indicative only). For those the check reads the Mission's own directory, lists each such Mission in its output and prints the count. That fallback count is environment-dependent, so it is reported and never floored or ceilinged. This is the recorded deviation from ADR D-3 and it is counted, not hidden.- Environment rule. Every pinned number (the floors below, the disagreement-list ceiling, and the pinned expected-output fixture) is computed in own-directory mode: a second, cheap pass that reads each Mission's own
kitty-specs/<slug>/directory withmaterialize_snapshotand builds no payload. That pass is a pure function of the checked-out tree, so the same figure holds in a developer clone and in CI. The coordination-aware read is used for building and validating the payloads, and the equality assertions re-read the same resolved directory as the payload, so they hold in either environment. The ceilings and floors are nevertheless confirmed from atests-corpusjob log (the CI checkout) at implement start and recorded in the PR; every figure in this spec is indicative until then, and a figure that differs between the developer clone and the CI log is a defect in the pass, fixed before the number is pinned. - Derivations that have no public reader (lifecycle status, phases,
readyToStart,missionCount; D-3, D-5, FR-005) are implemented in the test-side helper (a module undertests/contract/, not collected as a test; new path; no module undersrc/) as the executable form of the rules written in the contract's schema descriptions. Because the helper then holds its own copy, three things keep the check from testing only itself: the rule lives in the contract description, which is the single authority; the helper's unit tests cover every branch with planted lane-count fixtures; and a pinned expected-output fixture holds the derived values for a named sample of Missions chosen to cover each lifecycle value and each documented quirk, produced once by the original dashboard derivation at the recorded commit d78aa2345 and committed, and the reality check compares the helper's output with it. That fixture always runs; it is not skipped when the original code is gone. - It validates every built payload against the contract, one parametrised test case per Mission (the case id is the Mission directory name, so a failure names the Mission) plus a small number of corpus-level tests. A payload failing validation fails the case and names the Mission, the work package and the JSON pointer of the first failure; a Mission is never silently skipped, and there is no exclusion list at delivery (a later exclusion needs an issue, an owner and a drain date, per the charter's allowlist rule, and is printed in the test output). Corpus-level tests that must see every Mission carry an explicit
timeoutmark with the rationale recorded beside it, because thepytest.inidefault of 240 seconds applies to each test including fixture setup (NFR-001). - Non-vacuity floors: at least 500 Missions, at least 3000 file-backed work package payloads (3125
tasks/WP.mdfiles measured) and a summed own-directory snapshot work package count of at least 2800 (2936 measured); at least one Mission for each of the nine status lanes is represented instatusLaneCounts; each ofactive,planned,doneanddraftoccurs; each of the four stamped topology values andunknownoccurs (these four floors are evaluated over the own-directory pass); the test fails if any floor is not met and no fixture covers it. The floors are set below counts measured by a short, documented method (globkitty-specs//meta.json, counttasks/WP*.md, sumlen(materialize_snapshot(dir).work_packages)), and the numbers are re-measured at implement start and recorded in the PR, never copied from this spec. - Snapshot and files disagree: for every Mission the check asserts
wpTotalequalslen(snapshot.work_packages), and lists every Mission whose snapshot work package count differs from itstasks/WP*.mdcount (52 at measurement: 45 with fewer snapshot work packages than files, 7 with more), with both numbers. The list is computed in own-directory mode and its length is asserted at or below a pinned shrink-only ceiling, so the difference cannot grow unnoticed (D-4). ThelifecycleStatuscomparison with the pinned expected values samples only Missions where the two sets agree, and compares the helper's output over the own-directory snapshot. - Positive and negative controls on one shared fixture: a payload of a real Mission validates; the same payload with a planted e-mail, absolute path, unknown status lane, or extra property fails. The snapshot-equality assertion of FR-006 has its own positive and negative control.
- The test is read-only, which the reader choice above makes necessary and the hash check proves: it records a content hash of every tracked file under
kitty-specs/before and after the build and fails if any differ. - The contract it validates against is the contract the CI build resolves, not a hand copy: it either consumes the bundle the build produces or resolves the split files with a reference resolver whose output is proven equal to the bundle by a non-pytest parity script that the contracts workflow runs and that fails on divergence (OQ-1 chooses the mechanism; the requirement is that there is one resolution authority or a proven-equal pair). The resolver-parity pair is specified as follows. The resolver is
contracts/tools/contract_resolver.py(D-19): the reality check imports it from there (a test importing fromcontracts/tools/is the allowed direction; the tool never imports fromtests/and never imports pytest), andcontracts/tools/resolver_parity.pyimports the same module, so the two cannot diverge by being two copies. The parity script fully dereferences both the resolver's output and the CI bundle and compares the two trees, key order ignored. It fails, naming the first differing JSON pointer, on any divergence; it fails when the resolver cannot be imported, when the bundle is missing or empty, and when it compared fewer than the five v1 path items, zero schemas or zero resolved$refs (it prints the three counts). Its negative proof is a planted divergent resolver (a copy that drops one$reftarget or alters one schema, undercontracts/tools/fixtures/) which the negative-test job runs the script against, asserting the failure message and the pointer, with the real resolver as the clean control; it has a unit-test module like everycontracts/tools/script (D-19). - It gives a real verdict in every job that selects the
corpusmarker, which are the router'stests-corpusand the packsbuilt-in-corpus-suite(--cov=src/doctrine, D-16), both without a JVM; if the contract or its resolver is unavailable it fails (it never skips and never passes).
FR-020 - CI placement of the reality check
contracts/**is added to thecorpusfilter group in.github/workflows/ci-router.ymlas an additive, single-line change; the existing globs are untouched. The router jobtests-corpusis the reality check's blocking per-change home on a contracts-only pull request (D-16); the router's marker expression andci-module-registry.ymlare not edited.tests/architectural/test_ci_corpus_trigger_completeness.pyis updated only so that its curated registry liststests/contract/test_mission_status_reality.pyand every other newtests/contractmodule that carries the marker (including the helper's unit-test module and the unit-test module of eachcontracts/tools/script). Its_CORPUS_GLOBSand_CORPUS_DATA_ROOTSare not edited (D-16: documentation that no gate reads, and #5557 rewrites it). Every new module applies the marker on a single line,pytestmark = [pytest.mark.contract, pytest.mark.fast, pytest.mark.corpus], or as a decorator, because a multi-line list is not detected by the gate's regular expression.pytest.iniis not edited: the gate asserts only that thecorpusmarker is registered (test_corpus_marker_is_registered_in_pytest_ini) and no test reads the marker's description text, so a description edit would have no failing test behind it. Running that test file passes (it fails if the registry and the marked modules differ in either direction).- Selection is demonstrated, not assumed. After rebasing on the latest
main,pytest --collect-only -m "corpus and not windows_ci" tests/contract/test_mission_status_reality.pycollects the test, and the body of PR 5 namestests-corpusas the test's blocking home with a link to a green run of it on a contracts-touching diff. The evidence step lists every job whose pytest expression selectscorpusat that moment (at this base,tests-corpusinci-router.ymlandbuilt-in-corpus-suiteinpacks.yml) and verifies each expression with a collect-only run; the PR body records, per router shape onmainat that moment, which jobs select the test. - Exactly-once, scoped (D-16): on a
pull_requestdiff touchingcontracts/**and no path of the packsbuilt_infilter, the number of blocking per-change jobs whose logs show the reality check's test id executing is exactly one (tests-corpus). The contracts workflow'spull_requestruns, itsrelease-dry-runjob included, are change-triggered events and are counted: they run no pytest, so they add none; the release workflow has no change trigger. On a push tomainand on a diff that also matches the packsbuilt_infilter,built-in-corpus-suiteruns the test as well by existing design, and the module passes there (no JVM,--cov=src/doctrine).tests/architectural/test_no_duplicate_suite_execution.py,tests/architectural/test_workflow_coherence.py,tests/architectural/test_ci_corpus_trigger_completeness.py,tests/ci/test_fork_guard.py,tests/ci/test_fleet_verdict.py,tests/ci/test_fleet_main.pyandtest_reusable_workflow_ceiling_respectedstay passing. They are run as named files only (the charter forbids full architectural sweeps in mission work). Any ledger or allowlist addition, expected to be none, is justified in the plan with an owner, a rationale and an exit condition. .github/ci-module-registry.ymlis unchanged by this Mission; if the plan finds a reason to edit it, the reason and the conflict handling with the open CI PRs are recorded in the plan and SC-008 is amended.- Every other new test names its marker and the CI job that collects it, verified by a collect-only run. A test in
tests/contract/with onlyfastorcontractmarkers is collected by no per-PR job; any such new test in that directory carriescorpus(and a registry row), and tests for the workflow files go intests/ci/, whose module-matrix row already selects changes under.github/workflows/**. Checks that must run on an edit confined tocontracts/arecontracts/tools/scripts, nottests/ci/tests (D-19).
FR-021 - Planted-violation proof
- For each of validate, bundle, lint, breaking-change diff, citation check, leak scan, layout check, example and event-mapping checks, enum pinning, pin and checksum verification, resolver parity, the no-pytest scan, the README and CHANGELOG structural check, the CODEOWNERS check, reality check, the release dry run, and each guard test, there is at least one planted violation that makes that check fail, with the failure message naming the cause, and a clean control on the same fixture that passes. The evidence has two classes, fixed by where the check runs (the runner table), and no check is in both: class A (linked real failing run) covers every check run by the contracts workflow (validate, bundle, lint, breaking-change, pin and checksum verification, every
contracts/tools/script including resolver parity and the release script): a negative-test job runs it against committed planted fixtures and asserts the failure message, or throwaway commits whose failing runs are linked in the PR body. Class B (in-test planted case) covers the checks that are pytest tests, namely the reality check and the guard tests intests/ci/: the test itself runs the check against a planted input and asserts the failure and its message, and the evidence is the green run of that test, whose test ids are listed in the PR body (a deliberately red throwaway commit is not required, because the assertion that the planted input fails is what passes). A check with no evidence of its class is treated as not delivered. For thecontracts/tools/checks the workflow's negative-test job is the runner of record (D-19). - Where a negative-test job (class A) or an in-test case (class B) runs a check against a planted fixture, the job passes only if the check failed for the expected reason (it asserts the message), not merely because it exited non-zero.
FR-022 - Release workflow and dry run
- A new release workflow (new path, for example
.github/workflows/contracts-release.yml) triggers on tags matchingcontract--v..and onworkflow_dispatch(an input selects dry-run mode, which builds and verifies but never publishes; a manually dispatched workflow runs from the default branch, so it is not PR evidence). It has nopull_requesttrigger and no branch push (DEV-2, D-17), so it is not change-triggered and not aPR_WORKFLOWSmember, and it invokes no pytest on any event (D-16, FR-017). Its root jobs carry the repository fork guard (FR-017). The PR-green dry run is therelease-dry-runjob of the contracts workflow (FR-017, D-17). Both callcontracts/tools/release_check.pyand the shared build wrapper: for every discovered module the script derives the candidate tag frominfo.version, applies the tag rules, builds the bundle, writesopenapi.yaml.sha256, verifies it withsha256sum -c, and the job uploads both as a workflow artifact and executes no publication step. The dry run's module-root parameter lets the negative-test job point it at the planted fixture root (D-17). - Tag rules, checked before publication and in the dry run (against the derived candidate on a pull request, against the pushed tag on a tag push): the tag matches
contract-<module>-v<semver>;<module>names an existing module;<semver>equals that module'sinfo.version; the module'sCHANGELOG.mdhas a heading for that version. Each violation fails the job (planted, undercontracts/tools/fixtures/: a fixture module whose version has no CHANGELOG heading, and a mismatched version, each fail, with a clean control module on the same root). - Publication is reachable only on a tag push. Every publish step (creating the release, uploading an asset) carries an
ifcondition restricted to a push of a tag ref (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')). A guard test parses the workflow, fails when it finds zero publish steps, fails when any publish step lacks that restriction, and evaluates the condition for apull_request, aworkflow_dispatch, a push tomainand a push ofrefs/tags/contract-mission-status-v1.0.0, which must yield false, false, false and true (planted: a publish step without the condition fails). - Namespace guard: a test evaluates the release workflow's tag glob against the CLI release tags and the CLI release trigger glob against
contract-mission-status-v1.0.0; neither matches. It implements GitHub's filter-pattern semantics, notfnmatch(matches any characters except/,also matches/), so a tag that differs from a matching one only by a slash (for examplecontract-mission/status-v1.0.0) is not matched; it fails when its list of CLI release tags is empty, and fails when a contract tag would matchv... It does not call the CLI release scripts with the contract tag as a live tag. - The release workflow is not wired into
.github/workflows/release-readiness.yml,release.ymlor any CLI release gate. The contract release does not alterrelease.yml. - After merge (close-out step, CL-4): a maintainer pushes
contract-mission-status-v1.0.0; the evidence is a published release with exactly the bundle and its sha256, the downloaded checksum verifying, and the digest matching a locally rebuilt bundle. This evidence is recorded in the Mission close-out record and cannot be produced in the PR.
FR-023 - CODEOWNERS
.github/CODEOWNERS(new) contains a rule forcontracts/owned by@stijn-dejonghand@MOES-Media. A check (acontracts/tools/script run by the contracts workflow, whose path filter includes.github/CODEOWNERS, D-19) parses the file and fails if the rule is missing, if either handle is missing, or if the pattern does not covercontracts/mission-status/. The PR body states that review is not enforced by branch protection today.
FR-024 - Versioning and CHANGELOG
- The
mission-statusmodule hasinfo.versionset to its first release value and aCHANGELOG.mdwith an initial entry and sections for added, changed, removed and provisional elements.contracts/README.mdstates the versioning rule: breaking changes move the major version, additive changes the minor version, documentation-only changes the patch version; a bundle change with noinfo.versionchange fails the breaking-change job. The README and the CHANGELOG satisfy the structural check in FR-001 (markdown lint of them is advisory, FR-001).
FR-025 - Fail-loud matrix
The table below is binding; each row has a planted-violation test (FR-021). General rule (D-15): every check and every guard test first asserts a minimum input count (files globbed, uses: lines inspected, tags evaluated, publish steps found, examples validated, event names mapped, properties traversed) and only then asserts its properties, so none can pass on zero inputs.
| Check | Cannot do its job when | Required behaviour |
|---|---|---|
| Validate | no module found; a module has no root openapi.yaml; the JVM, Gradle or the validator is missing or its checksum mismatches; plugin or repository resolution fails; dependency verification fails; a $ref does not resolve | Non-zero exit; message names the module and the cause; reports module count; zero modules fails |
| Bundle | output missing, empty, or has fewer than the five v1 paths; two builds differ; JVM, Gradle or plugin resolution failure | Non-zero exit naming the missing or differing artefact |
| Lint | ruleset missing or empty; zero rules loaded; zero files linted; binary missing or checksum mismatch | Non-zero exit naming the cause |
| Breaking-change diff | no baseline and not the initial version; shallow checkout or tag listing error; baseline cannot be fetched or its checksum mismatches; oasdiff missing | Non-zero exit; the one allowed no-baseline state is printed loudly and recorded in the job summary |
| Layout check | no module found; zero files under any paths/ inspected; zero _index.yaml read | Non-zero exit naming the path and the rule; reports the counts inspected |
| Citation check | zero citations found; the x-source and x-derived counts do not sum to the computed property count; a cited path or symbol does not resolve; an x-derived is free text, has an empty rule, has empty or missing inputs, or has an input that does not resolve | Non-zero exit naming the property; prints the x-source, x-derived and resolved-input counts |
| Example validation and orphan-example check | examples/ empty or zero examples validated; an example validates against nothing; a file in examples/ is referenced by nothing | Non-zero exit naming the example |
| Event-mapping check | zero event names found in the stream description; a name with no schema; a schema with no name | Non-zero exit naming the name or schema |
| Enum pinning | a pinned enum list is empty or the schema's enum cannot be read; a value added or removed | Non-zero exit naming the enum and value |
| Leak scan and text-level scan | zero values scanned in a class; zero files scanned; a planted value of any kind not detected | Non-zero exit |
| Pin and checksum verification | the manifest is empty; zero tools verified; zero uses: lines inspected; a checksum mismatch; an unpinned uses:; Gradle dependency metadata missing or mismatching | Non-zero exit before any tool executes, naming the artefact |
| Resolver parity | zero documents; fewer than the five v1 path items, zero schemas or zero resolved $refs compared; the resolver cannot be imported; the bundle missing or empty; the two trees differ | Non-zero exit; prints the three counts; on divergence names the first differing JSON pointer |
No-pytest scan of contracts/tools/ | zero scripts scanned; a script imports or invokes pytest or reaches it through a shell string or make | Non-zero exit naming the script and the line; prints the count scanned |
| Guard tests (workflow shape and no Node, no pytest in the new workflow files, fork guard, SHA pin, namespace, publish-step condition) | zero workflow files, jobs, uses: lines, CLI release tags or publish steps found | Test failure naming the missing input, before any property is asserted |
| CODEOWNERS check | the file is missing or holds zero rules | Non-zero exit naming the missing input before any rule is asserted |
| Reality check | fewer than the floor of Missions; zero work packages; contract or resolver unavailable; payload invalid; readers modified a tracked file | Test failure, never skip, never pass |
| Release dry run and release | bundle empty; sha256 mismatch; tag not matching the rules; module or version missing; zero modules discovered; the module-root parameter names a directory with no module | Non-zero exit before any publication step |
Runner assignment (binding, D-19). Every check has one runner, and none is a second pytest execution of the corpus.
| Check | Runner | CI evidence of failure (FR-021) |
|---|---|---|
| Validate, bundle, lint, breaking-change diff, tool pin and checksum verification | contracts workflow jobs (Gradle, vacuum, oasdiff, checksum steps) | class A: negative-test job over planted fixtures, message asserted |
Layout check, citation check, example and orphan-example check, event-mapping check, enum pinning, leak scan and text-level scan, README and CHANGELOG structural check, CODEOWNERS check, resolver parity, the no-pytest scan of contracts/tools/, release script (tag rules, dry run) | contracts/tools/ scripts run by the contracts workflow (path filter contracts/** and .github/CODEOWNERS); each has a unit-test module in tests/contract/ (single-line corpus marker, registry row) run once by tests-corpus | class A: negative-test job over planted fixtures, message asserted |
| Reality check | pytest in tests-corpus (and built-in-corpus-suite by existing design, D-16) | class B: the controls inside the module, run green with the failure asserted |
| Guard tests on workflow files (workflow shape, no Node, no pytest in workflow files, fork guard, SHA pin, publish-step condition, namespace) | pytest in tests/ci/, selected by the module-matrix ci row on any workflow edit | class B: each guard's own planted cases |
| Release dry run | the release-dry-run job of the contracts workflow (pull_request), and the release workflow's workflow_dispatch dry-run mode after merge; both call release_check.py | class A: negative-test job running release_check.py over the planted fixture module root |
Non-Functional Requirements
| ID | Title | Requirement | Category | Priority | Status |
|---|---|---|---|---|---|
| NFR-001 | Reality-check runtime | The reality check over the corpus at delivery completes in at most 120 seconds wall-clock for the whole module on a standard hosted runner (539 Missions and 34148 event lines on this checkout), and no single test case, fixture setup included, takes more than 60 seconds (a quarter of the 240 second per-test ceiling in pytest.ini). Both numbers, and the duration of the whole tests-corpus job against its 10 minute timeout (at least 50 percent headroom), are measured from the CI job log, recorded in the body of PR 5 (WP10), and checked there by the reviewer. WP12 T076 measures the numbers and relays them; the orchestrator then updates PR 5's draft body with them (a body edit on a draft PR, not a merge); they are not asserted inside the test (no timing flake). | Performance | Medium | Open |
| NFR-002 | Deterministic bundle | Two builds of the same commit produce byte-identical bundles (equal sha256), checked in CI. | Reliability | High | Open |
| NFR-003 | Supply chain | 100 percent of downloaded tool artefacts have a pinned version and a verified sha256; 100 percent of uses: references in the new workflows are SHA-pinned; zero floating references. | Security | High | Open |
| NFR-004 | Leak-free artefacts | Zero absolute host paths and zero e-mail addresses in the contract tree (text-level scan, FR-012), in examples/ including their promptMarkdown, and in every payload the reality check builds (excluding authored promptMarkdown of corpus-derived payloads). | Security | High | Open |
| NFR-005 | No new Python critical-path cost | The contracts workflow adds no job to the Python module-shard matrix and changes no existing job's needs; the only router edit is the single corpus glob. Measured by diffing the workflow files. | Performance | Medium | Open |
| NFR-006 | Reproducible locally | A contributor with a JDK and network access can run validate and bundle locally with one documented command; the README states the prerequisite and the command. The Mission's own required checks run in CI and do not depend on local runs. | Usability | Low | Open |
| NFR-007 | Test discipline | New Python test code passes ruff check and ruff format (both hard-enforced). No existing CI job measures coverage of test-helper code (the coverage configuration maps src/ and the router corpus job runs without --cov), so the criterion is checkable behaviour: every projection helper function has a direct unit test, and the planted positive and negative controls exercise each branch. The implementer also runs a non-gating local pytest --cov=<helper module> --cov-branch --cov-fail-under=90 over the helper's unit-test file and pastes the output in the body of PR 5. Mission work runs targeted test files and the named architectural gate files only, never a full architectural, end-to-end or full-suite sweep; commands and counts are recorded in the PR. | Quality | Medium | Open |
Constraints
| ID | Title | Constraint | Category | Priority | Status |
|---|---|---|---|---|---|
| C-001 | Draft PRs until design acknowledged | Six stacked seam PRs (operator ruling of 2026-10-02, DD-21; supersedes "one PR for the Mission"): (1) WP01+WP02 onto main; (2) WP03-WP05; (3) WP06+WP07; (4) WP08+WP09; (5) WP10; (6) WP11+WP12, each onto the previous seam's head branch. Stack-level rules: all six are opened as drafts and stay drafts until the single acknowledgement on #5528 (CL-1); the ready-for-squad label goes on none until then; they merge bottom-up (PR 1 first) and only by the operator or a maintainer; the orchestrator and implementers never merge a PR and never mark one ready; retargeting an upper PR after a lower one merges is a maintainer act. | Process | High | Open |
| C-002 | Contract and CI only | No file under src/ changes; no status event schema, meta.json handling, CLI command or migration changes. Test helpers live under tests/. | Technical | High | Open |
| C-003 | Minimal shared-CI edits | .github/workflows/ci-router.yml receives only the additive corpus glob (FR-020); .github/ci-module-registry.yml, pytest.ini and the glob and data-root sets of tests/architectural/test_ci_corpus_trigger_completeness.py are not edited (that test gains registry rows only). The only other shared-CI edits are the fleet-verdict edits listed in FR-017: scripts/ci/fleet_verdict.py (one PR_WORKFLOWS entry), .github/workflows/ci-fleet-verdict.yml (one name in the workflow_run list), tests/ci/test_fleet_verdict.py and tests/ci/test_fleet_main.py; scripts/ci/fleet_main.py is not edited. Neither #5557 nor #5545 touches any of these four fleet files (checked with gh pr diff --name-only). The real conflict surface: open PR #5557 edits ci-router.yml and .github/ci-module-registry.yml, rewrites tests/architectural/test_ci_corpus_trigger_completeness.py (which this Mission also edits for its registry rows), edits _gate_coverage.py, test_no_duplicate_suite_execution.py, test_workflow_coherence.py and test_module_shard_registry.py (gates this Mission runs), and adds tests/ci/test_corpus_blocking_home.py; PR #5545 (merged after this branch's base) edited ci-router.yml, ci-module-registry.yml, ci-windows.yml, ci-nightly.yml and other CI files. Edits are additive, rebased last, and the named gates re-run after rebase. | Technical | High | Open |
| C-004 | contracts/fixtures/ untouched | The existing handoff fixtures and their reader tests/contract/test_handoff_fixtures.py are not modified. | Technical | High | Open |
| C-005 | Node-free | No Node, npm, npx, yarn or pnpm in the contracts tooling, workflows or tests. | Technical | High | Open |
| C-006 | Public repository hygiene | Nothing in the spec, plan, contract, examples, tests or commit messages contains an absolute host path, an e-mail address, a credential, or a reference to a private discussion. The standard co-author trailer that tooling appends to a commit message is the one exception to the e-mail rule. Upstream issue and PR numbers may be cited. | Security | High | Open |
| C-007 | Terminology | "Mission", never "feature", in all new text and identifiers; "status lane" is never conflated with "code lane", "repo-root lane" or the umbrella "execution lane", and "workflow phase" is never conflated with the glossary phase. The glossary was checked for the terms used: lane (.kittify/glossaries/spec_kitty_core.yaml, nine values), Code lane and Repo-root lane (docs/context/topology.md), phase (grouping container); execution lane is not a glossary term. | Governance | High | Open |
| C-008 | No version numbers in scope | The release stream's own version (contract-mission-status-v1.0.0, info.version) is the artefact's identity, not a CLI release scope; nothing in this Mission names or alters a CLI release version. | Governance | Medium | Open |
| C-009 | Pre-existing failures reported, not absorbed | Pre-existing red tests encountered are recorded against a baseline taken on the base commit and reported as the charter's pre-existing failure rule requires; they are never made green by this Mission, and never blamed on it without the baseline. | Governance | High | Open |
| C-010 | ATDD order | The reality-check test (and each check's planted-violation fixture) is committed first and shown red against an empty or wrong contract before the contract makes it green; reviewers verify red then green. | Quality | High | Open |
| C-011 | Docs and ADR | No ADR is authored here. The open ADR amendment on the read API's placement and transport (the ADR still records an in-process module and CLI JSON verbs, with no HTTP server in the CLI) is a separate decision on #5528, not this Mission's scope. | Governance | Medium | Open |
Key Entities (include if the mission involves data)
- Contract module: a self-contained OpenAPI 3.1 document split across files, with its own
info.version,CHANGELOG.mdand release stream (mission-statusfirst). - Shared building blocks: cross-module pieces in
contracts/_shared/(Problem, page cursor,PageInfo, shared parameters and responses). - Bundle: the single resolved document built by CI from a module; the release asset; never committed.
- Release asset pair: the bundle
openapi.yamland itsopenapi.yaml.sha256. - Mission overview: one record per Mission with identity, vocabulary values, raw status-lane counts, progress, timestamps and the stream cursor.
- Mission detail: the overview plus the five Mission phases and work package summaries.
- Work package detail: authored plan, resolved state, readiness, review evidence and history for one work package; prompt body only on request.
- Event stream: the
text/event-streamchange feed of projected, named event kinds, each with a data schema, amissionIdand a stream cursor. - Corpus: this repository's own
kitty-specs/Missions, the data the reality check validates against.
Contract Field Catalogue (requirements)
Evidence is cited as path:line at this checkout (branch HEAD fefa3bc73 on a base descending from d78aa2345). The symbol is what the citation check resolves (D-3, FR-010); line numbers are indicative and rot. src/ is implied. "Null" means the schema allows null. "Provisional" means x-provisional: true. Types are the contract's types, not the source's. Rows that cite specify_cli/dashboard/scanner.py record historic evidence: PR #5545 has merged and removed that module upstream, so after the required rebase those derivations are contract-owned x-derived rules stated in the schema description and the scanner is no longer cited (D-3, R-2).
Project (GET /project)
| Field | Type | Null | Source |
|---|---|---|---|
| name | string | no | .kittify/config.yaml project.slug; specify_cli/identity/project.py:44-58 (ProjectIdentity) |
| missionCount | integer | no | x-derived: the number of overview records, that is, of Missions the service lists (the removed dashboard's build_mission_registry is a cross-check on the clean corpus only) |
Mission overview (items of GET /missions; also the head of detail)
| Field | Type | Null | Source |
|---|---|---|---|
| missionId | string, ULID | no | meta.json mission_id; specify_cli/mission_metadata.py:226-269; ULID pattern specify_cli/status/models.py:99 |
| mid8 | string | yes | mission_runtime/identity.py:64-96 (resolve_mid8; empty becomes null) |
| slug | string | no | specify_cli/mission_metadata.py:254-256 |
| friendlyName | string | no | meta.json friendly_name; specify_cli/dashboard/scanner.py:730-737 (falls back to slug) |
| displayNumber | integer | yes | meta.json mission_number; specify_cli/mission_metadata.py:107-116. Display-only, not unique |
| missionType | string | yes | meta.json mission_type; specify_cli/mission_metadata.py:262. Open set (D-13) |
| targetBranch | string | no | meta.json target_branch; specify_cli/mission_metadata.py:61 |
| topology | enum (5) | no | meta.json topology; mission_runtime/context.py:44-56; unstamped or unknown is unknown (D-11) |
| createdAt | string, date-time | no | meta.json created_at; specify_cli/mission_metadata.py:62 |
| statusLaneCounts | object, nine required integer keys | no | StatusSnapshot.summary, specify_cli/status/models.py:740-765; nine keys at :29-68 |
| wpTotal | integer | no | len(snapshot.work_packages); ProgressResult.total_count in specify_cli/status/progress.py (compute_weighted_progress) (D-4). May differ from the number of work package files |
| blockedCount | integer | no | derived: statusLaneCounts.blocked (x-derived) |
| progress | object: weightedPercentage, donePercentage, doneCount, semantics (const weighted_readiness) | no | specify_cli/status/progress.py:79-116, :119-206, :48; weights :34-46 |
| lifecycleStatus | enum (5) | no | x-derived: the rule in D-5 over statusLaneCounts, wpTotal, discardedAt and acceptedAt; historic implementation _derive_mission_status in specify_cli/dashboard/scanner.py (board columns, file-count based) |
| acceptedAt, mergedAt, discardedAt | string, date-time | yes | meta.json; specify_cli/dashboard/scanner.py:235,244; specify_cli/mission_metadata.py:705-731 |
| lastEventId | string | yes | snapshot.last_event_id; specify_cli/status/models.py:749 |
| lastActivityAt | string, date-time | yes | derived: maximum work package last_transition_at (x-derived; there is no Mission-level event time) |
| eventCount | integer | no | snapshot.event_count; specify_cli/status/models.py:748 |
| streamCursor | object: offset (integer), invariant (64 hex characters) | no | specify_cli/status/tail_reader.py:59-73, :52 (empty digest at offset 0), :327-339; bound to one log file (D-8) |
| nextAction | string | yes | Provisional. Two prose strings or null: the review-then-accept string (when the Mission carries a merge baseline and is not accepted) or the consolidate string (all work packages terminal with some done, not accepted); both embed the Mission slug; null otherwise. Historic implementation _derive_next_action in specify_cli/dashboard/scanner.py; the runtime's Decision (runtime/next/decision.py:94-131) is per-agent and is not v1 |
Mission detail (adds to the overview)
| Field | Type | Null | Source |
|---|---|---|---|
| phases | array of exactly five: name (specify, plan, tasks, implement, review), status (pending, in_progress, complete), basis (artifact, lifecycle_event, derived_from_status_lanes) | no | x-derived, contract-owned rules in FR-005. Inputs: presence of spec.md, plan.md, tasks.md; the lifecycle event type constants in specify_cli/status/lifecycle_events.py; statusLaneCounts. There is no implement or review lifecycle event, and the historic get_workflow_status returned only four keys |
| workPackages | array of summaries | no | per work package fields below |
Work package (GET .../work-packages/{wpId}; summaries reuse a subset)
| Field | Type | Null | Source |
|---|---|---|---|
| wpId | string | no | specify_cli/status/wp_metadata.py:222 |
| title | string | no | specify_cli/dashboard/scanner.py:953-962; wp_metadata.py:223 |
| phaseLabel | string | yes | frontmatter phase; specify_cli/status/wp_metadata.py:269 |
| authored.role, authored.agentProfile, authored.model | string | yes | specify_cli/status/wp_view.py:117-135 |
| authored.subtasks, ownedFiles, dependencies, requirementRefs | array of string | no (may be empty) | specify_cli/status/wp_view.py:132-135, wp_metadata.py:226 |
| executionMode, taskType, priority, mergeTargetBranch, trackerRefs | string or array | yes | specify_cli/status/wp_metadata.py:235-265 (normalisation of quoted values documented) |
| statusLane | StatusLane | yes | snapshot lane; specify_cli/status/wp_view.py:80,181 (null when no events) |
| assignment (agent, assignee, role, agentProfile, agentProfileVersion, model, provider) | handle strings | yes each | specify_cli/status/wp_view.py:66-91,159-195; sentinels to null :176-194 |
| subtasks | object: subtask id to StatusLane | no | specify_cli/status/wp_view.py:85 |
| subtaskProgress | object: done, total | no | specify_cli/dashboard/scanner.py:1042-1057 |
| implementerOfRecord | handle | yes | specify_cli/status/reducer.py:123-172 (schema 2 snapshots only) |
| lastTransitionAt, forceCount, lastEventId | string, integer, string | no | snapshot keys present on every entry (specify_cli/status/reducer.py, survey of committed status.json) |
| actor | object: tool, role, profile (handles) | no | decode_actor and actor_identity_str in specify_cli/status/models.py (D-10: mapping from the stored dict and string forms; model is not part of the actor) |
| cancellation | object: reasonSource, reason | yes | specify_cli/status/reducer.py:114-115 |
| readiness | object: satisfied (boolean), unsatisfied (array of wpId) | no | specify_cli/core/dependency_graph.py:34-72 (a missing dependency's status lane defaults to planned, :87) |
| readyToStart | boolean | no | x-derived: true only when statusLane is planned and readiness.satisfied is true; false for every other status lane, including null. Inputs: dependency_readiness_for_wp in specify_cli/core/dependency_graph.py and the snapshot lane |
| review.latestResult | object: reviewer (handle), verdict (approved, changes_requested), reference (string) | yes | specify_cli/status/reducer.py:201-216; specify_cli/status/models.py:285-305 |
| review.override | object: at, actor (handle), reason | yes | specify_cli/status/wp_review.py:28-53; models.py:428-445 |
| history | array of: eventId, at, kind (transition only), fromStatusLane, toStatusLane, actor, force, reason, reviewVerdict | no | StatusEvent in specify_cli/status/models.py. Annotation (InnerStateChanged) and lifecycle rows are not exposed (D-8); reason is a human text field (D-14) |
| staleness | object: status (fresh, stale, not_applicable), reason, minutesSinceCommit | yes | Provisional. specify_cli/core/stale_detection.py:38-53 |
| promptMarkdown | string | yes | on request only; specify_cli/dashboard/scanner.py:1126. No promptPath (D-6, D-14) |
Events (GET /events)
| Event name | Data fields | Source |
|---|---|---|
status-transition | missionId, eventId, wpId, fromStatusLane, toStatusLane, at, actor, force, streamCursor | StatusEvent in specify_cli/status/models.py; reader-injected cursor keys in poll_once (specify_cli/status/tail_reader.py); projection table in FR-007 |
mission-lifecycle | missionId, eventId, eventType (one of the seven allow-listed lifecycle event types), at, streamCursor | event type constants in specify_cli/status/lifecycle_events.py; allow-list, missionId and at sources in FR-007 |
log-truncated | missionId, reason (size_shrink, content_mismatch), detectedAtOffset, streamCursor (reset to offset 0 with the empty digest) | _truncation_signal in specify_cli/status/tail_reader.py |
Citation drift check. The data-shape evidence this catalogue builds on was re-verified on this checkout (branch HEAD fefa3bc73, base descending from d78aa2345). The status-domain citations held; the MissionStatus class is declared at specify_cli/status/aggregate.py:168. Upstream main has since moved: #5545 merged and removed specify_cli/dashboard/, so the scanner-bound rows above are historic until the rebase (R-2).
Out of Scope
- The read service implementation (separate Mission, built against this contract) and the dashboard and Desktop screens (built in the UI repository against this contract).
- Artifact reads (#5533) and write surfaces (#5231).
- Execution-lane data (
lanes.json: code lanes and the repo-root lane), board-column grouping, retrospective data, done evidence, host-local claim data (shell process ids), the runtime's per-agent next-step decision, open decision counts, andMissionStatus.topologyin the status aggregate (a two-value legacy or coordination vocabulary, unrelated to the contract'sTopology). - Any change under
src/, any status ormeta.jsonschema change, any CLI behaviour change, any migration. - Authoring or amending an ADR (see C-011), fixing the quirks of the existing lifecycle derivation (D-5), fixing the existing readers' defects (including the reducer limit in D-4), or adding the glossary term
execution lane(the glossary was checked: see Terminology and R-9). The preserved surfaces named in Reflexivity are not touched. - Publishing the release inside the PR (the tag is pushed after merge, CL-4).
- Changes to
contracts/fixtures/or to the CLI's own release process.
Assumptions
- The maintainer-ratified design in the #5528 design comment stands; all six PRs of the stack are drafts until the design is acknowledged (CL-1, C-001).
- The status domain's readers (
materialize_snapshot,reconstruct_wp_view,compute_weighted_progress,dependency_readiness_for_wp, the lifecycle derivation) are stable enough to cite and to call from a test; they are not modified here. - The corpus (
kitty-specs/onmain) is large enough (on this checkout: 539 Missions withmeta.json, 518 with event logs, 3125 work package files, 2936 snapshot work packages, 34148 event lines; the earlier survey counted 538, 517 and 34146, the difference being this Mission's own scaffold) that the floors in FR-019 are meaningful, and its stored data is representative of what a read service will serve. Floors are re-measured at implement start. - A JDK, Gradle and network access are available on the CI runner for the contracts workflow; the corpus-marker jobs (the router's
tests-corpusand the packsbuilt-in-corpus-suite) do not have a JVM and are not given one (OQ-1, D-16). pytest.inikeeps thecorpusmarker; the CI router keeps a marker or node-id based corpus selection in some shape (the open CI-rework PR changes the shape, not the existence).
Risks and Dependencies
| # | Risk or dependency | Effect | Mitigation |
|---|---|---|---|
| R-1 | Open PR #5557 (CI rework) edits ci-router.yml, ci-modules.yml, module-tests.yml, ci-aggregate.yml, ci-nightly.yml, packs.yml and .github/ci-module-registry.yml; rewrites tests/architectural/test_ci_corpus_trigger_completeness.py (its glob set becomes the live router's corpus group) and edits tests/architectural/_gate_coverage.py, test_no_duplicate_suite_execution.py, test_workflow_coherence.py and test_module_shard_registry.py, plus many tests/ci/ files; adds tests/ci/test_corpus_blocking_home.py; and re-homes the corpus job to a named-node-id blocking router job with the marker-selected remainder elsewhere. | A marker-only corpus test could become advisory or orphaned after that merges; the one-line glob edit and the registry rows in test_ci_corpus_trigger_completeness.py can conflict; the gate files this Mission runs change under it; the workflow-file count ceiling (FR-017) may be consumed. | Additive single-line edit; no edit to the glob and data-root sets (D-16); rebase last and re-read each gate file; re-verify selection by collect-only plus a named job link (FR-020); if the blocking home is node-id based, the reality check's id is added to it (a router edit, recorded in the plan with SC-008 amended) or the plan records why it stays marker-selected. |
| R-2 | PR #5545 (remove the bundled dashboard) has merged on upstream main (after this branch's base, which still contains the dashboard code). It edited ci-router.yml, ci-module-registry.yml, ci-windows.yml, ci-nightly.yml and the ADR, and removed the dashboard code that many catalogue citations name. | Citations to specify_cli/dashboard/ dangle after the rebase, and the derivations that lived only there have no reader left. | Rebase onto current main before implementation; the citation check fails loudly on a dangling citation (FR-010); derivations that existed only in the dashboard become x-derived rules in the schema descriptions (D-3, D-5, FR-005) and are covered by the helper unit tests and the pinned expected-output fixture (FR-019); status-domain citations are re-verified after the rebase. |
| R-3 | Baseline of known-red tests on main. The corpus and contract tests have failed on main for unrelated reasons before. | Misattributing a red. | Record, at implement start, a baseline run of the targeted files (tests/contract corpus-marked tests, the three named architectural gates, tests/ci) on the base commit; judge the reality check and the gates against it; report pre-existing reds as the charter requires and never absorb them (C-009). |
| R-4 | The reality check couples contract validity to live kitty-specs/ data, and the router corpus glob already includes kitty-specs//tasks/, so every Mission work-package PR runs it against every other Mission's data. | A flawed stored Mission, including one mid-flight, turns a corpus-marked job red on an unrelated PR. | Zero exclusions at delivery. A failure naming a Mission the PR does not touch is first classified as contract gap, reader defect or data defect and routed to that Mission's owner in the PR conversation; the exclusion path (an issue, an owner and a drain date, printed in the output) is pre-approved for it so a stranger's PR is not stuck. It is never silenced. |
| R-5 | A JVM-less corpus job must still render a real verdict (FR-019). | A tooling dependency leaks into an unrelated job, or the test skips. | OQ-1 resolves the mechanism within the constraint that the existing corpus jobs are not edited; never skip; a non-pytest parity script in the contracts workflow proves any second resolution path equal. |
| R-6 | The leak guarantee relies on shape and scan rules, and a handle-shaped string can still be an account name; the status domain has recorded OS and git user names in actor-like fields before. | Residual identity exposure through handles. | Handle pattern plus value scan plus corpus assertion that known e-mail sources project to null; residual risk stated in the README; no claim of a stronger guarantee. |
| R-7 | The issue-matrix scaffold collects #N tokens from the spec and plan prose as issue rows. | Approval refusals for prose such as clarification numbers. | The spec cites only real upstream issue and PR numbers (#5528, #5558, #5529, #957, #460, #5533, #5231, #5545, #5557; each was checked against the upstream tracker) and names its own decisions CL-n and D-n; each cited number gets an honest verdict row, with a not-applicable verdict for the two PRs. |
| R-8 | The requirement parser reads every FR- token in the spec as a local requirement. | A false unmapped requirement at finalize. | Only FR ids defined in this spec appear in the prose; no other Mission's requirement ids are cited. |
| R-9 | execution lane is not a glossary term; the glossary defines Code lane and Repo-root lane (docs/context/topology.md), and its lane is the status meaning (with two glossary surfaces disagreeing on the lane list; see Terminology). | Residual terminology ambiguity in code and docs outside this Mission. | The contract and README use the glossary terms and keep execution lane only as a stated umbrella phrase tied to docs/architecture/execution-lanes.md; the glossary was checked for the terms used; adding the term, or fixing the stale Lane entry, is out of scope and noted for the maintainers. |
| R-10 | Gradle wrapper binaries are committed binaries, a Go binary download needs network, and the Gradle build resolves the generator plugin and its transitive JVM artefacts at run time. | Supply-chain and reproducibility concerns; an unverified transitive artefact would make the sha256-pinned bundle depend on unverified resolution. | Prefer a pinned Gradle distribution with distributionSha256Sum and a pinned setup action over a committed wrapper jar; Gradle dependency verification with a committed metadata file in strict mode, with a tampered-checksum fixture; FR-018. |
| R-11 | prose_only PR classification in the router treats corpus globs as doc paths. | Adding contracts/** to the corpus group changes how a mixed contracts plus docstring-only Python change is classified. | The plan reads scripts/ci/prose_only.py and scripts/ci/gate_selection.py and records the effect; a guard or test pins any behaviour that changes. |
| R-12 | The release job cannot be fully exercised from a PR (publication needs a tag; a manually dispatched workflow resolves from the default branch), and the PR dry run is a job of the contracts workflow, not of the release workflow (DEV-2). | An untested publication step, and release-workflow YAML that the PR run does not execute. | The dry run on the PR branch proves the shared release script and the build, everything except the publication call; the release workflow's steps are thin calls to the same script and wrapper, and its tag, namespace and publish-step conditions are guard-tested; the post-merge close-out step verifies the real release (FR-022). |
| R-14 | Reader drift is not selected by the corpus job. The router's corpus filter (ci-router.yml, the corpus: group) names packs/, Mission spec, plan, tasks, contracts and acceptance-matrix paths and .kittify governance paths; with the added contracts/ glob it still names none of the readers the contract tracks (src/specify_cli/status/, src/specify_cli/mission_metadata.py, src/mission_runtime/, src/specify_cli/core/dependency_graph.py), nor the reality check, its helper or the contracts/tools/ library under tests/contract/**. | A PR that edits a reader, or only the reality check or its helper, selects module shards, not tests-corpus; reader drift that breaks the contract is caught on the next push to main (the packs built-in-corpus-suite is forced on for every push to main, D-16 item 5) or on the next contract or Mission-data PR, not before merge. This is the failure the reality check exists to prevent, caught late. | Accepted residual, because C-003 and SC-008 forbid widening the router beyond the one glob and a wider filter would run a whole-corpus check on most src/ PRs. Mitigations: the push-to-main run; the README tells reader authors to run the reality check locally (one command) when they change a status reader; the PR body names the uncovered reader paths so a maintainer can choose to add them, with the test files, to the corpus group later (a router edit outside this Mission). |
| R-13 | Existing quirks (blocked-only reads as planned; all-canceled and accepted reads as done; tasks status counts differently) are pinned, not fixed. | A consumer may expect saner semantics. | Documented in the schema descriptions and the CHANGELOG as v1 behaviour; a later minor or major version may change them. |
Open Questions (plan phase; none block the spec)
Two items remain open; neither blocks the spec or its review. OQ-1 is a plan-level mechanism with a constraint the plan must satisfy; OQ-4 is a maintainer confirmation request. No requirement depends on either answer beyond that constraint and the DEV notes. The two questions that were open in an earlier draft are resolved and recorded as decisions: where the release dry run's evidence comes from (D-17) and the unscoped event stream (D-18); the numbering OQ-2 and OQ-3 is retired, not reused.
- OQ-1 Contract resolution in the reality check. The reality check must give a real verdict in the jobs that select the
corpusmarker, which have no JVM, but the bundle comes from the JVM build. Giving those jobs a JDK and a Gradle step is excluded: it would edit existing jobs (the router'stests-corpus, the packs workflow'sbuilt-in-corpus-suite), against C-003, FR-017 and SC-008. The plan therefore chooses between resolving the split files in Python with a reference resolver whose output is proven equal to the CI bundle by the non-pytest parity script in the contracts workflow, or another route that edits no existing job. Whichever it chooses, the resolver is the onecontracts/tools/contract_resolver.pythat the parity script also imports (FR-019). Constraints: no skip; no second unproven resolution authority; the verdict is real in every job that selects thecorpusmarker.
- OQ-4 Maintainer confirmation of DEV-1 and DEV-2 (non-blocking). The maintainer is asked, in the body of PR 1 and on #5528, to confirm or overturn two realised readings: DEV-1 (CL-2: the reality check runs once in
tests-corpus, and the contracts workflow proves resolver parity instead of running pytest) and DEV-2 (CL-4: the PR-green release dry run is a job of the contracts workflow, and the release workflow has nopull_requesttrigger). Work proceeds on the realised forms; the affected requirements are listed in CL-2 and in the DEV-2 note, so an overturn is a bounded edit. It does not block the spec, the plan or the draft PRs.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001 (FR-001 to FR-009, FR-013, FR-014, NFR-002): The bundle built from a clean checkout contains exactly the five v1 path items and zero unresolved file references; two consecutive builds have equal sha256. — [build] · evidence a no-op cannot produce: the bundle itself and its two builds' digests
- SC-002 (FR-003, FR-004, FR-005, FR-006, FR-008, FR-009, FR-019, NFR-001): The reality check validates at least the floors of FR-019 (500 Missions, 3000 file-backed work package payloads, a summed own-directory snapshot work package count of 2800; measured at delivery from a
tests-corpuslog and recorded, the survey counts 538 Missions and 3125 files), with zero failures and zero exclusions, each other floor in FR-019 holds, and the snapshot-versus-files disagreement list is printed and at or below its pinned ceiling, and the resolver-parity script (FR-019) passes in the contracts workflow with its three counts above their floors. — [build] · evidence a no-op cannot produce: floors, controls and the equality assertions - SC-003 (FR-014, FR-015, FR-016, FR-018, FR-019, FR-021, FR-024, FR-025, NFR-003): Every check listed in FR-021 has evidence of its class, with one passing control run and an asserted failure message: class A (every check run by the contracts workflow, resolver parity included) has at least one linked, real failing run on a planted violation; class B (the reality check and the
tests/ci/guard tests, which are pytest tests) has a green run of the in-test planted case, whose test ids are listed in the PR body. None is demonstrated only by a local run. — [build] · evidence a no-op cannot produce: the linked failing runs and the listed in-test cases - SC-004 (FR-010, FR-011): 100 percent of properties in resource schemas, at any depth, carry
x-sourceorx-derived(thex-sourceandx-derivedcounts sum to the computed property count, and both counts are printed), everyx-derivedhas a non-empty rule and non-empty inputs, 100 percent of cited paths, symbols andx-derivedinputs resolve in the checkout, and every provisional element carriesx-provisional. — [build] · evidence a no-op cannot produce: the printed checked and total counts - SC-005 (FR-012, NFR-004): Zero absolute host paths and zero e-mail addresses across the contract tree,
examples/and all reality-check payloads (excluding authoredpromptMarkdownof corpus-derived payloads), with the scan's positive controls detecting planted values of each kind in each field class. — [build] · evidence a no-op cannot produce: the scan floors and controls - SC-006 (FR-022): The release dry run (the contracts workflow's
release-dry-runjob) is green in PR 4 (WP08 and WP09) and its artifact contains a non-empty bundle and anopenapi.yaml.sha256that passessha256sum -c; the publish-step guard shows publication unreachable on apull_request. — [build] · evidence a no-op cannot produce: the dry-run artifact - SC-007 (FR-017, FR-020, NFR-005): On a
pull_requestdiff touchingcontracts/**and no packsbuilt_inpath, the reality check's test id executes in exactly one blocking per-change job (tests-corpus); on a push tomainand a mixed diff the packsbuilt-in-corpus-suitealso runs it by existing design and it passes there; and the named gate files (corpus trigger completeness, no duplicate suite execution, workflow coherence, fork guard, fleet verdict, fleet main, workflow-count ceiling) pass with every ledger or allowlist addition, expected to be none, carrying an owner, a rationale and an exit condition. — [build] · evidence a no-op cannot produce: the named job log and the gate runs - SC-008 (FR-017, FR-020, C-002 to C-005): Checked twice, over the stack's cumulative diff against
origin/mainand over each PR's own diff against its base (origin/mainfor PR 1, the previous seam head for PRs 2 to 6; the router glob line appears in PR 1 only). The diff touches zero files undersrc/, zero files undercontracts/fixtures/, zero lines of.github/ci-module-registry.ymland ofpytest.ini, and adds exactly one glob line to.github/workflows/ci-router.yml; its only other edits to shared CI are the four fleet-verdict files of C-003, limited to the changes listed in FR-017 (onePR_WORKFLOWSentry, oneworkflow_runname, and the two test files), withscripts/ci/fleet_main.py,_CORPUS_GLOBSand_CORPUS_DATA_ROOTSuntouched; no Node tooling appears anywhere in the diff. — [build] · no-op passable: yes on its own, because an empty diff satisfies it; it is evidence only in conjunction with SC-007 and SC-002, which a diff that does nothing cannot satisfy (see Traceability) - SC-009 (FR-022): Post-merge close-out: the tag
contract-mission-status-v1.0.0exists, its release has exactly the bundle and its sha256, the downloaded checksum verifies, and the digest equals a locally rebuilt bundle; recorded in the Mission close-out record, not a follow-up issue. — [build] · evidence a no-op cannot produce: the published release - SC-010 (FR-023, C-001): All six PRs are drafts until a maintainer acknowledgement is linked on #5528,
.github/CODEOWNERSroutescontracts/to both handles (PR 3 states review is advisory), and every PR body follows the repository's five-section PR body contract and lists the baseline of known-red tests used for judging; PR 1 (and #5528) carries DEV-1 and DEV-2 as confirmation requests. The per-PR evidence is assigned in the plan ("PR stack"). — [build] · evidence a no-op cannot produce: the linked acknowledgement and the CODEOWNERS check
Traceability
Every FR appears in at least one SC, or names the acceptance criterion that is its only evidence; the table is the check.
| SC | Requirements it evidences |
|---|---|
| SC-001 | FR-001, FR-002, FR-003, FR-004, FR-005, FR-006, FR-007, FR-008, FR-009, FR-013, FR-014, NFR-002 |
| SC-002 | FR-003, FR-004, FR-005, FR-006, FR-008, FR-009, FR-019, NFR-001 |
| SC-003 | FR-014, FR-015, FR-016, FR-018, FR-019, FR-021, FR-024, FR-025, NFR-003 |
| SC-004 | FR-010, FR-011 |
| SC-005 | FR-012, NFR-004 |
| SC-006 | FR-022 |
| SC-007 | FR-017, FR-020, NFR-005 |
| SC-008 | FR-017, FR-020, C-002, C-003, C-004, C-005 |
| SC-009 | FR-022 |
| SC-010 | FR-023, C-001 |
Requirements and constraints that are not an SC's subject have a named evidence: NFR-001 is a recorded measurement with thresholds the reviewer checks in the body of PR 5 (WP10), measured by WP12 T076 and written into that draft body by the orchestrator; NFR-006 is checked by the README's one documented command; NFR-007 by the recorded commands and the local coverage output pasted in PR 5; C-006 and C-007 by the text-level scan and the review.