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.

ContractFileOwns
Charter synthesize JSON envelopesynthesis-envelope.schema.jsonStable shape of spec-kitty charter synthesize ... --json stdout. Required fields, dry-run/non-dry-run parity, PROJECT_000 exclusion
Golden-path envelope assertionsgolden-path-envelope-assertions.mdWhat tests/e2e/test_charter_epic_golden_path.py asserts about issued-action and blocked-decision envelopes
e2e-cross-cutting mypy availabilityci-job-mypy-availability.mdThe 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 the lint extra (which already pins mypy>=1.10.0) is available.
  • python -m mypy --strict src/specify_cli/mission_step_contracts/executor.py exits 0 from the repository root.

Pre-mission state (the bug being fixed):

  • Project installed via pip install -e .[test] only — mypy not on PATH.
  • The test fails with python -m mypy: not found (or equivalent), and the failure surfaces as an e2e-cross-cutting job failure rather than a strict-typing regression.

Acceptance:

  • tests/cross_cutting/test_mypy_strict_mission_step_contracts.py passes inside the e2e-cross-cutting job on a PR built from current main plus 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" } } } } } }