Contracts

bundle-manifest.schema.yaml

JSON Schema (draft-7) for CharterBundleManifest

#

Authoritative contract consumed by src/charter/bundle.py, the migration at

src/specify_cli/upgrade/migrations/m_3_2_3_unified_bundle.py, and by

tests/charter/test_bundle_contract.py.

#

Schema version: 1.0.0 (D-1 / Q3=A — versioned independently of spec-kitty package).

#

v1.0.0 scope (narrowed after design-review 2026-04-14):

The manifest v1.0.0 declares ONLY the files that src/charter/sync.py

materializes today: governance.yaml, directives.yaml, metadata.yaml.

(These are listed in src/charter/sync.py:32-36 as _SYNC_OUTPUT_FILES.)

#

references.yaml is produced by src/charter/compiler.py :: write_compiled_charter

— a separate pipeline invoked by spec-kitty charter generate, NOT by sync().

#

context-state.json is written lazily by src/charter/context.py ::

build_charter_context() as runtime state, NOT by sync().

#

Both are out of v1.0.0 scope by design. Expanding the manifest to cover them

requires a corresponding change to the chokepoint's completeness contract

and is deferred to a later tranche. The project .gitignore may continue to

list them; the manifest's gitignore_required_entries is a "must include"

set, not an exclusive "only these" set.

$schema: "http://json-schema.org/draft-07/schema#" $id: "https://spec-kitty.dev/schemas/charter-bundle-manifest/1.0.0" title: CharterBundleManifest description: > Declares the files under .kittify/charter/ that src/charter/sync.py materializes, their tracked-vs-derived classification, derivation sources, and required .gitignore entries. The chokepoint (ensure_charter_bundle_fresh) uses this manifest as the authority for "what files must exist" during the completeness check. v1.0.0 scope is limited to the sync-produced artifacts. type: object additionalProperties: false required:

properties: schema_version: type: string pattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$" description: > Semver of the manifest schema itself. Bumped only when the manifest shape or scope changes in a way that requires a new migration. Independent of the spec-kitty package version. examples:

tracked_files: type: array minItems: 1 uniqueItems: true items: type: string description: Path relative to the project root. Must exist and be git-tracked. description: > Every file the bundle requires to be committed to version control. For v1.0.0: [".kittify/charter/charter.md"]. derived_files: type: array uniqueItems: true items: type: string description: Path relative to the project root. Regenerated by sync(); must be gitignored. description: > Every file that src/charter/sync.py materializes. For v1.0.0 the set is exactly governance.yaml, directives.yaml, metadata.yaml. references.yaml and context-state.json are explicitly out of scope (they are produced by different pipelines). derivation_sources: type: object description: > Maps each derived file to the tracked source file it is derived from. Every key must appear in derived_files; every value must appear in tracked_files. additionalProperties: type: string description: Path of a tracked source file. gitignore_required_entries: type: array uniqueItems: true items: type: string description: > Exact-match string that must appear on its own line in the project .gitignore for the derived files to be correctly ignored. The project .gitignore may contain additional entries (for files outside the v1.0.0 manifest scope, e.g. context-state.json, references.yaml); the manifest does not forbid those. description: > Strings that .gitignore interprets as glob patterns; the manifest treats them as opaque exact-match strings. This is a MUST-INCLUDE set, not an ONLY-THESE set.

  • schema_version
  • tracked_files
  • derived_files
  • derivation_sources
  • gitignore_required_entries
  • "1.0.0"

Example payload — the v1.0.0 CANONICAL_MANIFEST:

#

schema_version: "1.0.0"

tracked_files:

- ".kittify/charter/charter.md"

derived_files:

- ".kittify/charter/governance.yaml"

- ".kittify/charter/directives.yaml"

- ".kittify/charter/metadata.yaml"

derivation_sources:

".kittify/charter/governance.yaml": ".kittify/charter/charter.md"

".kittify/charter/directives.yaml": ".kittify/charter/charter.md"

".kittify/charter/metadata.yaml": ".kittify/charter/charter.md"

gitignore_required_entries:

- ".kittify/charter/directives.yaml"

- ".kittify/charter/governance.yaml"

- ".kittify/charter/metadata.yaml"

bundle-validate-cli.contract.md

Contract: spec-kitty charter bundle validate

CLI surface: new Typer subcommand under src/specify_cli/cli/commands/charter.py Introduced in: WP2.1 Companion: plan.md §D-4, spec.md Q4=B resolution v1.0.0 scope: validates the three sync()-produced derivatives (governance.yaml, directives.yaml, metadata.yaml) plus charter.md. Does NOT validate references.yaml, context-state.json, or the interview/ or library/ subtrees (explicitly out of v1.0.0 manifest scope; other pipelines own them).


Command

spec-kitty charter bundle validate [--json]

Arguments

None.

Options

FlagDefaultDescription
--jsonFalseEmit structured JSON to stdout instead of the human-readable report. Exit code unchanged.

Exit codes

CodeMeaning
0Bundle at .kittify/charter/ fully complies with CharterBundleManifest.CANONICAL_MANIFEST v1.0.0.
1Bundle is non-compliant (missing tracked files, missing derived files, or gitignore drift on a required entry). Details on stdout.
2Canonical-root resolution failed (path not inside a repo, or git missing). Details on stderr.

Behavior

1. Resolve the canonical project root: resolve_canonical_repo_root(Path.cwd()).

2. Load CharterBundleManifest.CANONICAL_MANIFEST from src/charter/bundle.py (v1.0.0). 3. Validate the bundle on disk (v1.0.0 scope):

4. Emit the report.

  • On NotInsideRepositoryError or GitCommonDirUnavailableError: exit 2 with the exception message on stderr (loud failure per C-001).
  • For every path in manifest.tracked_files: assert it exists at canonical_root / path and is tracked in git (via git ls-files).
  • For every path in manifest.derived_files: assert it exists at canonical_root / path unless the bundle is in a fresh-clone state (charter.md exists but no derivatives). Fresh-clone state is acceptable: the command does NOT trigger the chokepoint; it only reports.
  • For every path in manifest.gitignore_required_entries: assert the entry appears on its own line in canonical_root / ".gitignore".
  • Enumerate files under canonical_root / ".kittify/charter/" that are not declared by the manifest; treat as warnings (not failures), because v1.0.0 manifest scope is intentionally narrow and users may place ancillary files there (notably references.yaml, context-state.json, interview/answers.yaml, library/*.md). The warning text names the file and explains it is out of v1.0.0 manifest scope.

JSON output shape (--json)

{
  "result": "success",
  "canonical_root": "/absolute/path/to/repo",
  "manifest_schema_version": "1.0.0",
  "bundle_compliant": true,
  "tracked_files": {
    "expected": [".kittify/charter/charter.md"],
    "present": [".kittify/charter/charter.md"],
    "missing": []
  },
  "derived_files": {
    "expected": [".kittify/charter/governance.yaml", ".kittify/charter/directives.yaml", ".kittify/charter/metadata.yaml"],
    "present": [".kittify/charter/governance.yaml", ".kittify/charter/directives.yaml", ".kittify/charter/metadata.yaml"],
    "missing": []
  },
  "gitignore": {
    "expected_entries": [".kittify/charter/directives.yaml", ".kittify/charter/governance.yaml", ".kittify/charter/metadata.yaml"],
    "present_entries": [".kittify/charter/directives.yaml", ".kittify/charter/governance.yaml", ".kittify/charter/metadata.yaml"],
    "missing_entries": []
  },
  "out_of_scope_files": [".kittify/charter/references.yaml", ".kittify/charter/context-state.json"],
  "warnings": [
    "File '.kittify/charter/references.yaml' is present but out of v1.0.0 manifest scope (produced by the compiler pipeline); leaving untouched.",
    "File '.kittify/charter/context-state.json' is present but out of v1.0.0 manifest scope (runtime state written by build_charter_context); leaving untouched."
  ]
}

On failure:

{
  "result": "failure",
  "canonical_root": "/absolute/path/to/repo",
  "manifest_schema_version": "1.0.0",
  "bundle_compliant": false,
  "tracked_files": { "expected": [".kittify/charter/charter.md"], "present": [], "missing": [".kittify/charter/charter.md"] },
  "derived_files": { "expected": [...], "present": [...], "missing": [".kittify/charter/governance.yaml"] },
  "gitignore": { "expected_entries": [...], "present_entries": [...], "missing_entries": [".kittify/charter/governance.yaml"] },
  "out_of_scope_files": [],
  "warnings": []
}

Human-readable output (no --json)

Charter bundle validation
  Canonical root: /Users/alice/my-project
  Manifest schema: 1.0.0

  Tracked files:
    [OK] .kittify/charter/charter.md

  Derived files (v1.0.0 scope):
    [OK] .kittify/charter/governance.yaml
    [OK] .kittify/charter/directives.yaml
    [OK] .kittify/charter/metadata.yaml

  Gitignore:
    [OK] 3 required entries present

  Out-of-scope files present (informational):
    .kittify/charter/references.yaml
    .kittify/charter/context-state.json

Bundle is compliant (v1.0.0).

Rendered with rich for colour highlighting. OK → green; MISSING → red; warnings → yellow.


Non-goals

  • Not auto-repairing the bundle. This command validates only. The chokepoint is the repair mechanism; operators who want repair run spec-kitty charter sync or any command that exercises the chokepoint.
  • Not integrating with spec-kitty doctor. Per Q4=B user decision, the thin-wrapper shape is intentional.
  • Not scanning worktrees. The command validates only the main-checkout bundle. Worktrees read the same bundle via canonical-root resolution.
  • Not providing a --fix flag. Separation of concerns.
  • Not validating references.yaml / context-state.json. Explicitly out of v1.0.0 manifest scope. A future manifest version may broaden this.

Test surface

TestWPFocus
tests/charter/test_bundle_manifest_model.pyWP2.1Unit tests for the underlying manifest validation logic (v1.0.0 scope).
CLI integration test under tests/charter/ (new module, name TBD in WP2.1)WP2.1End-to-end invocation of spec-kitty charter bundle validate against a fixture project; asserts exit code and JSON shape, including out-of-scope-files / warnings behavior.

canonical-root-resolver.contract.md

Contract: resolve_canonical_repo_root()

Module: src/charter/resolution.py (new in WP2.2, per D-2 / Q1=A) Companion: plan.md §D-2, spec.md FR-003, C-009, NFR-003, research.md §R-2


Function signature

def resolve_canonical_repo_root(path: Path) -> Path:
    """
    Resolve `path` to the canonical (main-checkout) project root.

    Uses `git rev-parse --git-common-dir` to locate the shared git directory
    for the given path, then returns the parent of that directory — the main
    checkout — regardless of whether `path` is inside the main checkout or
    inside a linked worktree.

    Args:
        path: Any path (file or directory). May be absolute or relative. File
              inputs are normalized to their parent directory before invocation.

    Returns:
        Absolute path to the canonical project root (the main checkout).

    Raises:
        NotInsideRepositoryError: `path` is not inside any git repository, or
            the resolved input path is inside a `.git/` directory itself.
        GitCommonDirUnavailableError: `git` binary is unavailable, or
            `git rev-parse --git-common-dir` failed for any reason other
            than "not a git repository".
    """

Algorithm (precise)

1. Normalize input:
     input_abs = path.resolve()
     if input_abs is a file:
         cwd = input_abs.parent
     else:
         cwd = input_abs

2. Invoke git:
     result = subprocess.run(
         ["git", "rev-parse", "--git-common-dir"],
         cwd=cwd,
         capture_output=True,
         text=True,
         check=False,
     )
     If subprocess.run raises FileNotFoundError (git binary missing):
         raise GitCommonDirUnavailableError(path, "git binary not found on PATH")

3. Classify exit code:
     if result.returncode != 0:
         if "not a git repository" in result.stderr.lower():
             raise NotInsideRepositoryError(path)
         else:
             raise GitCommonDirUnavailableError(path, result.stderr.strip())

4. Parse stdout:
     raw = result.stdout.strip()
     common_dir = Path(raw)
     if not common_dir.is_absolute():
         common_dir = (cwd / common_dir).resolve()
     else:
         common_dir = common_dir.resolve()

5. Detect "inside .git/" edge case:
     if input_abs == common_dir or common_dir in input_abs.parents:
         raise NotInsideRepositoryError(path)

6. Return:
     canonical_root = common_dir.parent
     return canonical_root

Why common_dir.parent: For the main checkout and for any subdirectory inside it, git rev-parse --git-common-dir returns a path whose parent is the working directory of the main repo. For a linked worktree, it returns the shared git-dir path (absolute), whose parent is also the main checkout's working directory. This is the semantic the contract relies on.


git rev-parse --git-common-dir observed behavior (verified locally 2026-04-14)

Invocation cwdstdoutis_absolute?resolved tocanonical_root
<repo> (main checkout).gitNo<repo>/.git<repo>
<repo>/src/charter (subdirectory)../../.gitNo<repo>/.git<repo>
<repo>/.git (inside git dir).No<repo>/.gitdetected in step 5 → raise NotInsideRepositoryError
<repo>/.worktrees/foo (linked worktree)/abs/path/to/<main>/.gitYes<main>/.git<main>
/tmp (non-repo)empty; exit 128; stderr contains "not a git repository"raise NotInsideRepositoryError
<repo> with submodule-attached checkout at <repo>/sub.git/modules/subNo<repo>/.git/modules/sub<repo>/.git/modules — submodule-specific path, treated as submodule's working directory (see note below)
<repo> with sparse-checkout enabledsame as main checkoutsame as main checkout ✓
<repo> with detached HEADsame as main checkoutsame as main checkout ✓

Submodule note: when path is inside a submodule's working directory, the resolver returns the submodule's working directory via common_dir.parent. The current working directory of the submodule is common_dir.parent.parent (two levels up from .git/modules/<name>), which is the submodule's working tree, distinct from the superproject. Tests in tests/charter/test_canonical_root_resolution.py exercise this case explicitly so the behavior is documented, not surprising.


Caching

  • Cache type: functools.lru_cache(maxsize=256) on a private _resolve_cached(absolute_path_str: str) -> str helper. The public resolve_canonical_repo_root(path: Path) resolves path to its absolute form (normalizing files to parents), stringifies for the cache key, calls _resolve_cached, and re-wraps the result as Path.
  • Cache lifetime: Process lifetime. Tests that mutate the filesystem layout mid-test must clear the cache explicitly via resolve_canonical_repo_root.cache_clear() — the function exposes the cache's cache_clear attribute per functools.lru_cache convention.
  • Rationale: NFR-003 binds the resolver to <5 ms p95 with ≤1 git invocation per call. Cache amortizes the subprocess overhead across the dashboard's hot loop (per-frame charter-read path per NFR-002).

Performance contract (NFR-003)

MetricThreshold
p95 latency per call (cold, first call for a given path)<50 ms (dominated by the single subprocess.run invocation)
p95 latency per call (warm, cached)<5 ms
git invocations per call (warm)0
git invocations per call (cold)1
Cache size bound256 entries (evicted LRU-wise)

tests/charter/test_resolution_overhead.py enforces these thresholds with a subprocess.run spy and a timing harness.


Error-surface contract

NotInsideRepositoryError

class NotInsideRepositoryError(RuntimeError):
    """
    Raised when `resolve_canonical_repo_root(path)` is called with a path
    that is not inside any git repository (or is inside a `.git/` directory
    itself, which the resolver treats as "not a valid project root").
    """
    def __init__(self, path: Path):
        self.path = path
        super().__init__(
            f"Path {path!r} is not inside a git repository. "
            f"Charter resolution requires a git-tracked project root."
        )

GitCommonDirUnavailableError

class GitCommonDirUnavailableError(RuntimeError):
    """
    Raised when `git rev-parse --git-common-dir` cannot be invoked (binary
    missing) or fails with a non-"not a repo" error (corrupt .git, permission
    denied, etc.).
    """
    def __init__(self, path: Path, detail: str):
        self.path = path
        self.detail = detail
        super().__init__(
            f"git rev-parse --git-common-dir failed for {path!r}: {detail}. "
            f"Install a supported git binary and retry."
        )

Neither exception has a fallback handler in ensure_charter_bundle_fresh(). Both propagate to the caller and surface as loud failures per C-001.


Test surface

TestWPFocus
tests/charter/test_canonical_root_resolution.pyWP2.2Behavioral matrix above — one test per row. Also covers: file input normalized to parent; relative-path stdout correctly resolved; absolute-path stdout used as-is; cache hit/miss accounting.
tests/charter/test_resolution_overhead.pyWP2.2NFR-003 performance thresholds (warm/cold, subprocess spy).

Non-goals

  • Not resolving paths across multiple repositories in one call. Each call returns the canonical root for exactly one input path.
  • Not parsing .git/config for core.worktree or similar. Git's --git-common-dir is authoritative.
  • Not providing a "best-effort fallback" when git is missing. Per C-001, the resolver raises loudly.
  • Not watching for filesystem changes. If the user manually deletes .git/ mid-session, subsequent calls will raise; that is correct behavior.
  • Not using --absolute-git-dir or re-invoking git a second time to force absolute output. The contract resolves stdout against cwd in step 4 — one subprocess invocation per cold call.

chokepoint.contract.md

Contract: ensure_charter_bundle_fresh() and extended SyncResult

Module: src/charter/sync.py Introduced in: WP2.2 Consumes: CharterBundleManifest (WP2.1, v1.0.0), resolve_canonical_repo_root() (WP2.2) Referenced by: plan.md §D-3, spec.md FR-003, FR-004, data-model.md Scope (v1.0.0): the chokepoint is authoritative for the files src/charter/sync.py materializes — governance.yaml, directives.yaml, metadata.yaml. It is NOT authoritative for references.yaml (compiler-produced) or context-state.json (runtime-state-produced). This scope is pinned by CharterBundleManifest.SCHEMA_VERSION = "1.0.0". Future manifest versions may broaden scope; those require a new migration.


Function signature

def ensure_charter_bundle_fresh(repo_root: Path) -> SyncResult | None:
    """
    Auto-refresh the unified charter bundle when it is stale or incomplete.

    This is the sole entry point for every reader of the `sync()`-produced
    derivatives (governance.yaml, directives.yaml, metadata.yaml). Direct
    reads of those files that bypass this function are forbidden (enforced by
    tests/charter/test_chokepoint_coverage.py).

    The function resolves `repo_root` to the canonical (main-checkout) project
    root via `resolve_canonical_repo_root()`, then consults
    `CharterBundleManifest.CANONICAL_MANIFEST` for the set of files that must
    exist under the canonical root's `.kittify/charter/` tree. If any required
    file is missing (tracked_files existence + derived_files existence) or the
    bundle is stale (content hash of `charter.md` does not match the hash
    stored in `metadata.yaml`), `sync()` is triggered to regenerate the
    derivatives.

    Args:
        repo_root: Any path inside the project. May be the main checkout or a
            worktree. The function resolves it to the canonical root before
            doing anything else.

    Returns:
        - `SyncResult` on every successful invocation (whether a sync was
          triggered or not).
        - `None` when `charter.md` does not exist at the canonical root — the
          project has no charter and there is nothing to refresh.

    Raises:
        `NotInsideRepositoryError` — `repo_root` is not inside any git repo.
        `GitCommonDirUnavailableError` — the `git` binary is unavailable or
          `git rev-parse --git-common-dir` failed for a non-"not a repo" reason.
    """

Extended SyncResult dataclass (post-WP2.2)

@dataclass(frozen=True)
class SyncResult:
    synced: bool
    stale_before: bool
    files_written: list[Path]      # paths relative to canonical_root
    extraction_mode: str
    error: str | None
    canonical_root: Path           # NEW in WP2.2 — absolute path to canonical project root

Contract for files_written:

  • Every Path in files_written is relative to canonical_root (not absolute, not relative to caller's CWD).
  • To reconstruct an absolute path for a specific written file: canonical_root / files_written[i].
  • For v1.0.0 manifest, files_written entries are drawn from CANONICAL_MANIFEST.derived_files (governance.yaml, directives.yaml, metadata.yaml). A sync that writes fewer than all three leaves the not-written files off the list (e.g., if sync() fails partway through, only the successfully-written files appear).

Contract for canonical_root:

  • Always absolute.
  • Equal to the return value of resolve_canonical_repo_root(repo_root) for the repo_root passed into the chokepoint call.
  • Stable across invocations from the same working directory (per the resolver's LRU cache).

Behavioral contract

Invariant 1: canonical resolution happens first

ensure_charter_bundle_fresh(repo_root) calls resolve_canonical_repo_root(repo_root) as its first operation. Every subsequent file system access is rooted at the resolved canonical root. A reader in a worktree and a reader in the main checkout produce byte-identical SyncResult.files_written lists when the bundle is identical.

Invariant 2: the manifest is authoritative for completeness within its scope

The chokepoint consults CharterBundleManifest.CANONICAL_MANIFEST rather than hard-coding the list of files to check. Adding a file to scope in a future schema version requires exactly one edit: the manifest. The chokepoint does not need to change.

However, the manifest v1.0.0 scope is limited to the sync()-produced files. The chokepoint does not track, regenerate, or validate references.yaml or context-state.json. Readers of those files go through their own pipelines (compiler / context builder respectively); those pipelines may themselves route through the chokepoint to guarantee the upstream derivatives are fresh, but the chokepoint is not responsible for materializing them.

Invariant 3: no path writes happen inside worktrees

When invoked from a worktree path, files_written entries are relative to canonical_root (the main checkout), and any sync() invocation writes to the canonical root's .kittify/charter/not to the worktree's .kittify/charter/. This is enforced by construction: sync() receives canonical_root as its base path.

Invariant 4: staleness is a hash comparison, not a timestamp check

Staleness is detected by comparing the content hash of canonical_root / ".kittify/charter/charter.md" against the hash stored in canonical_root / ".kittify/charter/metadata.yaml". Timestamps are not consulted for staleness (they may be consulted as an mtime short-circuit optimization inside the completeness check — see NFR-002 mitigation in plan §Risks).

Invariant 5: None return means "no charter"

If charter.md does not exist at canonical_root / ".kittify/charter/charter.md", the function returns None. It does not raise, does not create the file, and does not error. Callers that require a charter must check for None and handle accordingly (typical caller: fall back to a "no governance context" path).

Invariant 6: failure is loud, not silent

Both exception types (NotInsideRepositoryError, GitCommonDirUnavailableError) propagate. No fallback to filesystem-heuristic resolution per C-001 / C-009.


Interaction with existing contracts

sync()

Unchanged externally. Internally, it now receives canonical_root (a Path) from the chokepoint and produces paths relative to it. The extraction logic itself is untouched. _SYNC_OUTPUT_FILES (at src/charter/sync.py:32-36) remains the authoritative list of files sync() writes.

load_governance_config() / load_directives_config()

Already route through ensure_charter_bundle_fresh() at src/charter/sync.py:204 and :244. After WP2.2 they automatically benefit from canonical-root resolution with no code change at those call sites — the SyncResult they receive carries the new canonical_root field, but they do not need to consume it (they read the derived YAMLs from the paths the chokepoint guarantees).

post_save_hook()

Called after a CLI charter write. Consumes SyncResult. WP2.2 updates the hook to anchor displayed paths against canonical_root (R-3).

Compiler pipeline (write_compiled_charter at src/charter/compiler.py:169-196)

Out of scope for v1.0.0 chokepoint. Callers that need references.yaml materialized invoke the compiler pipeline explicitly (spec-kitty charter generate). The compiler pipeline MAY call the chokepoint to guarantee upstream governance.yaml / directives.yaml / metadata.yaml are fresh before compiling, but the chokepoint does not call the compiler.

build_charter_context() context-state.json writes (src/charter/context.py:385-398)

Out of scope for v1.0.0 chokepoint. context-state.json remains lazily written by the context builder and is not validated by the chokepoint. The project .gitignore may continue to ignore it; the manifest is silent about it.


Test surface (WP2.2 + WP2.3)

The following tests are introduced / updated to exercise this contract:

  • tests/charter/test_canonical_root_resolution.py — unit tests for the resolver per R-2 fixture matrix.
  • tests/charter/test_chokepoint_overhead.py — NFR-002 warm-overhead benchmarks.
  • tests/charter/test_resolution_overhead.py — NFR-003 resolver overhead benchmarks.
  • tests/charter/test_bundle_contract.py (WP2.3) — end-to-end manifest-vs-disk check using the chokepoint (v1.0.0 scope only).
  • tests/charter/test_chokepoint_coverage.py (WP2.3) — AST-walk asserting every reader of the three sync()-produced derivatives routes through this function.
  • tests/init/test_fresh_clone_no_sync.py (WP2.3) — fresh-clone smoke test; chokepoint auto-refreshes governance.yaml / directives.yaml / metadata.yaml.
  • tests/test_dashboard/test_charter_chokepoint_regression.py (WP2.3) — dashboard typed contracts survive the cutover byte-identically.

Non-goals

  • Not introducing caching on SyncResult itself. Each invocation produces a fresh result. Caching is at the resolver layer only (LRU by working directory) and at the hash-check layer (implementation detail; not part of the contract).
  • Not changing the public API of SyncResult in any way beyond adding canonical_root. No renames, no retypings.
  • Not introducing a new "bundle state" enum. Existing synced / stale_before / extraction_mode fields cover the observable states.
  • Not expanding v1.0.0 to cover references.yaml or context-state.json. Those are separate pipelines and out of scope for this tranche.
  • Not touching src/specify_cli/core/worktree.py's .kittify/memory/ and .kittify/AGENTS.md sharing. Those symlinks are documented-intentional (see src/specify_cli/templates/AGENTS.md:168-179) and unrelated to the charter bundle. Canonical-root resolution solves the worktree-charter-visibility problem without touching that code path.

migration-report.schema.json

{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://spec-kitty.dev/schemas/migration-report/m_3_2_0rc35_unified_bundle.json", "title": "MigrationReport (m_3_2_0rc35_unified_bundle)", "description": "Structured JSON output emitted by the m_3_2_0rc35_unified_bundle migration when run via spec-kitty upgrade --json. Tested by tests/upgrade/test_unified_bundle_migration.py against the FR-013 fixture matrix. v1.0.0 scope: the migration does NOT scan worktrees, does NOT remove charter-related symlinks (no such symlinks exist for the charter bundle), and does NOT reconcile .gitignore (v1.0.0 manifest matches current .gitignore verbatim). The migration exists to (a) seal the project version at 3.2.0rc35, (b) ensure derivatives are present via the chokepoint, and (c) validate the bundle against the manifest.", "type": "object", "additionalProperties": false, "required": [ "migration_id", "target_version", "applied", "charter_present", "bundle_validation", "chokepoint_refreshed", "errors", "duration_ms" ], "properties": { "migration_id": { "type": "string", "const": "3.2.0rc35_unified_bundle", "description": "Literal identifier of this migration." }, "target_version": { "type": "string", "const": "3.2.0rc35", "description": "Literal target version the migration advances the project to." }, "applied": { "type": "boolean", "description": "Whether the migration changed anything on disk. False means it was a no-op (e.g., second apply against an already-upgraded project, or a project whose bundle was already complete when the migration ran)." }, "charter_present": { "type": "boolean", "description": "True if .kittify/charter/charter.md exists at the canonical project root at migration time. When false, every subsequent bundle-related field is empty or default; the migration is a no-op for projects without a charter." }, "bundle_validation": { "type": "object", "additionalProperties": false, "required": ["passed", "missing_tracked", "missing_derived", "unexpected"], "properties": { "passed": { "type": "boolean", "description": "True if the main-checkout bundle at .kittify/charter/ satisfies the CharterBundleManifest v1.0.0 contract after any chokepoint-driven refresh." }, "missing_tracked": { "type": "array", "items": { "type": "string" }, "description": "Tracked files (relative to project root) that the manifest declares but are missing from disk. Must be empty for passed=true." }, "missing_derived": { "type": "array", "items": { "type": "string" }, "description": "Derived files (relative to project root) that the manifest declares but are missing. May be non-empty on the pre-refresh snapshot, expected to be empty post-refresh." }, "unexpected": { "type": "array", "items": { "type": "string" }, "description": "Files present under .kittify/charter/ that the v1.0.0 manifest does not declare (typically references.yaml, context-state.json, interview/answers.yaml, library/*.md). Listed for operator visibility; the migration does NOT delete these. v1.0.0 manifest scope is intentionally narrow — these files are produced by other pipelines and are not bugs." } }, "description": "Result of validating the main-checkout bundle against CharterBundleManifest v1.0.0." }, "chokepoint_refreshed": { "type": "boolean", "description": "True if the migration invoked ensure_charter_bundle_fresh() and it actually triggered a sync (i.e., derivatives were missing or stale). False if the bundle was already complete and fresh." }, "errors": { "type": "array", "items": { "type": "string" }, "description": "Non-fatal errors encountered during the migration. Fatal errors raise and do not produce a report at all." }, "duration_ms": { "type": "integer", "minimum": 0, "description": "Wall time of the migration in milliseconds. Must be ≤ 2000 on the FR-013 reference fixture (NFR-006)." } } }

occurrence-artifact.schema.yaml

Occurrence-classification artifact schema (per #393 / FR-015)

#

This schema is inherited verbatim from Phase 1 (`excise-doctrine-curation-and-

inline-references-01KP54J6/contracts/occurrence-artifact.schema.yaml`). It is

re-published here for reading convenience; the Phase 1 file remains the

authoritative source. The verifier script scripts/verify_occurrences.py

(introduced in Phase 1 WP1.1) is reused without modification.

#

Purpose: declare, per WP and per mission, every string / symbol / path

occurrence that the WP intends to change or delete, classify it into a

category with explicit include/exclude rules, and assert at merge time that

the "to-change" set is empty on disk (verification-by-completeness). This

replaces hand-reviewed semantic diffs for bulk-edit categories.

$schema: "http://json-schema.org/draft-07/schema#" $id: "https://spec-kitty.dev/schemas/occurrence-artifact/1.0.0" title: OccurrenceArtifact description: > Per-work-package declaration of occurrences the WP edits or deletes. Consumed by scripts/verify_occurrences.py at WP PR merge time.

type: object additionalProperties: false required:

properties: wp_id: type: string pattern: "^WP[0-9]+\\.[0-9]+$" description: > Work package identifier (e.g., "WP2.1"). Matches the WP section in plan.md. mission_slug: type: string description: > Full mission slug for cross-referencing against kitty-specs/<slug>/. Example: "unified-charter-bundle-chokepoint-01KP5Q2G". requires_merged: type: array items: type: string pattern: "^WP[0-9]+\\.[0-9]+$" description: > WP IDs that must be merged before this WP's verifier will run green. Encodes the strict-sequencing contract from C-007. categories: type: object description: > Map of category key to category definition. Keys are stable identifiers (e.g., "import_path", "symbol_name", "filesystem_path_literal", "yaml_key", "cli_command_name", "docstring_comment", "skill_template_reference", "test_identifier"). additionalProperties: type: object additionalProperties: false required:

properties: description: type: string description: What this category covers. include: type: array items: type: string description: > Glob patterns / regex patterns that describe what the verifier should scan. exclude: type: array items: type: string description: > Patterns to exclude from the scan (e.g., test fixtures, carved-out files). occurrences: type: array items: type: object additionalProperties: false required:

properties: path: type: string description: File path or path glob. pattern: type: string description: Exact string or regex the occurrence matches. action: type: string enum: ["delete", "rewrite", "leave"] description: > What the WP does with this occurrence. "delete" means the occurrence must be absent after merge; "rewrite" means the occurrence is replaced by a new string (the new form is documented in the rewrite_to field if present); "leave" means this occurrence is an explicit carve-out and must remain unchanged. rewrite_to: type: string description: > If action is "rewrite", the new string the occurrence is replaced with. Informational; verifier does not act on it. rationale: type: string description: Human-readable rationale for the chosen action. carve_outs: type: array items: type: object additionalProperties: false required:

properties: path: type: string description: File path or path glob that the verifier must exclude from must_be_zero_after checks. reason: type: string description: Human-readable rationale for the carve-out. description: > Mission-level or WP-level carve-outs that are allowed to retain strings in the must_be_zero_after list. must_be_zero_after: type: array items: type: string description: > Strings or regex patterns that must NOT appear anywhere under src/ and tests/ (outside carve_outs) after this WP merges. Verifier fails the PR if any match is found. verification_notes: type: string description: > Optional free-text notes for the reviewer about how completeness was verified (e.g., "AST walk used in addition to grep because symbol references can be indirect").

  • wp_id
  • mission_slug
  • requires_merged
  • categories
  • carve_outs
  • must_be_zero_after
  • description
  • include
  • exclude
  • occurrences
  • path
  • pattern
  • action
  • path
  • reason