Contracts
doctor-shim-registry-cli.md
CLI Contract: spec-kitty doctor shim-registry
Mission: migration-shim-ownership-rules-01KPDYDW Spec refs: FR-009, NFR-001, NFR-004, C-004, C-007 Command group: spec-kitty doctor (new subcommand alongside command-files, state-roots, identity, sparse-checkout)
Invocation
spec-kitty doctor shim-registry
spec-kitty doctor shim-registry --json
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--json | bool | false | Emit machine-readable JSON instead of the Rich table. |
No --fix flag. The check is read-only (C-004); it never writes to the registry or to the filesystem.
Inputs (read-only)
architecture/2.x/shim-registry.yaml— parsed viaruamel.yamlsafe loader.pyproject.toml—[project].versionread via stdlibtomllib.src/specify_cli/*/.py— only for existence probes on each entry'slegacy_path(no import).
Algorithm
1. Locate repo root.
2. Read project version from pyproject.toml. On failure, exit 2 with config error.
3. Read registry YAML. On missing or malformed file, exit 2 with config error.
4. Validate every entry against the schema in contracts/shim-registry-schema.yaml.
On validation failure, exit 2 and list every invalid entry with its field-level error.
5. For each entry:
status = derive_status(entry, project_version, fs)
append row to output table
6. Count statuses.
7. If any overdue: exit 1.
Else exit 0.
Status derivation
def derive_status(entry, project_version, fs) -> Status:
exists = fs.shim_file_exists(entry.legacy_path)
if entry.grandfathered:
return Status.GRANDFATHERED
if Version(project_version) >= Version(entry.removal_target_release):
return Status.OVERDUE if exists else Status.REMOVED
return Status.PENDING # exists is expected to be True; mismatch is an advisory
Edge case: pending but module file absent — treated as a consistency advisory ("registry says shim exists but file was not found"). Non-fatal for the check's exit code but flagged in the output.
Exit codes
| Code | Meaning |
|---|---|
| 0 | All entries pending, removed, or grandfathered. Build passes. |
| 1 | At least one overdue shim. Build fails. |
| 2 | Configuration error (pyproject.toml missing, registry missing/unparseable, schema violation). |
Human output (Rich table)
+--------------------------------+-------------------------------+-----------------+--------------+
| legacy_path | canonical_import | removal_target | status |
+--------------------------------+-------------------------------+-----------------+--------------+
| specify_cli.charter | spec_kitty.charter | 3.2.0 | pending |
| specify_cli.legacy_helper | specify_cli.helpers.canon... | 4.0.0 | grandfathered|
| specify_cli.runtime | runtime.mission, runtime... | 3.3.0 | pending |
| specify_cli.old_feature | specify_cli.new_feature | 3.1.0 | overdue (!) |
+--------------------------------+-------------------------------+-----------------+--------------+
Summary: 2 pending, 1 grandfathered, 1 overdue, 0 removed.
OVERDUE: specify_cli.old_feature
Canonical: specify_cli.new_feature
Target: 3.1.0 (current project version: 3.2.0a3)
Tracker: #NNN
Remediation: delete src/specify_cli/old_feature.py OR update
removal_target_release with extension_rationale.
Exit code: 1
JSON output schema
{
"project_version": "3.2.0a3",
"entries": [
{
"legacy_path": "specify_cli.charter",
"canonical_import": "spec_kitty.charter",
"removal_target_release": "3.2.0",
"grandfathered": false,
"tracker_issue": "#610",
"status": "pending",
"shim_file_exists": true
}
],
"summary": {
"pending": 2,
"grandfathered": 1,
"overdue": 1,
"removed": 0
},
"overdue": [
{
"legacy_path": "specify_cli.old_feature",
"canonical_import": "specify_cli.new_feature",
"removal_target_release": "3.1.0",
"tracker_issue": "#NNN",
"remediation": "delete-or-extend"
}
],
"exit_code": 1
}
Performance budget
Per NFR-001: ≤2 seconds wall-clock at up to 50 registry entries. The check does no network I/O; time cost is YAML parse + up to 50 filesystem stats + table render.
Read-only guarantee
Per C-004, the handler:
- Does not call
Path.write_*,open(..., "w"),os.remove, or any filesystem mutator. - Does not invoke
gitcommands. - Exits with a status code only.
This is enforced by a unit test that patches builtins.open for write mode and asserts the call count stays at zero across a full doctor run.
Integration with the existing doctor group (C-007)
- Registered in
src/specify_cli/cli/commands/doctor.pyas@app.command(name="shim-registry")alongside the four existing subcommands. - Uses the same
Consoleinstance and_is_interactive_environment()helper already defined in that module. spec-kitty doctor --helpwill listshim-registryas one of the five subcommands.
shim-registry-schema.yaml
Shim Registry Schema Contract
Mission: migration-shim-ownership-rules-01KPDYDW
Spec refs: FR-007, FR-010, FR-011, C-004
#
This file is a human-readable JSON-Schema-style description of the
architecture/2.x/shim-registry.yaml format. The FR-011 pytest asserts
every entry in the live registry conforms to this contract.
#
Schema version: 1
$schema: "http://json-schema.org/draft-07/schema#" title: Shim Registry description: | Machine-readable enumeration of every compatibility shim under src/specify_cli/ that re-exports from a canonical package. One entry per shim; the FR-010 scanner test asserts every on-disk shim appears here.
type: object required: [shims] additionalProperties: false properties: shims: type: array description: List of shim entries. Order is not significant. items: $ref: "#/definitions/ShimEntry"
definitions: ShimEntry: type: object required:
additionalProperties: false properties: legacy_path: type: string pattern: "^[A-Za-z_][A-Za-z0-9_](?:\\.[A-Za-z_][A-Za-z0-9_])*$" description: | Dotted Python import path of the shim module, e.g. "specify_cli.charter". Acts as the entry's primary key. Unique within the registry. canonical_import: oneOf:
pattern: "^[A-Za-z_][A-Za-z0-9_](?:\\.[A-Za-z_][A-Za-z0-9_])*$"
minItems: 1 items: type: string pattern: "^[A-Za-z_][A-Za-z0-9_](?:\\.[A-Za-z_][A-Za-z0-9_])*$" description: | Target canonical import. String for single-target shims; list for umbrella shims that re-export from multiple canonicals (spec edge-case #3). introduced_in_release: type: string pattern: "^\\d+\\.\\d+\\.\\d+(?:[a-z]\\d+)?$" description: Release in which the shim was first introduced. removal_target_release: type: string pattern: "^\\d+\\.\\d+\\.\\d+(?:[a-z]\\d+)?$" description: | Release in which the shim must be removed. Must be >= introduced_in_release (semver-aware comparison). One-release deprecation window is the default; larger windows require extension_rationale. tracker_issue: type: string pattern: "^(#\\d+|https?://.+)$" description: GitHub issue reference. Either "#NNN" shorthand or full URL. grandfathered: type: boolean description: | True for pre-existing shims that do not fully match the new rules but are allowed to persist under explicit rationale. No new entry should set this to true after mission 615 lands (FR-008 one-shot exception). extension_rationale: type: string minLength: 1 description: | Required when removal_target_release has been extended beyond the original one-release deprecation window. Free-text rationale reviewed like any architecture PR. notes: type: string description: Optional free-text annotations.
- legacy_path
- canonical_import
- introduced_in_release
- removal_target_release
- tracker_issue
- grandfathered
- type: string
- type: array