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

FlagTypeDefaultDescription
--jsonboolfalseEmit 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 via ruamel.yaml safe loader.
  • pyproject.toml[project].version read via stdlib tomllib.
  • src/specify_cli/*/.py — only for existence probes on each entry's legacy_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

CodeMeaning
0All entries pending, removed, or grandfathered. Build passes.
1At least one overdue shim. Build fails.
2Configuration 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 git commands.
  • 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.py as @app.command(name="shim-registry") alongside the four existing subcommands.
  • Uses the same Console instance and _is_interactive_environment() helper already defined in that module.
  • spec-kitty doctor --help will list shim-registry as 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