Contracts
drg-writer-discovery-gate.md
Contract — DRG Writer-Registry Unification + Discovery Gate
Owning concern: IC-06 · Requirements: FR-010, NFR-005/006 · Closes: #3075, #2977 (iff all land).
C1 — Single canonical document serializer
Every src/ site that serializes a DRGGraph to a document dict (the five top-level keys schema_version, generated_at, generated_by, nodes, edges) MUST delegate to graph_document_to_dict (src/doctrine/drg/migration/extractor.py:1424). The three known sites MUST be converted:
raw .model_dump(), so it also MUST route node/edge dicts through model_to_graph_dict to restore FIELDS_WITHHELD_FROM_GRAPH_OUTPUT + omit-when-empty discipline.
src/specify_cli/migration/rewrite_opposed_by.py:_write_graph(#2977)src/charter/synthesizer/project_drg.py:_serialize_graphsrc/specify_cli/doctrine/pack_assembler.pyprune (~495-501) — currently bypasses the mapping funnel via
C2 — Registry membership
Each converted site is registered as a DocumentWriter member (src/specify_cli/drg_writers/registry.py) so the existing member-iterating completeness test (test_registry_completeness.py:199-220) covers it.
C3 — Discovery gate (non-vacuous, BOTH shapes)
A NEW gate MUST scan src/ for graph-document emitters in BOTH shapes — (i) dict literals carrying schema_version+nodes+edges, and (ii) DRGGraph→dict/YAML dumps via raw .model_dump() (the pack_assembler shape) — and assert each callsite delegates to graph_document_to_dict and/or is a registry member. The gate MUST be non-vacuous: the self-mutation battery injects an unregistered dict-literal writer AND an unregistered .model_dump()-shaped writer, each proven to red INDEPENDENTLY (a single dict-literal mutation leaves clause (ii) — the shape that motivated the gate — unproven; memory: gate-unmask can't self-validate; prove the gate PARSES its authority, not literal-vs- literal). Bounded claim: the gate closes the known literal + .model_dump() shapes + regressions of them; graph-document construction via merge/comprehension/**spread remains uncovered → residual note.
C4 — Protocol typing (fold-in)
Replace the object-typed repository surfaces (progressive_disclosure.py:216; context.py:552,568,1520, 2526,2757,3375,3515-3518) with an ArtifactRepository Protocol (get(id)->T|None, get_provenance(id)->str|None) that the concrete repos already structurally satisfy via BaseDoctrineRepository. The 12 # type: ignore[attr-defined] suppressions MUST be REMOVED (NFR-005 — net-removal, no new suppressions), mypy --strict green.
C5 — Closure
Close #3075 AND #2977 only if C1+C2+C3+C4 all land (C-007). Otherwise leave both open with a residual note naming what remains.
Acceptance (ATDD)
dict-literal writer and (b) an injected unregistered .model_dump()-shaped writer; each greens when it delegates to graph_document_to_dict (NFR-006, both clauses proven).
- A1: adding a hypothetical new top-level
DRGGraphfield, all three sites emit it (RED before C1). - A2: the discovery-gate self-mutation battery reds independently for (a) an injected unregistered
- A3:
mypy --strictpasses with the 12# type: ignore[attr-defined]removed (NFR-005).
suggests-delivery-walk.md
Contract — Profile-Channel suggests Delivery Walk
Owning concerns: IC-01, IC-02 · Requirements: FR-001/002/003, FR-006, NFR-003 · Layer split: doctrine walk stays node-level; when surfaced in the charter/consumer layer (D1).
C1 — Relation set
PROFILE_CHANNEL_RELATIONS (src/doctrine/drg/reachability.py) MUST include Relation.SUGGESTS in addition to REQUIRES and SPECIALIZES_FROM. The action channel (action_channel_reachable/ resolve_context) is a SEPARATE walk and MUST remain unaffected.
C2 — when surfacing (reuse link_references, do NOT re-derive)
For every artefact delivered because it was reached via a suggests edge, the consumer MUST surface an applicability condition equal to that edge's when clause; when the edge has no authored when, surface STATED_DEFAULT_WHEN (never empty/undefined).
Seam (D11 — critical): when lives on the edge that reaches the artefact (an inbound edge source → target), and profile_channel_reachable returns visited − seed_set — it strips the profile seeds, which are exactly the sources of the first-hop Family A/B edges (architect→DDD, profile→DISCIPLINED_REFACTORING). Therefore a per-reached-node edges_from(reached, SUGGESTS) MISSES those edges and surfaces no when for the headline families. The consumer MUST instead reuse the existing link_references(merged, roots=profile_seeds, delivered=<kind-filtered reached set>, bridge_urns=reached ∪ seeds) projection (progressive_disclosure.py:110-154 + edge_to_reference at :52-68), which iterates sources = roots ∪ delivered ∪ bridge_urns and keeps (target, when) where target ∈ delivered. Note reached ∪ seeds == visited, so no second walk is needed — the consumer already holds the seeds it passed in. Do NOT author a bespoke projection or a reachability_delivery/ copy of link_references.
C3 — Delivery cadence (links, not bodies)
Suggested artefacts MUST be delivered as when-labelled REFERENCES (links) — the "fetch + when-doing" stanza — not as inlined full bodies. Artefacts inside the roots' requires-closure keep their eager full body (existing cadence); suggests-only artefacts render as links. requires precedence wins on a diamond (an artefact reachable via both requires and suggests delivers eager, once). Delivery MUST build on _ActionDoctrineBundle.bridge_urns + the requires-closure render cadence (C-003), not re-derive.
C4 — Kind delivery
Following suggests reaches non-procedure kinds (paradigm/styleguide/directive/tactic/toolguide). The consumer's NodeKind delivery table (context.py) decides which kinds deliver; the sole existing consumer profile_channel_procedure_ids (procedures-only) MUST be widened accordingly (OQ-3 resolved in IC-01).
C5 — Dedup & determinism
(target, relation) pairs dedup across multiple source profiles / diamonds; delivered reference lists are deterministically ordered (stable sort by URN), matching the existing payload determinism.
C6 — Forward-API consumption (allowlist retirement)
Each forward-API symbol used to implement this walk becomes genuinely src-imported-and-used. As each is consumed it is removed from _CATEGORY_C_DELIVERY_RAIL_FORWARD_API + its _baselines.yaml mirror (final sweep in IC-05); test_no_dead_symbols stays green. A symbol referenced ONLY in tests is NOT removed.
Acceptance (ATDD — assert on the PROFILE channel, D10)
Entry point MUST be profile_channel_reachable(graph, {agent_profile:…}) + the profile render path (_render_profile_sections/profile_channel_procedure_ids). doctrine.drg.query.resolve_context (the ACTION channel) is FORBIDDEN as the entry point — DDD is already action-reachable there (vacuously green) and resolve_context from a profile seed reaches nothing (test_reachability.py:720, permanently red).
is the guaranteed walk-level red — it pins the relation set that FR-001 widens.
paradigm:domain-driven-design (Family A) and the render surfaces its when (RED today — edge A12 inert).
is authored; assert on a still-when-less edge to prove the default path).
precedence collapses it — target ∈ delivered filter + partition_delivery split).
- A0 (mechanical red):
test_reachability.py:710 test_profile_relations_are_requires_and_specializes_from - A1:
profile_channel_reachable(graph, {agent_profile:architect-alphonso})includes - A2: an implementer profile's channel includes the
refactoring-*tactics (Family B) withwhen. - A3: a
suggestsedge with no authoredwhensurfacesSTATED_DEFAULT_WHEN(Family A after backfill - A4: a diamond artefact (reachable via
requiresANDsuggests) is delivered once, eager (requires - A5: suggested artefacts appear as references (links), not inlined bodies (NFR-003).
- A6: the action channel's delivered set (
resolve_context) is unchanged by this WP (isolation).