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
| Flag | Default | Description |
|---|---|---|
--json | False | Emit structured JSON to stdout instead of the human-readable report. Exit code unchanged. |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Bundle at .kittify/charter/ fully complies with CharterBundleManifest.CANONICAL_MANIFEST v1.0.0. |
| 1 | Bundle is non-compliant (missing tracked files, missing derived files, or gitignore drift on a required entry). Details on stdout. |
| 2 | Canonical-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
NotInsideRepositoryErrororGitCommonDirUnavailableError: exit 2 with the exception message on stderr (loud failure per C-001). - For every path in
manifest.tracked_files: assert it exists atcanonical_root / pathand is tracked in git (viagit ls-files). - For every path in
manifest.derived_files: assert it exists atcanonical_root / pathunless the bundle is in a fresh-clone state (charter.mdexists 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 incanonical_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 (notablyreferences.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 syncor 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
--fixflag. 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
| Test | WP | Focus |
|---|---|---|
tests/charter/test_bundle_manifest_model.py | WP2.1 | Unit 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.1 | End-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 cwd | stdout | is_absolute? | resolved to | canonical_root |
|---|---|---|---|---|
<repo> (main checkout) | .git | No | <repo>/.git | <repo> ✓ |
<repo>/src/charter (subdirectory) | ../../.git | No | <repo>/.git | <repo> ✓ |
<repo>/.git (inside git dir) | . | No | <repo>/.git | detected in step 5 → raise NotInsideRepositoryError |
<repo>/.worktrees/foo (linked worktree) | /abs/path/to/<main>/.git | Yes | <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/sub | No | <repo>/.git/modules/sub | <repo>/.git/modules — submodule-specific path, treated as submodule's working directory (see note below) |
<repo> with sparse-checkout enabled | same as main checkout | — | — | same as main checkout ✓ |
<repo> with detached HEAD | same as main checkout | — | — | same 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) -> strhelper. The publicresolve_canonical_repo_root(path: Path)resolvespathto its absolute form (normalizing files to parents), stringifies for the cache key, calls_resolve_cached, and re-wraps the result asPath. - 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'scache_clearattribute perfunctools.lru_cacheconvention. - Rationale: NFR-003 binds the resolver to <5 ms p95 with ≤1
gitinvocation per call. Cache amortizes thesubprocessoverhead across the dashboard's hot loop (per-frame charter-read path per NFR-002).
Performance contract (NFR-003)
| Metric | Threshold |
|---|---|
| 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 bound | 256 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
| Test | WP | Focus |
|---|---|---|
tests/charter/test_canonical_root_resolution.py | WP2.2 | Behavioral 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.py | WP2.2 | NFR-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/configforcore.worktreeor similar. Git's--git-common-diris authoritative. - Not providing a "best-effort fallback" when
gitis 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-diror re-invoking git a second time to force absolute output. The contract resolves stdout againstcwdin 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
Pathinfiles_writtenis relative tocanonical_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_writtenentries are drawn fromCANONICAL_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., ifsync()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 therepo_rootpassed 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 threesync()-produced derivatives routes through this function.tests/init/test_fresh_clone_no_sync.py(WP2.3) — fresh-clone smoke test; chokepoint auto-refreshesgovernance.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
SyncResultitself. 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
SyncResultin any way beyond addingcanonical_root. No renames, no retypings. - Not introducing a new "bundle state" enum. Existing
synced/stale_before/extraction_modefields cover the observable states. - Not expanding v1.0.0 to cover
references.yamlorcontext-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.mdsharing. Those symlinks are documented-intentional (seesrc/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