The module graph — derived structure and declared control units
State: open/proposed
Summary
Section titled “Summary”- The graph has two dimensions. Structure is every code module and how it relates to others. It is derived and never declared. Control is the set of units the project manages as one thing. It is declared. See Decision.
- A control unit is a planning root. It has one planning directory, its internal model, and one outside view. Every code module belongs to exactly one unit. Moving a boundary changes config, not code. See Decision.
- The outside view and rollups belong to control units. Summary, surfaces, capabilities claimed, relations, and rollups of status, readiness, and drift. Code modules carry none. See Decision.
- A unit is a folder, with its corpus inside. Every unit’s planning root is
<folder>/docs/planning/. Product-shipping solutions sit undersolutions/; library suites, including the engine, sit underpackages/<suite>/. The repo root keeps only what is repo-wide.members:is only for cross-tree exceptions. See Placement. - Ownership is disjoint. One root per entity; views overlap through links; rollups count ownership. See Decision.
- Definitions inherit down the containment tree. A unit adds but never redefines; work kinds never inherit. See Decision.
- No new entity kind. Structure comes from the manifests that exist. Control comes from
planning_roots:insdlc.yaml(D-SK9G-cross-project-addressing). See Options considered. - SDF draws it first, read-only. Control units collapsed by default, structure visible on drill-in, as the first milestone (M-D2I1). See Migration.
Context
Section titled “Context”This repo holds modules at several depths. The repo holds solutions/ontological, which holds
desktop. The determined suite sits beside it. Peer repos sit outside it. Every entity lives in one
root corpus today, so nothing says which module a decision or a task belongs to.
The vision makes the module graph phase 1. Multi-repo dispatch, SDF’s structural views, and publishing the definition half all assume it.
Parts of it exist:
| Piece | What it gives | Where |
|---|---|---|
| Manifest walk | Packages and crates, containment, dependency edges | @sksizer/manifest-graph, crates/manifest-graph |
| Capability coverage | Each capability landed on the deepest unit its location falls in | sdlc capability coverage |
| System-graph experience | Force, diagram, and table renderings over a SystemGraph, with a foldable containment tree and a source switch | @sdlc/exp-system-graph, hosted by SDF |
| Planning roots | Several planning corpora in one repo | Decided in D-SK9G-cross-project-addressing; not landed |
What is missing is the idea of a unit the project manages, and a view of each unit from outside.
Two drivers ask for it. DR-8JAH: the system is too big to hold in one head or one corpus, and
too hard to coordinate across people and agents. DR-6CO5: a chosen set of modules must be
managed as one thing while the structure across and inside it stays visible.
Decision
Section titled “Decision”Two dimensions, two terms.
| Term | What it is | Source | Carries |
|---|---|---|---|
| Module | A structural node: a code module such as a package, crate, app, or skill pack. | Derived from manifests. Never declared by hand. | Kind, directory, ecosystem, published surfaces, structural edges |
| Control unit | The set of modules managed as one thing. | Declared as a planning root. The project root is the default unit. | One planning directory (its internal model) and one outside view |
A control unit can be one package that holds several internal code modules. It can also be a suite of several packages and apps. The structure inside a unit stays visible either way.
Structure. Nodes are code modules. Edges are contains (directory nesting), depends-on
(declared dependencies), and consumes (one module reads another’s published surface,
S-0008-apps-consume-substrate-through-published-surfaces). External systems are nodes outside
the containment tree. Structure is always visible, including edges that cross control boundaries.
Control. A planning root is a control boundary. Membership is derived: a code module belongs to
the deepest unit whose directory contains it, unless another unit claims it under members:. The
rules:
| Rule | Consequence |
|---|---|
| Every code module belongs to exactly one control unit. | Audit fails on a module claimed by two units. The root unit takes every module no other unit claims, so no module is orphaned. |
| Structural edges may cross control boundaries. | A dependency between units is data, not an error. It becomes an edge between the two units. |
| Rollups follow control units. | Counts and states sum over a unit’s planning directory and its child units, never over code modules. |
| Moving a boundary is a declaration change. | Editing planning_roots: regroups modules. No file moves. |
The outside view. From outside, a control unit is one node:
| Part | Source |
|---|---|
| Summary | Derived from the unit’s corpus root (see the summary rule below) |
| Surfaces | The published surfaces of its member modules |
| Capabilities claimed | Capabilities whose locations land on its member modules, by the sdlc capability coverage join |
| Relations | Contains (child units), depends on and consumes (from structural edges that cross its boundary) |
| Rollups | Entity counts by kind and status, readiness, and drift, from its planning directory, plus a total that includes child units |
Summary rule. A unit’s outside-view summary is derived from its corpus root, never hand-set. Product entities inside the unit are unaffected.
First rollup definitions: readiness is the share of the unit’s open tasks at open/ready. Drift is
the count of audit findings in its planning directory plus capability locations that no longer
resolve. Each rollup names its rule in the output.
Nesting and repos. Units nest to any depth by directory. A peer repo is a unit, reached through
the projects: roster, and a link into it is a qualified id (D-SK9G-cross-project-addressing).
An absent peer is one unverified unit.
One op. sdlc module graph --output json returns both layers: the structural nodes and edges,
and the control units with their members and outside views. Its JSON schema is the wire contract.
SDF reads it through the dashboard API.
SDF first, read-only. SDF’s system-graph experience gains a Modules source. Control units are collapsed containers by default. Unfolding one shows its member modules and their structural edges. Selecting a unit opens its outside view and links into its internal model.
Placement
Section titled “Placement”Decided: the hybrid layout, with suites co-located. A unit is a folder. Product-shipping
solutions live under solutions/; library suites, including the engine, under packages/.
| What | Where |
|---|---|
| Every control unit | Co-located: <unit folder>/docs/planning/ |
A solution that ships products (ontological, determined, agent-pants) | solutions/<solution>/ |
A library suite (engine, markdown, app-kit, cli-substrate) | packages/<suite>/, grouped by suite, not by language |
| A module claimed across trees | Listed under the claiming unit’s members: |
| Repo-wide definitions (principles, standards, terms) | The repo root corpus |
| The single point of access | Derived: the generated index and SDF over planning_roots:, not one folder |
| Links into another repo | Qualified wikilinks and the projects: roster (D-SK9G-cross-project-addressing) |
There is no docs/planning/units/ and no docs/suites/. Library suites stay in the packages/
tier of D-0001-project-structure, so its publishing rules hold. The engine is a library suite:
by D-0001’s tier rule, what one solution consumes from another lives in packages/, and Ontological
consumes Foreman. A suite’s planning root
sits at the suite folder, above its packages, so no package snapshot or crate archive ships it.
Moving code into the suite folder is optional per suite. It is what makes a suite a folder. Until a suite moves, its members stay where they are. Moving a boundary is still a declaration change.
members: remains only for cross-tree exceptions, such as agent-pants claiming
exp-agent-pants and product-agent-pants out of solutions/determined. Those modules may move
later too.
Each unit carries its own decisions. The root holds only what is repo-wide, which every unit inherits (Inheritance).
The planning-roots shape
Section titled “The planning-roots shape”D-SK9G-cross-project-addressing’s planning_roots: entry carries root and label, plus two
optional keys this decision defines:
| Key | Meaning | Default |
|---|---|---|
scope | The directory whose modules the unit takes by containment. | The parent of docs/planning when root ends in it, else none. |
members | Extra paths the unit claims, each with everything under it. | None. |
An explicit claim beats containment. Two explicit claims on one path fail audit. The summary is never set through this block. Example, one co-located suite and one cross-tree exception:
planning_roots: engine: root: packages/engine/docs/planning label: Foreman engine agent-pants: root: solutions/agent-pants/docs/planning label: Agent Pants members: - solutions/determined/experiences/agent-pants - solutions/determined/products/agent-pantsThe full declaration for this repo’s 12 units is in
docs/planning/notes/N-A6ZD-module-breakdown.md.
Ownership and views
Section titled “Ownership and views”- One home per entity. Every entity file lives in exactly one planning root, so it belongs to
exactly one control unit — the same rule as modules. In the hybrid layout the folder is the
declaration; no
unit:frontmatter field is needed. - Edges may cross. A wikilink between entities in different units is a relation, not membership. A unit’s view is its owned entities plus inbound and outbound edges.
- Rollups follow ownership. Counts, readiness, and drift aggregate owned entities only, so nothing is double-counted.
- Audit suggests, never assigns. When an entity’s paths (
locations[], files-to-touch) and links all point at one unit but it is filed in another root, audit flags a likely misfile. Moving it is a file move between roots — a declaration change.
Inheritance
Section titled “Inheritance”- A unit sees its ancestors’ definitions. The definition kinds — principle, standard, decision, term, reference, product, driver, capability — declared in a parent unit’s corpus apply inside every child unit, down the containment tree. Repo-wide definitions therefore live once, at the root, and reach every unit.
- A unit may add, never redefine. A child may declare its own definitions; it cannot
re-declare an ancestor’s id or override its text. Where a child needs a narrower rule, it writes
a new entity that
related-links the ancestor and states the narrowing. - Work kinds do not inherit. Tasks, milestones, and backlog are owned where they are filed and never flow down or up; they appear in ancestors only through rollups.
- Resolution is nearest-first. When a wikilink target exists in several roots (which audit
forbids for ids, but slugs can repeat), the nearest ancestor wins; cross-repo targets go through
the
projects:roster (D-SK9G-cross-project-addressing). - Audit checks the chain. A child that re-declares an ancestor’s id fails audit; a child
definition that contradicts an ancestor without a
relatedlink to it is flagged.
Structure is a fact; control is a choice. Manifests already say what depends on what. Who manages what is a decision people make and change. Keeping the two apart lets structure stay accurate without upkeep, and lets a boundary move without touching code.
Planning roots are already the declaration. D-SK9G-cross-project-addressing adds
planning_roots: so a repo can hold several corpora. A control unit needs exactly that: a directory
for its internal model, named in one config file. A second mechanism would drift from it.
Rollups need one owner per module. If a module sat in two units, its capabilities and work would count twice. The one-unit rule makes every sum well defined.
Reuse what renders. The system-graph experience already draws containment and dependency over a
SystemGraph, with a source switch. A projection is cheaper than a new page, and it is how the
capability source was added.
Options considered
Section titled “Options considered”| Option | What it is | For | Against |
|---|---|---|---|
| A. A Module entity kind | A new MD-XXXX entity per module, hand-written, with kind, members, and relations. | Explicit. Reviewable. Can carry prose. | Restates what manifests say, so it drifts. Needs a producer and a consumer before it ships (D-3PDM-cross-project-data-model-adoption). Hundreds of records for this repo alone. |
| B. Derived structure plus declared control units (chosen) | Structure from manifests. Control units from planning_roots:. The outside view derived per unit. | Nothing to keep in sync for structure. One declaration per boundary. Builds on a decided config key. | A unit’s summary is only as good as its corpus root. Blocked on the planning_roots: key landing. |
| C. Product and capability kind values | Treat a product entity as a unit and a capability kind: module as a module. | Uses existing kinds. Products already carry a summary and boundary. | Capabilities describe what a system does, not its code layout. Products are few, so most modules would have no node. Mixes the definition corpus with structure. |
B is the recommendation. Product entities stay where they are and still describe what a unit ships.
Consequences
Section titled “Consequences”Easier. SDF gets one map of the project. Rollups have one definition. A new package appears in the graph with no edit. A suite can be managed as one unit by one declaration.
Harder. Every tool that counts work must count by control unit. Audit gains the one-unit check.
A suite becomes a folder only by moving its code. A module claimed across trees is listed under
members:.
Binds future work. Multi-repo dispatch reads units from this graph. The definition package decides where the op lives (D-ME8T-definition-and-execution-packages): structure and control are definition-side; rollups over tasks are execution-side.
Breaks nothing today. With no planning roots declared, the whole repo is one unit.
Migration
Section titled “Migration”M-D2I1 carries the work:
Open questions
Section titled “Open questions”- Whether the first readiness and drift rules hold, and what “status” rolls up to for a unit.
- Whether
fleetsplits out ofcli-substrate. Undecided; split only when a milestone changes one side alone. cageas its own unit once it ships apart fromzoo. Likely.determinedkeeps its ownsdlc.yaml, which D-SK9G forbids under a sub-root. Per-root verbs or a nested project are the options.- The rule for code modules below the manifest grain, such as directories inside one package.
- How SDF addresses a unit in another repo: by slug and planning-root id, or by a qualified id.
- Which package owns
sdlc module graphonce definition and execution split.