Skip to content

The module graph — derived structure and declared control units

State: open/proposed

  • 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 under solutions/; library suites, including the engine, sit under packages/<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: in sdlc.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.

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:

PieceWhat it givesWhere
Manifest walkPackages and crates, containment, dependency edges@sksizer/manifest-graph, crates/manifest-graph
Capability coverageEach capability landed on the deepest unit its location falls insdlc capability coverage
System-graph experienceForce, diagram, and table renderings over a SystemGraph, with a foldable containment tree and a source switch@sdlc/exp-system-graph, hosted by SDF
Planning rootsSeveral planning corpora in one repoDecided 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.

Two dimensions, two terms.

TermWhat it isSourceCarries
ModuleA 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 unitThe 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:

RuleConsequence
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:

PartSource
SummaryDerived from the unit’s corpus root (see the summary rule below)
SurfacesThe published surfaces of its member modules
Capabilities claimedCapabilities whose locations land on its member modules, by the sdlc capability coverage join
RelationsContains (child units), depends on and consumes (from structural edges that cross its boundary)
RollupsEntity 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.

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/.

WhatWhere
Every control unitCo-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 treesListed under the claiming unit’s members:
Repo-wide definitions (principles, standards, terms)The repo root corpus
The single point of accessDerived: the generated index and SDF over planning_roots:, not one folder
Links into another repoQualified 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).

D-SK9G-cross-project-addressing’s planning_roots: entry carries root and label, plus two optional keys this decision defines:

KeyMeaningDefault
scopeThe directory whose modules the unit takes by containment.The parent of docs/planning when root ends in it, else none.
membersExtra 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-pants

The full declaration for this repo’s 12 units is in docs/planning/notes/N-A6ZD-module-breakdown.md.

  • 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.
  • 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 related link 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.

OptionWhat it isForAgainst
A. A Module entity kindA 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 valuesTreat 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.

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.

M-D2I1 carries the work:

StepTaskNeeds
StructureT-N3RW-derive-the-structural-module-graph-from-existing-manifests—
Control unitsT-CY6T-control-units-a-planning-root-is-the-boundary-and-everyStructure; planning_roots: key (T-K9AI-slug-projects-and-planning-roots-keys-in-sdlc-yaml-with)
The opT-JJHY-sdlc-module-graph-emits-structure-and-control-units-as-jsonStructure; control units
Outside view and rollupsT-SX0F-control-unit-outside-view-summary-capabilities-claimed-andThe op; union loading (T-G3EH-federated-planning-roots-union-loading-flat-resolution-union)
This repo’s unitsT-EIZX-declare-this-repo-s-own-control-units-as-planning-rootsControl units; union loading
Dashboard routeT-S4K4-serve-the-module-graph-to-sdf-through-the-dashboard-apiThe op
SDF graphT-9TBQ-sdf-draws-the-module-graph-control-units-collapsed-structureDashboard route
SDF drill-downT-KWJS-sdf-control-unit-drill-down-outside-view-rollups-andSDF graph; rollups
Inheritance and resolutionT-XIU4-nearest-first-link-resolution-and-the-add-never-redefineControl units; union loading
Peer reposT-CTEV-cross-repo-modules-and-qualified-links-in-the-module-graphThe op; roster and qualified links (T-KBOI-qualified-wikilink-pattern-resolver-and-the-audit-unverified)
Suite folders (optional, each)T-SKMB-co-locate-the-engine-suite-under-packages-engine (to packages/engine/), T-Z0KK-co-locate-the-markdown-suite-under-packages-markdown, T-PLW6-co-locate-the-app-kit-suite-under-packages-app-kit, T-G6KG-co-locate-the-cli-substrate-suite-under-packages-cliStructure
  • Whether the first readiness and drift rules hold, and what “status” rolls up to for a unit.
  • Whether fleet splits out of cli-substrate. Undecided; split only when a milestone changes one side alone.
  • cage as its own unit once it ships apart from zoo. Likely.
  • determined keeps its own sdlc.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 graph once definition and execution split.

← Back to Decisions