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.
| Gate | Rule | Allowlist | Floor on the planning base (non-vacuity) |
|---|---|---|---|
| G1 | resolve_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 |
| G2 | resolve_owned_mission( / adopt_owned_checkout( are called only from specify_cli/cli/commands/_owned_checkout.py and owned_mission.py | empty | ≥ 1 offender: status_transition.py:917, plus every CLI command calling it directly today |
| G3 | OwnedCheckout._mint is referenced only in owned_mission.py | empty | the symbol does not exist on the planning base; covered by self-mutation |
| G4 | The 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) |
| G5 | No 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[Path | None, 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). |
| G6 | Positive 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.pysrc/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_claimattribute access;effective_root: "Path | None";Optional[Path];- a TypedDict key
effective_root; - a
**{"effective_root": x}splat; - a renamed
checkout_root: Path | Noneparameter; - a direct
OwnedCheckout._mintreference outside the minter; - a second class with an
owned_root: Pathfield. - a parameter annotated
OwnedCheckoutOptionis not flagged, while the same parameter annotatedAnnotated[Path | None, typer.Option(...)]is (pinsCLI_CLAIM_INPUT_RULE); - a
checkout_root: Pathparameter undersrc/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.yamlgains a sectiontest_owned_checkout_single_authority: {owned_root_bare_path_params: 0}.- Transitional surfaces are marked
# TRANSITIONAL(WP18): <reason>; the closing WP deletes everythinggrep -rn "TRANSITIONAL(WP18)" src testsfinds and asserts that grep is empty. It fails on any marker that no WP's DoD lists. grep -rn "# bridging:" srcis empty at the mission tip, andtests/architectural/test_no_dead_symbols.pyis green there (not per lane).
cli-owned-checkout-surface.md
Contract: CLI owned-checkout surface
| Command | --owned-checkout | Allowed topologies | Owned success output | Notes |
|---|---|---|---|---|
agent tasks status | new | single_branch | the owned WP list plus stale_repository_root_copy | FR-004 |
agent mission setup-plan | new | single_branch | plan.md created and committed in P, plus stale_repository_root_copy | FR-005 |
agent context resolve | new | single_branch | wp_file/workspace_path under P, resolution_kind="owned_checkout", lane_id=null, plus stale_repository_root_copy and warnings[] | FR-006 |
next | existing (now through the validator) | single_branch, lanes_with_coord, coord | a step or blocked decision, plus stale_repository_root_copy | FR-002/008/009/010/012 |
agent mission finalize-tasks | existing | single_branch | atomic, plus stale_repository_root_copy | FR-013/015 |
agent action implement | new (refusal only) | none | exit ≠ 0, OWNED_ACTION_UNSUPPORTED, with guidance naming next --owned-checkout and agent tasks move-task --owned-checkout | FR-018 |
agent action review | new (refusal only) | none | as above | FR-018 |
agent mission create | existing | n/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, accept | existing | single_branch | unchanged 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_seammission_context_forresolve_action_contextlocate_work_packageresolve_workspace_for_wpEventLogReadContractTransitionRequestDecideNextContext- 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.