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_PATTERNScover every registration-writing kind:directive, tactic, styleguide, procedure, agent_profile(agent_profile → .agent.yaml), derived from the module-levelDIRECT_WRITE_KINDSconstant.DIRECT_WRITE_KINDSlives insrc/charter/offering/artifact_kinds.py(leaf module).scan_project_artifacts(project_scan.py), theManifestArtifactEntry.kindLiteral(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
Literalall equalDIRECT_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 kinderror.
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 sluglove-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_pathset and on-disk artifacts against the manifestpathset. 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 testtest_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.yamlwith noghost.agent.yamlstill fails; (b) an on-diskorphan.directive.yamlabsent 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 whenprevious 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_pathORprevious.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_manifestraise forever. slugreuse caveat (squad MEDIUM→HIGH): where the code reusesprevious.slugfor an existing entry, it must reconcile againstslug_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_pathdiffers 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 validateis 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_directivesconfig-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, soprovenance_path_for(kind, slug)cannot escape.kittify/charter/provenance/. Dropping it regresses the existing guardtests/charter/test_project_registration.py:165-166(asserts the encodedteam%2Fops-responderform 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.pyenforces). Under this grammarquote()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 optionalNNN-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 callslug_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).