Contracts

adversarial-evidence-contract.md

Contract: adversarial evidence

Every adversarial-squad point-cut (post-plan, post-tasks, pre-merge) that surfaces a contested finding records its disposition in research.md (or the WP's notes for post-tasks/pre-merge). No contested finding is silently dropped.

Disposition vocabulary

  • accepted — the finding is valid; the plan/spec/code is changed to address it. Record the change.
  • changed — the finding is partially valid; record what was adjusted and what was not.
  • deferred_with_rationale — the finding is valid but out of scope for this mission; record the rationale and, where warranted, the follow-up ticket number.

Rule

A finding may only be closed as deferred_with_rationale with an explicit reason. "Rejected" requires stating why the finding does not hold (evidence), not silence.

bundle-validate-contract.md

Contract: charter bundle validate — kind coverage, manifest-driven resolution, corruption honesty

Kind coverage (#4833) behind one importable SSOT

  • _KIND_SUFFIX / _ALL_ARTIFACT_PATTERNS cover every registration-writing kind: directive, tactic, styleguide, procedure, agent_profile (agent_profile → .agent.yaml), derived from the module-level DIRECT_WRITE_KINDS constant.
  • DIRECT_WRITE_KINDS lives in src/charter/offering/artifact_kinds.py (leaf module). scan_project_artifacts (project_scan.py), the ManifestArtifactEntry.kind Literal (manifest.py), and this table all reference it — no hand-duplicated tuple (post-plan squad MEDIUM: the kind list was a non-importable function-local literal).
  • Parity guard (NFR-002): a test asserts the validator kind set, the scanner kind set, and the manifest Literal all equal DIRECT_WRITE_KINDS. Adding a future kind without updating the constant fails the guard (drift closed by construction, DIRECTIVE_043).
  • Behavior: activating a project agent profile, or registering a project procedure, produces no Provenance file has unknown kind error.

Hybrid resolution (#4832 durable) — manifest for registered, filesystem for orphans

  • Positive resolution: for a registered artifact, resolve (kind, slug, path, provenance_path) through the synthesis manifest entry — NOT by re-parsing the on-disk filename.
  • Behavior: a registered directive whose filename is still LOVE_THY_ENEMY.directive.yaml (manifest slug love-thy-enemy) validates clean — no "has no provenance sidecar", no "references non-existent artifact" — with the file unmodified (go-forward: no rename needed).
  • Orphan sweep stays filesystem-driven (post-plan squad MEDIUM, both directions): compare on-disk sidecars against the manifest provenance_path set and on-disk artifacts against the manifest path set. The switch is hybrid, not "fully manifest-driven" — a manifest-only walk cannot see an on-disk artifact/sidecar absent from the manifest.
  • The <NNN>- digit-strip in _kind_and_slug_from_artifact (and its test test_bundle_validate_extension.py:500) is retained for the orphan/legacy path, or retired deliberately with the parse it guards — not silently broken.

Corruption honesty preserved (NFR-001 — both directions, must not weaken)

  • A provenance sidecar with no backing artifact still errors ("references non-existent artifact").
  • An artifact with no provenance sidecar (and no manifest entry) still errors ("has no provenance sidecar") — this is the orphan-artifact direction the hybrid sweep must keep catching.
  • Tests (guards): (a) an orphaned agent_profile-ghost.yaml with no ghost.agent.yaml still fails; (b) an on-disk orphan.directive.yaml absent from the manifest with no sidecar still fails. Both prove the kind-table + manifest switch did not trade a false-positive for a false-negative in either direction.

Backward compatibility (NFR-003)

  • A legacy project with no synthesis state still passes charter bundle validate (existing C-012-style contract unchanged).

registration-path-update-and-migration-contract.md

Contract: registration path-update (#4834) — independent correctness fix

> Scope change (post-plan squad): the filename migration/self-heal was dropped (go-forward only). Existing SCREAMING-filename repos validate green via the hybrid manifest-driven validator (see bundle-validate-contract.md), with authored files untouched — so _registration_records performs no rename, and the registration module's "never mutate authored source" invariant (C-004) is preserved. This contract therefore covers only the #4834 path-update fix, retained as a standalone latent-bug fix.

Path-update on drift (#4834 / FR-007)

  • _registration_records (src/charter/activation/project_registration.py:195) early-continues when previous and previous.content_hash == content_hash and (root/previous.provenance_path).is_file(), skipping the manifest-entry rebuild at ~L216 entirely.
  • New rule: even when the content hash is unchanged, the manifest entry is re-written when previous.path != resolved_path OR previous.provenance_path != resolved_provenance_path.
  • Behavior: if an artifact's resolved path or provenance path drifts on unchanged content, the manifest is corrected in a single registration pass — no sidecar deletion, no half-stale entry that would make verify_manifest raise forever.
  • slug reuse caveat (squad MEDIUM→HIGH): where the code reuses previous.slug for an existing entry, it must reconcile against slug_for(kind, id) and not perpetuate a legacy non-canonical slug. Under today's data this is benign (the engine has always kebab'd directive slugs), but the reconciliation is stated so a future non-canonical manifest slug cannot silently survive.

Test (red-first)

  • Seed a manifest entry whose path/provenance_path differs from the current resolved values on an artifact with unchanged content; run one registration pass; assert the manifest entry is updated (not skipped by the early-continue).
  • Confirm verify_manifest / charter bundle validate is green afterward, and no sidecar was deleted.

Explicitly out of scope (dropped)

  • No rename of authored artifact files. No clobber/dirty-tree/crash-atomicity migration machinery. No activated_directives config-stem rewrite. These were the migration's risks; dropping the migration removes them.

slug-authority-contract.md

Contract: slug authority (slug_for) + cross-surface convergence

slug_for(kind: str, identifier: str) -> str

  • Pure: deterministic, no IO, no global state. Same (kind, identifier) → same slug.
  • Rule (extracted verbatim from src/charter/activation/project_registration.py:187):
  • kind == "directive" → quote(identifier.lower().replace("_", "-"), safe="").
  • any other kind → quote(identifier, safe="").
  • quote(…, safe="") is load-bearing, not decoration (post-plan squad CRITICAL): it collapses a slash-bearing URN identity (e.g. agent_profile:team/ops-responder) into a single safe path component, so provenance_path_for(kind, slug) cannot escape .kittify/charter/provenance/. Dropping it regresses the existing guard tests/charter/test_project_registration.py:165-166 (asserts the encoded team%2Fops-responder form and the pinned parent dir). Do NOT "simplify" it away.
  • id-grammar precondition: directives match ^[A-Z][A-Z0-9_-]*$; non-directive ids are lowercase-kebab and URN-safe (project_scan.py enforces). Under this grammar quote() is a no-op for well-formed ids, but it is retained as defense-in-depth.
  • Guarantee: for a directive id matching ^[A-Z][A-Z0-9_-]*$, the result never begins with a pure-digit segment (ids require a leading letter), so a downstream optional NNN- prefix strip cannot truncate it.

Convergence invariant (the anti-drift guarantee)

For every registration-writing kind and any valid id:

scaffolder_filename_stem(kind, id) == slug_for(kind, id)
registration_slug(kind, id)        == slug_for(kind, id)
validator_slug_for(registered artifact) == manifest.slug   # not re-derived from filename
  • The scaffolder (doctrine.py::_artifact_filename) and the engine (project_registration.py) both call slug_for — there is exactly one producer-side slug derivation.
  • Test: a parametrized convergence test asserts scaffolder == engine == manifest slug for a SCREAMING directive id and for a kebab non-directive id.

Non-directive preservation (C-002)

slug_for("agent_profile", "retrospective-facilitator") == "retrospective-facilitator" (already-kebab id, quote a no-op). No case-folding of non-directive kinds. For a namespaced id, slug_for("agent_profile", "team/ops-responder") == "team%2Fops-responder" (quoted — no path escape).