Contracts
README.md
Contracts — Charter Contract Cleanup Tranche 1
This directory carries the user-visible contracts the mission introduces or hardens. Each file is normative; tests and PR-time review check against them.
| Contract | File | Owns |
|---|---|---|
| Charter synthesize JSON envelope | synthesis-envelope.schema.json | Stable shape of spec-kitty charter synthesize ... --json stdout. Required fields, dry-run/non-dry-run parity, PROJECT_000 exclusion |
| Golden-path envelope assertions | golden-path-envelope-assertions.md | What tests/e2e/test_charter_epic_golden_path.py asserts about issued-action and blocked-decision envelopes |
e2e-cross-cutting mypy availability | ci-job-mypy-availability.md | The CI environment contract for tests/cross_cutting/test_mypy_strict_mission_step_contracts.py |
Cross-references:
- spec.md — mission spec (FRs, NFRs, acceptance criteria)
- research.md — Phase 0 decisions and rationale
- data-model.md — data shapes the contracts reference
- quickstart.md — verification recipe
ci-job-mypy-availability.md
CI Job Contract — e2e-cross-cutting mypy Availability
Mission: charter-contract-cleanup-tranche-1-01KQATS4 Authority: spec.md FR-008 + research.md R-006 (decision_id 01KQAVR8S1299R9N67BTFAD67Q)
This document is the contract between tests/cross_cutting/test_mypy_strict_mission_step_contracts.py and the CI job that runs it.
Job: e2e-cross-cutting
File: .github/workflows/ci-quality.yml
Required environment (post-mission):
- Python 3.12 (unchanged).
- Project installed via
pip install -e .[test,lint]so thelintextra (which already pinsmypy>=1.10.0) is available. python -m mypy --strict src/specify_cli/mission_step_contracts/executor.pyexits 0 from the repository root.
Pre-mission state (the bug being fixed):
- Project installed via
pip install -e .[test]only —mypynot on PATH. - The test fails with
python -m mypy: not found(or equivalent), and the failure surfaces as ane2e-cross-cuttingjob failure rather than a strict-typing regression.
Acceptance:
tests/cross_cutting/test_mypy_strict_mission_step_contracts.pypasses inside thee2e-cross-cuttingjob on a PR built from currentmainplus this mission's diff.- No other job in the workflow regresses (NFR-002).
Local-developer contract (informational)
To run the same test locally, developers should already use the documented path:
uv run --extra test --extra lint python -m pytest \
tests/cross_cutting/test_mypy_strict_mission_step_contracts.py -q
This matches the test's docstring and is unaffected by this mission.
golden-path-envelope-assertions.md
Golden-Path E2E Envelope Assertion Contract
Mission: charter-contract-cleanup-tranche-1-01KQATS4 Test surface: tests/e2e/test_charter_epic_golden_path.py Authority: spec.md FR-006, FR-007 + research.md R-005
This document is the assertion contract the Charter golden-path E2E enforces against runtime-emitted lifecycle envelopes. It binds the test, not the producer; the producer's wire format is the upstream truth, and this contract describes what the test inspects.
Issued Action (kind=step)
Discriminator: envelope where the documented public discriminator is "step" (typically kind == "step").
Required of the test, per envelope:
1. Read prompt_file (or the documented public equivalent the runtime guarantees). 2. Assert it is present (key exists), non-null, and non-empty (!= ""). 3. Resolve it to a path. Acceptable shapes:
4. If resolution fails, the test fails with a message that names the issued-action's identifier (envelope ID, step name, or whatever stable handle the envelope carries) and the unresolvable prompt value.
- Path relative to the E2E's test-project root — must
Path(test_project_root, prompt).is_file()or equivalent. - Absolute path — must
Path(prompt).is_file(). - Documented shipped-prompt-artifact path that the runtime guarantees exists in the installed package — must resolve via the same lookup the runtime would use.
Permitted multiplexing: if the runtime emits more than one stable field that may carry a prompt path (e.g. prompt_file and a prompt.path sub-object), the assertion treats "at least one of them carries a resolvable prompt file" as success. The test must not hard-code internal field names beyond what the runtime publicly documents.
Non-required: the test does not assert prompt file content. Resolvability is the contract.
Blocked Decision
Discriminator: envelope where the existing runtime indicator marks the decision as blocked (whatever flag/sub-object the runtime publicly defines for that state).
Required of the test, per envelope:
1. Read reason. 2. Assert it is present, non-null, and reason.strip() != "". 3. Do NOT assert anything about prompt_file. Blocked decisions may carry one or none; either is acceptable.
Failure mode: a blocked decision with missing/null/whitespace-only reason causes the test to fail with a message naming the offending decision and (if available) which step it blocked.
All Other Envelope Kinds
Existing assertions in tests/e2e/test_charter_epic_golden_path.py are unchanged. This contract adds the prompt-file/reason invariants without rewriting the rest of the test.
Producer-side note (non-binding for this mission)
If the runtime ever changes the public discriminator or the prompt-path field, the test contract above is what governs the consumer. Producer-side changes that move the field name without a deprecation cycle would break this contract; this mission does not introduce any such producer-side change.
synthesis-envelope.schema.json
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://spec-kitty.priivacy.ai/contracts/charter/synthesis-envelope.schema.json", "title": "Charter Synthesis CLI JSON Envelope", "description": "Stable contract for stdout of spec-kitty charter synthesize ... --json. The four contracted fields are required on every success and dry-run envelope. Legacy compatibility fields are permitted but not required.", "type": "object", "required": ["result", "adapter", "written_artifacts", "warnings"], "properties": { "result": { "type": "string", "enum": ["success", "failure", "dry_run"], "description": "Outcome label. 'dry_run' is used only when the run was invoked with --dry-run." }, "adapter": { "type": "object", "required": ["id", "version"], "additionalProperties": true, "properties": { "id": { "type": "string", "minLength": 1, "description": "Stable identifier of the synthesis adapter (e.g. 'fixture')." }, "version": { "type": "string", "minLength": 1, "description": "Adapter version string (semver, model name, or snapshot identifier)." } } }, "written_artifacts": { "type": "array", "description": "Artifacts staged or promoted by this run. May be empty. For result == 'dry_run', byte-equal entries to what a real run with the same SynthesisRequest would produce. Sourced from typed staged-artifact entries; never derived from kind:slug reconstruction.", "items": { "$ref": "#/$defs/WrittenArtifact" } }, "warnings": { "type": "array", "description": "Non-fatal warnings (e.g. evidence-gathering warnings). Stored inside the envelope so callers reading only stdout still see them. May be empty.", "items": { "type": "string" } }, "target_kind": { "type": "string", "description": "Legacy compatibility field (optional)." }, "target_slug": { "type": "string", "description": "Legacy compatibility field (optional)." }, "inputs_hash": { "type": "string", "description": "Legacy compatibility field (optional)." }, "adapter_id": { "type": "string", "description": "Legacy compatibility field (optional, duplicates adapter.id)." }, "adapter_version": { "type": "string", "description": "Legacy compatibility field (optional, duplicates adapter.version)." } }, "additionalProperties": true, "$defs": { "WrittenArtifact": { "type": "object", "required": ["path", "kind", "slug", "artifact_id"], "additionalProperties": true, "properties": { "path": { "type": "string", "minLength": 1, "description": "Repo-relative POSIX path of the staged-or-promoted artifact. For result == 'dry_run', byte-equal to the path the non-dry-run would write." }, "kind": { "type": "string", "minLength": 1, "description": "Doctrine kind (e.g. 'directive', 'tactic', 'styleguide')." }, "slug": { "type": "string", "minLength": 1, "description": "Slug component used in the artifact filename." }, "artifact_id": { "type": ["string", "null"], "description": "Concrete artifact identifier. Non-null for kinds that carry an ID. MUST NOT be the placeholder 'PROJECT_000' on user-visible output.", "not": { "const": "PROJECT_000" } } } } } }