Mission Specification: Doctrine DRG Silent-Drop Boundary Fix

Mission Branch: fix/doctrine-drg-silent-drop-boundary Created: 2026-08-23 Status: Draft Input: Research brief docs/plans/investigations/2026-08-declared-but-inert-doctrine-seams.md; scope = GitHub #3608 + #3629 + verify-and-close #3530.

Problem Statement

Doctrine/DRG artifacts are authored, schema-legitimized, and validated — and then silently never take effect at runtime, because the boundary between declaration and consumption is neither derived-from-a-single-source, nor fail-loud, nor test-pinned. A governance author gets a green result over an inert artifact: no exception, no diagnostic, no delivery. This mission closes three concrete instances of that failure and pins the boundary so it cannot silently reopen.

is a hand-copied subset of the canonical NodeKind enum (src/doctrine/drg/models.py). It has drifted 6 kinds (anti_pattern, asset, glossary, glossary_pack, mission_step_contract, template), so legitimate DRG-URN selectors such as glossary_pack:<id> silently fall through the resolution tier at topic_resolver.py:236 and never resolve. No drift-guard test exists.

(1) ContextSources.{tactics, toolguides, styleguides, doctrine-layers, additional} (src/doctrine/agent_profiles/profile.py) are declared and schema-legitimized but never reach a delivery path — only context-sources.directives is projected (extractor.py:920-921). A research squad (see research/context-sources-drg-projection.md) established that context-sources. is a redundant, mostly-inert second surface: the renderer that delivers profile text to a dispatched agent reads the top-level -references fields (the canonical, rationale-bearing, DRG-provisioned surface), not context-sources.. Resolved direction (DM-01M0PEAQ5G1VDR3CSJSV51SD8Y): consolidate on -references and remove context-sources.. (2) extract_governance_profile_scope_edges (extractor.py:1336) minted scope edges from bare ids with no existence check. Already fixed on main by commit d8beee2761assert_governance_scope_edges_resolve (extractor.py:1406, wired at :1574, tested at test_extractor.py:1608-1653) now fails loud on an unresolved selection. This mission verifies* the fix (incl. any org-tier gap) and closes the item; it does not re-implement it. (3) a doc-nit on golden re-ledger wording (extractor.py:557).

whose chain-merge fix landed (merge.py:1122 iterates all fragments). Its closing condition (a chain of org packs delivers every declared kind, and a misconfigured pack fails loud) needs execution-level verification. Per operator direction the fixture is the repo's own packs/internal/ (spec-kitty-internal) org pack. Grounding that fixture surfaced a new instance of the same family: load_validated_graph's org_roots= seam (_drg_helpers.py:138-182) never reads drg/fragment.yaml and suppresses the "no graph" warning when one exists, so the executor and action_doctrine_bundle callers silently drop the internal pack's doctrine (test_executor.py:878-916 documents the degrade). This mission fixes that seam and verifies chain delivery on the real pack.

  • #3608_DRG_NODE_KINDS (src/charter/synthesizer/topic_resolver.py:37)
  • #3629 — three follow-ups from the M2 DRG-projection squad:
  • #3530 — a tracking issue whose 7 of 8 direct members are now closed and

Milestone 3.2.x (#4): advances G1 (deepen Doctrine/Charter/DRG runtime impact), G2 (strangle core domains onto canonical SSOTs — no hand-copied authorities), and the "no new shadow paths" principle. Related fail-loud epic: #3410. Whack-a-copy family: #3562, #3461, #3427.

User Scenarios & Testing (mandatory)

User Story 1 - DRG-URN selectors for every canonical kind resolve (Priority: P1)

A doctrine/charter author writes a selector that references a legitimate DRG node kind — for example glossary_pack:my-terms or mission_step_contract:plan-review — in a synthesizable artifact. Today, because the resolver's kind set is a hand-copied subset that has drifted, the URN silently fails to resolve as a DRG node and falls through with no error. After this mission, every kind the canonical NodeKind enum defines is recognized by the resolver, and a newly-added enum member is recognized automatically without a second hand-edit.

Why this priority: This is the highest-leverage, lowest-risk fix (a single SSOT derivation + a pin), it directly restores currently-broken resolution for glossary_pack/mission_step_contract/etc., and it prevents the next drift. It is the archetype of the whole mission's thesis.

Independent Test: With _DRG_NODE_KINDS derived from NodeKind, assert the resolver recognizes a glossary_pack:<id> URN as a DRG node kind (previously returned None), and run a drift-guard test asserting the recognized set equals {k.value for k in NodeKind}.

Acceptance Scenarios:

1. Given the canonical NodeKind enum defines 16 kinds, When the resolver evaluates the set of DRG node kinds it recognizes, Then the set equals exactly {k.value for k in NodeKind} (no missing kinds, no extras). 2. Given a selector glossary_pack:<id>, When the DRG-URN resolution tier at topic_resolver.py evaluates it, Then the left-hand side is recognized as a known DRG node kind (it is no longer silently rejected at the membership gate). 3. Given a hypothetical new member added to NodeKind, When the drift-guard test runs, Then it passes without any edit to topic_resolver.py (derivation, not a guarded copy).


User Story 2 - A mistyped governance-profile selection fails loud, on both tiers (Priority: P2)

A mission-type author writes a governance-profile.yaml that selects doctrine by bare id (e.g. selected_directives, selected_tactics). If they typo an id or reference a nonexistent artifact, the selection must be reported loudly rather than silently pruned — whether the profile ships in the built-in pack or an org pack. Built-in tier: this already landed on main (commit d8beee2761) — assert_governance_scope_edges_resolve raises naming each unresolved selected_, wired into generate_graph; this mission adds the missing end-to-end test that pins that wiring (the shipped tests use synthetic edges). Org tier (research finding): there is no governance-profile scope path at all today — an org-tier selected_ typo is unread and unguarded (a total no-op). This mission implements the net-new org-tier extraction + post-merge fail-loud guard.

Why this priority: The built-in half is a regression pin; the org-tier half is net-new implementation (not "verify") — the squad reclassified it (F9), and the operator chose to build it now rather than defer. Both halves share the mission's fail-loud thesis.

Independent Test: Run the existing regression (test_extractor.py:1608-1653); additionally construct an org-tier governance-profile.yaml with a nonexistent selection and assert it also fails loud (or document the gap and add the guard).

Acceptance Scenarios:

1. Given a built-in governance-profile.yaml selecting a nonexistent id, When the DRG is generated, Then assert_governance_scope_edges_resolve raises naming the offending id (confirmed already true). 2. Given an org-tier governance-profile.yaml selecting a nonexistent id, When doctrine is loaded/merged, Then it fails loud too (verify; close the gap if the guard does not cover the org path).


User Story 3 - Profile references live on one canonical surface (Priority: P1)

A profile author writes context-sources.tactics (6 profiles do) or context-sources.additional (16 profiles do), reasonably expecting that content to reach a dispatched agent — the schema legitimizes it. It does not: the renderer delivers from the top-level -references surface, and context-sources. is a redundant, mostly-inert duplicate (research: research/context-sources-drg-projection.md). After this mission there is one canonical profile-reference surface — the top-level -references fields — and context-sources. is removed. Authored tactic/toolguide/styleguide intent is preserved by migrating it onto the *-references surface (which the extractor projects to DRG edges and the renderer delivers); the non-artefact fields (additional free-text, doctrine-layers layer-names) are removed outright as they have no edge/delivery shape.

Why this priority: This is the most authored-against instance of the silent-drop family (25 shipped profiles carry inert context-sources.*), and resolving it removes a whole duplicated authority surface (G2).

Independent Test: After the mission, assert (a) context-sources is no longer a valid profile field (schema rejects it / migration removed it); (b) each shipped profile's previously-authored tactic/toolguide/styleguide intent is present on the *-references surface and reaches a dispatched agent via the profile DRG channel; (c) no profile-reference kind remains simultaneously schema-legal and never-read.

Acceptance Scenarios:

1. Given a profile that authored context-sources.tactics: [t1] before migration, When the migration runs, Then t1 appears under tactic-references and is delivered to a dispatched agent (via the existing profile DRG channel), with no context-sources block remaining. 2. Given any profile after the mission, When it authors a context-sources block, Then the loader rejects it (the field no longer exists) — it is not silently accepted-and-ignored. 3. Given the 25 shipped profiles, When the mission completes, Then 0 of them carry context-sources., and their directive/tactic/toolguide/ styleguide references are all on the -references surface. 4. Given additional / doctrine-layers content (no artefact/edge shape), When the migration runs, Then those fields are removed (not silently retained as dead fields).


User Story 4 - The spec-kitty-internal pack's doctrine reaches every consumer seam (Priority: P1)

The repository's own spec-kitty-internal org pack (packs/internal/) is the #3530 verification fixture — the real dogfooded org pack, not a synthetic one. Research established the pack already conforms to current org-tier conventions (plural node kinds are canonical; drg/fragment.yaml is the required shape; pack.yaml/pack-manifest.yaml are deferred for org packs; the validator runs green). What "updating it to current conventions" actually surfaces is a branch-aligned silent-drop bug: load_validated_graph's org_roots= seam (src/charter/_drg_helpers.py:138-182) reads only root *.graph.yaml, never reads drg/fragment.yaml, and suppresses the "no graph" warning when a fragment exists (:174). Two production callers pass only org_roots= (mission_step_contracts/executor.py:362, charter/action_doctrine_bundle.py:192), so the internal pack's entire doctrine is silently dropped on those seams — an existing test (test_executor.py:878-916) even documents the degrade. After this mission, every seam that consumes org doctrine folds drg/fragment.yaml (or the warning is honest), and the built-in + internal chain delivers every declared kind to every consumer — closing #3530 with evidence on the real pack.

Why this priority: This is a live silent-drop bug on the exact family the mission targets (declared-and-conformant, validates green, reaches no consumer via the executor/action-bundle seam), it is the reason the dogfooded pack's doctrine does not take effect, and it is what makes the #3530 chain verification meaningful.

Independent Test: Register packs/internal/ as an org pack over built-in and drive the executor / action-doctrine-bundle path; assert every kind it declares (glossary pack, procedure, directive, DRG nodes + refines edges to built-in) reaches that consumer (currently dropped); and that a deliberately-misconfigured variant fails loud rather than degrading silently.

Acceptance Scenarios:

1. Given the internal pack registered as an org tier, When the executor / action_doctrine_bundle path (org_roots= seam) loads doctrine, Then the pack's drg/fragment.yaml nodes and edges are folded (not silently dropped), or a missing fragment produces an honest warning (no false suppression). 2. Given the built-in + spec-kitty-internal chain (≥2 layers), When doctrine is loaded/merged/activated across all consumer seams, Then every kind declared by the internal pack (not only built-in's) reaches its consumer, including its DRG nodes and refines edges. 3. Given a deliberately-misconfigured variant of the internal pack, When doctrine is loaded, Then the misconfiguration is reported loudly instead of counted as success. 4. Given the verification suite passes, When #3530's closing condition is evaluated, Then it is met (leaving only the explicitly-non-child #3412 open).

Edge Cases

derivation must key on the canonical value used at the resolution membership gate, matching how URNs are parsed.

doctrine-layers may be a layer selector rather than an artifact id — the resolved direction must treat non-artifact-reference kinds differently from artifact-reference kinds (do not mint a dangling edge for a freeform value).

not-yet-loaded org pack — the fail-loud guard must not false-positive on legitimately-deferred cross-pack references (align with existing extractor posture; scope the check to genuinely unresolvable ids).

it shrinks (exact set equality, both directions).

  • A NodeKind member exists whose URN prefix differs from its value — the
  • context-sources.additional is freeform (not an artifact-kind reference) and
  • A governance-profile selection that is valid but points at an artifact from a
  • The drift-guard test must fail if the copy grows extra kinds too, not only if

Requirements (mandatory)

Functional Requirements

IDTitleUser StoryPriorityStatus
FR-001Derive DRG node-kind set from NodeKindAs a doctrine author, I want the resolver's recognized DRG node kinds derived from the canonical NodeKind enum so that no kind is silently unrecognized.HighOpen
FR-002Drift-guard test (behaviour-pinned)As a maintainer, I want a drift-guard that pins the membership-gate behaviour — a monkeypatched/extra NodeKind member is recognized without editing topic_resolver.py — so future drift fails loudly in CI. (NOT literal set-equality, which is a post-derive tautology.)HighOpen
FR-003glossary_pack/mission_step_contract/etc. URNs resolveAs a charter author, I want previously-dropped DRG-URN kinds recognized at the resolution tier so that legitimate selectors no longer fall through silently.HighOpen
FR-004Remove context-sources.* from model + schemaAs a profile author, I want the redundant context-sources block removed from ContextSources, AgentContextSources, and agent-profile.schema.yaml so that there is one canonical profile-reference surface.HighOpen
FR-005Migrate authored refs onto *-references (set-merge, no loss)As a maintainer, I want a migration that set-merges (not appends) authored directives/tactics/toolguides/styleguides from context-sources. onto -references, and deliberately re-homes additional bindings that carry meaning (e.g. reviewer-renata's adversarial-evidence-disposition, pinned by a supply-chain test) rather than dropping them, so no authored intent is lost.HighOpen
FR-006Update 25 shipped profiles + all context-sources consumersAs a maintainer, I want the 25 packs/built-in/agent_profiles/*.agent.yaml profiles migrated AND every context-sources consumer updated (agent_profiles/__init__.py __all__, scripts/generate_schemas.py, scripts/doctrine/inline_reference_inventory.py, and the asserting tests) so removal breaks nothing silently.HighOpen
FR-007Extractor projects agent_profile edges from *-references; regenerate golden + reconcile overlayAs a doctrine consumer, I want the extractor to project agent_profile edges from *-references, the golden agent_profile.graph.yaml regenerated, and hand_authored_overlay.py reconciled (incl. the deliberate python-pedro/DIRECTIVE_034 delivery decision) with a composition-ledger entry, so migrated references reach a dispatched agent with an auditable graph.HighOpen
FR-008Close #3629 p2 (built-in e2e) AND implement org-tier fail-loudAs a mission-type author, I want (a) an end-to-end generate_graph test pinning the built-in assert_governance_scope_edges_resolve guard, and (b) net-new org-tier governance-profile scope extraction + fail-loud guard + tests (no org-tier path exists today), so a nonexistent selection fails loud on both tiers.MediumOpen
FR-009Fix the org fragment silent-drop at the two deficient callersAs a doctrine consumer, I want executor.py:362 and action_doctrine_bundle.py:192 to thread org_fragments=load_org_drg(repo_root, strict=False) (mirroring the 4 callers that already do) so an org pack's drg/fragment.yaml reaches those consumers. Fix at the callers, not the org_roots= seam (a seam fix double-folds for the dual-callers and mis-tiers org content).HighOpen
FR-010Refresh packs/internal/ READMEAs a maintainer, I want the internal pack's stale README updated (it omits the on-disk directives/ dir + OPERATOR_SIGNAL_CONTRACT node); the pack is already structurally conformant, so no restructure.LowOpen
FR-011Chain delivery verification (class-b internal + class-a 2nd fixture)As an operator, I want tests proving (b) built-in + spec-kitty-internal delivers every kind the internal pack declares across the executor/action-bundle seam, AND (a) built-in + internal + a 2nd minimal org fixture folds pack #2's fragment node/edge (the multi-org-pack path), so #3530 is evidenced for both classes.MediumOpen
FR-012Misconfigured-pack-in-chain fails loud (enumerated)As an operator, I want enumerated misconfig cases (nonexistent refine target; missing required fragment key; declared kind with no node) each to raise with a target-naming message (distinct from the honest "no graph" warning) so the chain never reports success over an inert pack.MediumOpen
FR-013Golden re-ledger doc-nit correctionAs a maintainer, I want the extractor procedure-branch docstring wording on golden re-ledger (extractor.py:557) clarified so that it matches the M2 WP04 reality.LowOpen

Non-Functional Requirements

IDTitleRequirementCategoryPriorityStatus
NFR-001No new suppressionsZero new # noqa, # type: ignore, or per-file ignore additions; new code passes ruff and mypy with zero issues.MaintainabilityHighOpen
NFR-002Test coverage for every new branchEvery new helper/branch (derivation, fail-loud guard, projection/removal, chain verification) has a focused test in the same work package.ReliabilityHighOpen
NFR-003Terminology guard greenpytest tests/architectural/test_no_legacy_terminology.py passes (Mission-not-Feature, canonical terms) before push.ComplianceMediumOpen
NFR-004Complexity ceilingTouched functions remain at cyclomatic complexity ≤15 (ruff C901 / Sonar S3776).MaintainabilityMediumOpen

Constraints

IDTitleConstraintCategoryPriorityStatus
C-001Single source of truthThe resolver's DRG node-kind set MUST be derived from NodeKind, not a hand-maintained copy (G2; no new shadow authority).TechnicalHighOpen
C-002Fail-loud over silent-pruneBoundary failures (unknown kind, nonexistent selection, misconfigured pack) MUST surface as an error/diagnostic, never a silent drop or a success count.TechnicalHighOpen
C-003No silent fallbackAny new resolution/validation path MUST fail closed with a structured error on ambiguity or absence — no silent fallback (repo doctrine).TechnicalHighOpen
C-004Consolidate on *-referencesThe resolved direction (DM-01M0PEAQ5G1VDR3CSJSV51SD8Y) is to remove context-sources. and consolidate on the top-level -references surface; no authored artefact-reference intent may be lost in migration.ProcessHighResolved
C-005Schema change requires migrationRemoving context-sources.* MUST be accompanied by a migration and MUST update all 25 shipped profiles; a profile authoring the removed block MUST be rejected, not silently ignored.TechnicalHighOpen
C-006No silent behaviour change to delivered contentConsolidation MUST preserve what reaches a dispatched agent today, verified by an empty golden agent_profile.graph.yaml diff except deliberately-ledgered deltas. The one known delta — python-pedro/DIRECTIVE_034 (overlay suggests link suppressed once 034 becomes a requires-diamond) — MUST be resolved deliberately and ledgered, never silent.TechnicalHighOpen
C-007#3514 and #3511 out of scopeThe P0 test-authority gap (#3514) and the pack-metadata integration cutover (#3511) are NOT in this mission's scope. #3412 (malformed-manifest → None) also stays out and open.ScopeMediumOpen

Key Entities (include if feature involves data)

the resolver's recognized set must derive from.

recognized node-kind set at the resolution tier.

(directives, tactics, toolguides, styleguides, doctrine-layers, additional); today only directives is projected.

agent_profile --REQUIRES--> directive) that carries authored intent to the consumer; candidate replacement for the inert context-sources fields.

that mint scope edges; the fail-loud guard target.

packs/built-in/ layer 0 + packs/internal/ org layer) whose every declared kind must reach its consumer; #3530's bug used to drop edges past the first.

org-tier pack; the #3530 verification fixture, to be updated to the latest structural conventions first (FR-009).

  • NodeKind: the canonical enum of DRG node kinds; the single source of truth
  • DRG URN: a <kind>:<id> selector whose <kind> is gated against the
  • ContextSources: the agent-profile block declaring reference channels
  • DRG edge / Relation: the canonical projection mechanism (e.g.
  • Governance-profile selection: a mission-type's selected_* bare-id lists
  • Org doctrine pack chain: an ordered set of doctrine layers (here
  • spec-kitty-internal pack (packs/internal/): the repository's own dogfooded

Success Criteria (mandatory)

Measurable Outcomes

canonical enum exactly (16 of 16 kinds; 0 missing, 0 extra), verified by a drift-guard test that fails on any future divergence.

(anti_pattern, asset, glossary, glossary_pack, mission_step_contract, template) are recognized at the resolution tier after the fix (measured: 6 of 6 now resolve where 0 did).

fails loud naming the offending id in 100% of cases on both the built-in tier (end-to-end generate_graph test) and the newly-implemented org tier; valid selections produce 0 new diagnostics (no false positives).

remain simultaneously schema-legal and never-read; all 25 shipped profiles carry references only on the *-references surface; 0 authored artefact references lost in migration.

built-in, delivers 100% of the kinds it declares — including the executor / action-doctrine-bundle seam that drops it today; (class-a) a built-in + internal + 2nd-org-fixture chain folds pack #2's fragment node/edge; and enumerated misconfigurations fail loud — together satisfying #3530's closing condition.

at complexity ≤15.

  • SC-001: The set of DRG node kinds recognized by the resolver equals the
  • SC-002: 100% of previously-dropped DRG-URN kinds
  • SC-003: A governance-profile selection naming a nonexistent artifact
  • SC-004: context-sources is removed entirely — 0 profile-reference kinds
  • SC-005: (class-b) The spec-kitty-internal pack, loaded as an org tier over
  • SC-006: 0 new lint/type suppressions introduced; touched functions remain

Assumptions

iterating all fragments) are already landed on main; this mission verifies and closes, it does not re-fix them.

child of #3530 and remains open after this mission.

full consolidation on *-references) from a 3-agent research squad; findings recorded in research/context-sources-drg-projection.md.

— this mission adds the end-to-end pin. The org-tier governance-profile fail-loud path is net-new (built here, not verified) — the #3629 close comment MUST disclose this (post-tasks G3); a follow-up issue may be filed for org-tier governance ergonomics beyond parity.

bump (DIR-009) and a next-unreleased migration name (m_3_3_1_*, since m_3_2_6m_3_3_0 already ship). Both are WP02 DoD items.

packs/internal/ (spec-kitty-internal) org pack as its fixture. Research established the pack is already structurally conformant (plural kinds + drg/fragment.yaml are the canonical org shape; pack.yaml/manifest deferred), so "update to current conventions" resolves to a README refresh (FR-010) plus the real code fix (FR-009, the org_roots-seam silent-drop). built-in + internal is the ≥2-layer chain. If a strict multi-org-pack chain (≥2 org packs) is needed to exercise the exact #3530 merge path, a second minimal org fixture may be added — resolved at plan time.

  • The #3530 per-seam org-tier fixes and the chain-merge fix (merge.py:1122
  • #3412 (YAML-syntax-malformed manifest degrades to None) is explicitly not a
  • The context-sources.* direction is resolved (DM-01M0PEAQ5G1VDR3CSJSV51SD8Y,
  • #3629 part 2 built-in guard is already fixed on main (commit d8beee2761)
  • Breaking change (context-sources removal) requires a CHANGELOG entry + version
  • Per operator direction, the #3530 chain verification uses the real
  • This mission targets a draft PR to upstream; the operator merges.