Contracts

charter-doctrine-service-contract.md

Contract: charter.resolver.DoctrineService public surface (post-mission)

This is the internal contract every in-scope call site (FR-001/002/003) must construct/consume against. Not a network API — an internal Python object contract, enforced by the FR-007 gates.

Construction

Exactly one path constructs the activation-aware instance: the unified builder (FR-008). No call site outside src/charter/resolver.py and that one builder constructs doctrine.service.DoctrineService directly (NFR-001).

DoctrineService(inner: doctrine.service.DoctrineService, pack_context: PackContext | None)

unfiltered. Callers using this mode MUST carry an inline comment naming the diagnostic reason.

  • pack_context set (normal case) → every gated property below applies three-state filtering.
  • pack_context=None (diagnostic case, FR-002/R4) → every gated property returns the raw inner catalog,

Gated properties (9 of 10 kinds)

paradigms, procedures, agent_profiles (pre-existing) plus directives, tactics, styleguides, toolguides, mission_step_contracts, glossary_packs (FR-005, new). Each follows the three-state contract in data-model.md. None of these properties differ in filtering shape from each other — a reviewer diffing any two of the 9 getters should see the same structure modulo the kind name.

New resolution methods (FR-003)

Method names are an implementation choice for the tasks phase, but the contract is: every one of the 5 resolver tiers (OVERRIDE, LEGACY, GLOBAL_MISSION, GLOBAL, PACKAGE_DEFAULT) and the mission-config resolution remains reachable ONLY via a method on this class from outside src/charter/**. doctrine/resolver.py's _resolve_asset/resolve_mission functions are the implementation these methods delegate to; they are not moved, renamed, or duplicated.

Explicitly NOT on this class

charter.mission_type_profiles.resolve_mission_type_context() (see the sibling contract file — NOT by editing MissionTypeProfileRepository's own file, per a post-tasks squad ownership-boundary correction). Do not add a mission_types property here as a shortcut; that would contradict R (D4)'s finding that the token has no matching raw-service property to filter.

require the new agent_profile_repository accessor, a second, explicitly-named accessor, not a widening of the agent_profiles property's return type. (Post-tasks squad correction: get_ancestors() and resolve_profile() were named in earlier drafts of this contract but are not the verified surface — get_ancestors() is unused by any real call site; resolve_profile() is used only by runtime_bridge_io.py, not by the two ._inner-reach-around sites this bullet originally described.)

  • mission_types — no property exists, and none is added. The mission-type token is gated separately by
  • Lineage/mutation operations (register_overlay() and get_provenance()) on agent_profiles — these

Non-regression obligations

Non-fakeable assertion shape (post-plan squad correction — an existence check like assert svc. directives is satisfied even if 3 of 40 directives silently leaked away): the test asserts wrapped.<prop> == unwrapped_inner.<prop> for a bare PackContext, per kind — equality against the raw unwrapped inner service's output, not merely "returns something."

doctrine.service.DoctrineService would have returned — proven by the same equality regression shape, since this mode exists specifically to preserve pre-mission diagnostic behaviour.

  • A bare project (no activated packs) must see its full built-in default catalog on every gated property.
  • pack_context=None construction must return the identical catalog a raw, unwrapped

Lineage/mutation accessor semantics (pinned — post-plan squad, was previously under-specified)

The new public accessor charter.resolver.DoctrineService gains for projection.py/runtime_bridge_io.py/ registry.py/org_profiles.py (FR-001, FR-010) has two semantic questions that do NOT have a default and must not be left for tasks-time improvisation:

1. Does register_overlay() of a non-activated profile become readable through the gated agent_profiles property afterward? Pinned answer: noregister_overlay() mutates the underlying repository's lineage graph; the gated agent_profiles property still applies the same three-state activation filter on every read, including reads that follow a mutation. Mutation capability and activation filtering are orthogonal; the accessor does not create a way to read an unfiltered profile through the filtered surface. 2. Does resolve_profile()'s specializes_from traversal cross into a deactivated parent profile? Pinned answer: yes, lineage traversal reads through the raw repository — lineage composition is a below-the-activation-grain operation (it answers "what does this profile inherit from," not "is this profile enabled"), matching the existing precedent at resolver.py:402-413's resolve_governance_for_ profile, which already reads the raw inner repository for exactly this reason. The accessor returns the raw, lineage-capable repository object directly (not re-wrapped) — callers needing both lineage composition AND activation filtering call the gated property for the filtering decision and the accessor for the composition, as two separate questions, not one merged call.

mission-type-and-builder-contracts.md

Contracts: mission-type activation gating and the unified builder

resolve_mission_type_context() activation contract (FR-006)

Distinct from the three-state contract used by the other 9 kinds, because PackContext. activated_mission_types is never None:

PackContext.activated_mission_typesMeaningPost-mission behaviour
Equals builtin_mission_type_id_set() (the collapse default)No selection was authoredReturn the full built-in mission-type set
A proper, non-default subsetA selection was authoredReturn only the named mission-types

The gating point is charter.mission_type_profiles.resolve_mission_type_context() ONLY — NOT charter.mission_type_profile_repository.MissionTypeProfileRepository's own file (post-tasks squad correction: that file is WP06's exclusive ownership for an unrelated change; implementing filtering there too would create a real ownership overlap), and NOT charter.resolver.DoctrineService (see the sibling contract file's "Explicitly NOT on this class" section). A bare project (no mission-type key authored) must still resolve every built-in mission-type — proven by a dedicated regression test asserting set-equality against builtin_mission_type_id_set(), not a fakeable subset check (post-plan squad correction: "research, software-dev, documentation, plan, at minimum" is satisfied even if a fifth built-in type silently drops out; the non-fakeable comparand already exists and must be used directly), since the default-collapse already happened before this repository ever sees the value and a filtering bug here has no three-state safety net to fall back on.

Unified builder contract (FR-008)

One function, replacing both specify_cli.doctrine_service_factory.build_activation_aware_doctrine_service and charter.doctrine_service_builder._build_activation_aware_doctrine_service:

build_activation_aware_doctrine_service(repo_root: Path) -> charter.resolver.DoctrineService

Behavioural contract (both former call sites' inputs must produce identical output):

doctrine.service.DoctrineService construction (the charter builder's fuller behaviour wins).

wins) — no caller can silently lose the org layer by omitting an argument.

  • active_languages is always computed via infer_repo_languages(repo_root) and passed to the inner
  • org_roots is always self-resolved via resolve_org_roots (the specify_cli builder's fuller behaviour

Regression proof: construct the unified builder with the same repo_root twice, once exercising each former call site's original argument shape, and assert byte-identical (or structurally-identical, if a __eq__ is impractical) resulting catalogs across all 9 gated properties.

Both src/specify_cli/doctrine_service_factory.py::build_activation_aware_doctrine_service and src/charter/doctrine_service_builder.py::_build_activation_aware_doctrine_service become either the same function (one deleted, callers repointed) or one thin re-export of the other — never two independent implementations after this mission (C-001).