Contracts

architectural-gate.md

Contract: single-authority architectural gate

File: tests/architectural/test_owned_checkout_single_authority.py. It is AST-based and reuses tests/architectural/_ast_scan.parse_file. Comments and docstrings never count as offenders.

GateRuleAllowlistFloor on the planning base (non-vacuity)
G1resolve_ownership_claim is referenced only in specify_cli/core/owned_mission.py (plus its definition in checkout_ownership.py). Every reference form counts: ast.alias.name (including import … as x), Attribute.attr, Name.id, and string Constants used with getattr/importlib.empty≥ 1 offender: next_cmd.py:170, mission_creation.py:848
G2resolve_owned_mission( / adopt_owned_checkout( are called only from specify_cli/cli/commands/_owned_checkout.py and owned_mission.pyempty≥ 1 offender: status_transition.py:917, plus every CLI command calling it directly today
G3OwnedCheckout._mint is referenced only in owned_mission.pyemptythe symbol does not exist on the planning base; covered by self-mutation
G4The identifier effective_root does not appear anywhere in src/ outside the org-pack module rule (below): not as a parameter, field, local annotation, keyword argument, TypedDict key or string dict key ("effective_root"). Annotation spelling is irrelevant, because the rule bans the identifier, not the annotation type.empty≥ 1 offender (≈ 400 sites)
G5No parameter or field carries an owned root as a path under any name: owned_root, owned_checkout, checkout_root or effective_root annotated Path, pathlib.Path, `Path \None, Optional[Path], Union[Path, None], os.PathLike or the string form of any of these. Two named exemptions only, each pinned by a self-mutation test: (a) the fields of mission_runtime.owned_checkout.OwnedCheckout (by fully-qualified class name); (b) CLI_CLAIM_INPUT_RULE: a parameter annotated with the CLI claim-input alias OwnedCheckoutOption (specify_cli/cli/commands/_owned_checkout.py), or with its help-preserving form Annotated[PathNone, owned_checkout_option(help=...)] (the same alias with a custom help string), because it carries the raw, unvalidated --owned-checkout input to the minter and is never an owned root. The checkout_root name check additionally excludes src/specify_cli/upgrade/migrations/**` by module rule (historical migrations, an unrelated sense).
G6Positive signature pin: each consumer in contracts/owned-checkout-carrier.md §7 declares a parameter or field `owned: OwnedCheckout \None. The topology-agnostic review-base helper claim_commit_for_wp(mission_dir: Path, wp_id: str)` is not a G6 consumer.n/a

Org-pack exclusion (ORG_PACK_MODULE_RULE). effective_root is also the org-pack root method OrgPackConfig.effective_root (src/charter/offering/drg/org_pack_config.py:374), a different concept. It is excluded from G4/G5 by one module-scoped rule with a written reason, covering exactly:

  • src/charter/**
  • src/specify_cli/doctrine/**
  • src/specify_cli/cli/commands/_doctrine_collect.py
  • src/specify_cli/analysis_inputs.py

src/specify_cli/charter_runtime/lint/checks/org_layer.py is not exempted: its walrus local (effective_root := pack.effective_root(repo_root)) is renamed (for example to pack_root) by WP18 T097, and the method call itself is exempt everywhere by the OrgPackConfig.effective_root call rule (a Call whose func is Attribute(attr="effective_root"); an uncalled attribute read is still flagged).

Scan floor (exact). The set of files G1/G2/G4/G5 scan equals src/*/.py minus the named module-rule exemptions, compared as sets; an emptied or narrowed scan root fails. WP18 also runs the gate against a worktree of the planning base and records per-gate offender counts meeting the floors above.

Self-mutation tests. Synthetic sources must each be flagged:

  • a stray claim call;
  • import resolve_ownership_claim as r;
  • mod.resolve_ownership_claim attribute access;
  • effective_root: "Path | None";
  • Optional[Path];
  • a TypedDict key effective_root;
  • a **{"effective_root": x} splat;
  • a renamed checkout_root: Path | None parameter;
  • a direct OwnedCheckout._mint reference outside the minter;
  • a second class with an owned_root: Path field.
  • a parameter annotated OwnedCheckoutOption is not flagged, while the same parameter annotated Annotated[Path | None, typer.Option(...)] is (pins CLI_CLAIM_INPUT_RULE);
  • a checkout_root: Path parameter under src/specify_cli/upgrade/migrations/ is not flagged, while the same source under any other path is.

A clean synthetic source passes.

Scope boundary. G2 enforces where validation may be called. It does not enforce "once per command"; NFR-002's CLI-level count carries that.

Commit discipline:

  • The self-mutation and floor tests land green in the foundation WP.
  • The G1–G6 assertions are committed red as the first commit of the closing WP and go green there.
  • _baselines.yaml gains a section test_owned_checkout_single_authority: {owned_root_bare_path_params: 0}.
  • Transitional surfaces are marked # TRANSITIONAL(WP18): <reason>; the closing WP deletes everything grep -rn "TRANSITIONAL(WP18)" src tests finds and asserts that grep is empty. It fails on any marker that no WP's DoD lists.
  • grep -rn "# bridging:" src is empty at the mission tip, and tests/architectural/test_no_dead_symbols.py is green there (not per lane).

cli-owned-checkout-surface.md

Contract: CLI owned-checkout surface

Command--owned-checkoutAllowed topologiesOwned success outputNotes
agent tasks statusnewsingle_branchthe owned WP list plus stale_repository_root_copyFR-004
agent mission setup-plannewsingle_branchplan.md created and committed in P, plus stale_repository_root_copyFR-005
agent context resolvenewsingle_branchwp_file/workspace_path under P, resolution_kind="owned_checkout", lane_id=null, plus stale_repository_root_copy and warnings[]FR-006
nextexisting (now through the validator)single_branch, lanes_with_coord, coorda step or blocked decision, plus stale_repository_root_copyFR-002/008/009/010/012
agent mission finalize-tasksexistingsingle_branchatomic, plus stale_repository_root_copyFR-013/015
agent action implementnew (refusal only)noneexit ≠ 0, OWNED_ACTION_UNSUPPORTED, with guidance naming next --owned-checkout and agent tasks move-task --owned-checkoutFR-018
agent action reviewnew (refusal only)noneas aboveFR-018
agent mission createexistingn/a (the mission does not exist yet)unchanged; governance reads now come from P (FR-016)goes through resolve_owned_create_root
move-task, mark-status, spec-commit, check-prerequisites, acceptexistingsingle_branchunchanged behaviour (FR-022)rewired onto the shared helper

Flagless. Owned-capable commands run without the flag call adopt_owned_checkout on cwd. The result is either a fact or the unchanged repository-root behaviour (FR-021). If the same selector resolves to different mission ids in cwd's checkout and in R, the command keeps today's mission-surface-conflict refusal (US7-AS5).

Stale-copy key. stale_repository_root_copy is emitted only in owned runs (explicit or adopted fact). A non-owned run's JSON payload is byte-identical to the planning base.

Option. Every --owned-checkout declaration uses the shared alias OwnedCheckoutOption; gate G5 exempts parameters annotated with it (or with its help-preserving form Annotated[Path | None, owned_checkout_option(help=...)]) by the named CLI_CLAIM_INPUT_RULE (they carry the raw, unvalidated claim input to the minter).

Refusals. emit_owned_refusal keeps each command's existing JSON envelope keys. error_code is always one of the registered codes (data-model.md). No refusal writes to P or R.

Help text. It uses "repository root checkout" and "owned checkout"; never bare "primary" and never "feature".

owned-checkout-carrier.md

Contract: validated ownership fact

Scope: mission_runtime.OwnedCheckout and its sole minter, specify_cli.core.owned_mission.

1. Sole construction. The only way to obtain an OwnedCheckout in src/ is through owned_mission.resolve_owned_mission, owned_mission.adopt_owned_checkout, or a tests/ helper. OwnedCheckout(...) called directly raises TypeError. Gates G1–G3 enforce this. Until WP18, the transitional legacy factory function owned_mission.OwnedMission(primary, root, directory, slug, target) (marked # TRANSITIONAL(WP18)) also mints, inside the minter module, so G3 holds. 2. Once per command. A CLI entry point validates at most once. Every downstream function receives owned: OwnedCheckout | None and never re-validates. Gate G2 enforces this, and NFR-002 counts it. 3. No bare owned roots. No function parameter or dataclass field in src/ carries the owned root as Path / Path | None. Gates G4/G5 enforce this with an empty allowlist. The org-pack OrgPackConfig.effective_root is a different concept and is excluded by one module rule (src/charter/, src/specify_cli/doctrine/, _doctrine_collect.py, analysis_inputs.py; see the gate contract). 4. Topology per command. Each call site passes its allowed_topologies. The fact records the stored topology. 5. Repository root is not owned. Passing R as --owned-checkout raises OWNED_CHECKOUT_IS_REPOSITORY_ROOT. Passing a lane worktree of the mission or a coordination worktree raises OWNED_CHECKOUT_IS_MISSION_WORKTREE. 6. Adoption guard. adopt_owned_checkout returns None in each of these cases, never a fact:

7. Consumers. The following accept owned and, when it is set, read only under owned.owned_root, never calling get_main_repo_root:

  • the repository root checkout;
  • a lane worktree;
  • a coordination worktree;
  • another repository;
  • any checkout the validator rejects.
  • placement_seam
  • mission_context_for
  • resolve_action_context
  • locate_work_package
  • resolve_workspace_for_wp
  • EventLogReadContract
  • TransitionRequest
  • DecideNextContext
  • the prompt builder

The review-base helper claim_commit_for_wp(mission_dir: Path, wp_id: str) is deliberately not a consumer: it is topology-agnostic and serves owned and repository-root review paths alike, so it takes the mission directory whose HEAD is reviewed, never the fact.