Skip to content

Project structure — evolutionary; substrate at apps/sdlc/lib/ today, the ontological solution as end-state

Status: open/accepted

  • Project structure is evolutionary: the SDLC/ontological product — substrate, protocol adapters, harness surfaces, and its applications — lives under apps/sdlc/ today and merges into solutions/ontological/ at end-state: one solution-local cluster whose consumers reach it only through published surfaces. See Decision and End-state target.
  • Vocabulary. An adapter is a protocol surface over the substrate (CLI, MCP, HTTP); a harness is an agent runner that consumes the substrate (Claude Code, Gemini); an application is a deployable expression of SDLC for people (web dashboard, desktop app). A harness is conceptually app-like; an application may bundle and deploy harnesses within itself.
  • The repo is polyglot — at least two ecosystems (TypeScript and Rust, n ≥ 2). The dashboard is TypeScript; a planned Tauri app brings Rust. This drives the workspace tooling in D-0012-monorepo-tooling.
  • Substrate lives at apps/sdlc/lib/ today, headed for the solution’s substrate subtree. No top-level src/ tree: consumption through published surfaces (compiled binary / npm externally, launcher shim / dev-link locally) keeps the source solution-local. See Current state.
  • apps/<name>/ holds deployable applications (each possibly polyglot internally); packages/<ecosystem>/ holds shared libraries partitioned by language. Tooling by D-0012-monorepo-tooling; dependency direction by S-0008-apps-consume-substrate-through-published-surfaces.
  • A solution (solutions/<name>/) is a cluster of apps and packages that belong to one solution and are shared only within it — typically a nested {apps/, packages/{ts,rust}}. Its sharing scope is narrower than repo-wide packages/ and wider than a single app. The tell: a solution carries multiple deployables (CLI, desktop, web, workers) sharing solution-local packages; an app is a single deployable, however polyglot inside. Placement follows sharing scope, and a package earns a wider home by observed use, not by up-front guess. See Placement and promotion.
  • The move is decided (2026-08-14) and executes as one dedicated mechanical PR in a freeze window — after the in-flight apps/sdlc PRs land and orchestrator dispatch is paused. See Transition rule.
  • apps/sdlc/lib/ internals and the registry/adapter-generation model are fixed by D-0007-deterministic-op-substrate, which refines this ADR.
  • sdlc.yaml and similar repo-level configs live at the repo root intentionally: SDLC’s configuration should be visible at top level. See Resolved questions.

Project structure is evolutionary. The end-state is the ontological solution: SDLC and ontological are one product — a control plane bundled and deployed to manage projects — and its whole cluster (substrate, protocol adapters, harness surfaces, applications, solution-local packages, its own planning unit) lives under solutions/ontological/. The separations survive as solution-internal subtrees under the same import discipline. The cluster stays at apps/sdlc/ until the move window. This ADR fixes both ends, the path between them, and the rule for when the move happens.

docs/ repo documentation; the root planning unit
solutions/
ontological/ the merged SDLC + ontological product — one deployable control
plane. Internal subtrees keep this ADR's separations: lib/
(substrate), cli/ + later mcp/ http/ (protocol adapters), the
harness surfaces (Claude plugin tree), apps/ (design-studio,
sdf, suspenders), packages/, docs/planning/
augmented/ sibling solutions
determined/
apps/ standalone single-deployable applications (pumice, zoo, …)
packages/ shared libraries, partitioned by ecosystem — see D0012
sites/ generated docs sites

The substrate is the solution’s lib/ subtree. Protocol adapters (cli/, later mcp/ and http/) are generated from the substrate’s op registry (per D-0007-deterministic-op-substrate) and are consumer-agnostic — a plain shell, CI, or any agent can drive them. Each harness surface (the Claude plugin tree today) consumes the substrate through an adapter; no harness depends on another. Applications — the solution’s own and everything outside it — consume the product only through its published surfaces (S-0008-apps-consume-substrate-through-published-surfaces): the compiled binary / npm artifact externally, the launcher shim or dev-link locally, a service’s HTTP/JSON API, or generated bindings — never by importing the source. An application may still include and deploy one or more harnesses within itself (a desktop app shipping an embedded agent). A solution (solutions/<name>/) stages a related cluster of apps and packages that are shared only among themselves, typically nesting its own {apps/, packages/{ts,rust}}. Shared libraries live in packages/<ecosystem>/, partitioned by language because a library is single-ecosystem — a package reaches that tier by being used outside its origin solution, per Placement and promotion. Tooling, build, and ship mechanics for apps and packages are fixed by D-0012-monorepo-tooling; the dependency rule by S-0008-apps-consume-substrate-through-published-surfaces.

What the ADR fixes is the separation — substrate, adapters, harness surfaces, and applications stay distinct subtrees inside the solution, never collapsed into one another — and the scope: the whole cluster is solution-local, because every consumer reaches it through published surfaces. The solution’s exact internal subtree names are its own (Placement and promotion).

docs/ as above
apps/ deployable applications (D0012) — agentpants/, legacy-polish/,
ontological/, pumice/, sdlc/, zoo/
solutions/ solution clusters — augmented/, determined/
packages/ shared libraries by ecosystem (D0012) — ts/, rust/
sites/ Astro docs sites (generated-content consumers; see D0010)

The former plugin/ tree is gone; SDLC lives at apps/sdlc/. apps/sdlc/lib/ houses the substrate; its internal organization (registry.ts, config/, util/, model/, services/) is fixed by D-0007-deterministic-op-substrate, and nothing under it is inherently harness-specific. The sdlc CLI under apps/sdlc/cli/ is a protocol adapter (consumer-agnostic, runs in a plain shell); apps/sdlc/{skills,.claude-plugin}/ is the harness-coupled surface. Both move with the cluster into solutions/ontological/.

apps/sdlc/ also carries a desktop app (desktop/), experiences, and products alongside the CLI — multiple deployables sharing local code, the solution shape named in Placement and promotion. That observation is now the ruling: the cluster is headed for solutions/ontological/. apps/pumice/ shows the contrasting case: internally polyglot (nested crates and packages), but a single deployable, so an app.

The solutions/ tier is live: solutions/augmented/ (first occupant) and solutions/determined/ (moved from apps/determined/). packages/rust/ is populated, no longer reserved.

The move apps/sdlc/solutions/ontological/ is decided (2026-08-14). It executes as one dedicated mechanical PR in a freeze window:

  • the in-flight apps/sdlc PRs have landed or been parked;
  • orchestrator dispatch is paused;
  • git mv preserves history. Deployables (desktop, products, experiences) may stage first; the substrate subtree moves last, where the tooling references concentrate.

The accompanying sweep: tsconfig @lib/@cli aliases, .moon/workspace.yml globs and the sdf source entry, root package.json workspaces, lefthook commands, the project-check scripts’ path prefixes, CI path filters, the release pipeline, and harness registrations (dev-link re-link). Consumers are unaffected: published surfaces do not move.

Per-application build and ship (e.g. committing built output into the plugin) stays fixed in each application’s own decision.

Ship now, defer correctly. Today’s only harness is the Claude Code plugin. Authoring substrate under apps/sdlc/lib/ matches that deployable’s mental model with zero ceremony: no build step, no symlink, no divergence between authored tree and runtime tree. The plugin is self-contained as authored.

Keep the consumer/substrate distinction discoverable. The end-state target names where substrate, adapters, harnesses, and applications intend to live. A reader asking “what is consumer-agnostic here?” sees the answer: anything under apps/sdlc/lib/ or apps/sdlc/cli/ today. Everything under apps/sdlc/skills/ and apps/sdlc/.claude-plugin/ is Claude-coupled. The same answer holds inside the solution at end-state — the subtrees keep the distinction.

Preserve P-0008-harness-agnostic-substrate. The substrate must remain operable without any one consumer. Hosting it under apps/sdlc/lib/ does not violate that, as long as no code in apps/sdlc/lib/ reaches into harness-coupled surface (apps/sdlc/skills/, apps/sdlc/.claude-plugin/). The boundary is enforced by import discipline, not by directory placement, until the lift.

Substrate ships unbuilt; build tooling is for applications. The substrate ships from source with no build step — authored path equals runtime path. Build tooling enters only for applications, which have a concrete consumer; an application’s build copies application output, not substrate, so the debugging tax of a build step is paid only where it buys something. The substrate itself stays unbuilt.

  • New substrate code goes to apps/sdlc/lib/<domain>/ by default, organized per D-0007-deterministic-op-substrate (registry.ts, config/, util/, model/, services/).
  • Per-domain subdirs under apps/sdlc/lib/<domain>/ are reserved for grouped data (the way entities/<type>/ groups per-type declarations). Code files stay flat at the domain root until concerns cluster.
  • Protocol-adapter code goes to apps/sdlc/cli/ (the only adapter today; mcp and http would land as sibling trees); harness-coupled code goes to apps/sdlc/skills/ and apps/sdlc/.claude-plugin/.
  • Per D0007, capability code becomes a registry op in apps/sdlc/lib/ exposed via the generated CLI, not a standalone script.
  • Skills (apps/sdlc/skills/<skill>/) may carry genuinely skill-private scratch helpers. An operation that acts on a domain entity or shared infra belongs in apps/sdlc/lib/ from the start; S-0001-co-locate-first-promote-when-shared governs skill-private helpers.
  • Import discipline: nothing under apps/sdlc/lib/ imports from apps/sdlc/skills/, apps/sdlc/.claude-plugin/, or other harness-coupled surface. The dependency arrow points only inward.
  • A standalone application goes in apps/<name>/; a related cluster goes in solutions/<name>/; code shared across applications or solutions goes in packages/<ecosystem>/<name>/ (partitioned by language — packages/ts/, packages/rust/). Which of the three, and when a package moves between them, is fixed by Placement and promotion. An application consumes the substrate through an adapter (the sdlc CLI, a service’s HTTP/JSON API, or generated bindings), never by importing apps/sdlc/lib/ — see S-0008-apps-consume-substrate-through-published-surfaces. An application may include and deploy harnesses within itself. Build and ship mechanics are fixed by D-0012-monorepo-tooling.

Placement follows sharing scope, not kind:

LocationSharing scope
apps/<name>/A standalone application — shared with nothing
solutions/<s>/Related apps + packages shared WITHIN the one solution
packages/<eco>/A package that spans 2+ apps or solutions — shared repo-wide

Shape is how sharing scope shows itself. A solution carries multiple deployables — CLI, desktop app, web app, workers — that share solution-local packages; solution-local packages exist precisely because several deployables need them. An app is a single deployable, even when internally polyglot: nested packages or crates consumed only by that one deployable keep it an app (apps/pumice/). Several sub-apps sharing local packages is solution shape (apps/sdlc/ — desktop, CLI, experiences, products — wears it today; an observation, not a placement ruling).

Solution-local packages stay workspace members: each solution enrolls its subtrees explicitly in the root registries (root package.json workspaces, Cargo.toml members, .moon/workspace.yml projects) — per-solution entries, not tier-wide wildcards — so workspace:* and { workspace = true } resolve as before. The tier fixes sharing scope; a solution’s internal layout is its own (solutions are heterogeneous — one may be a bare {apps/, packages/} container, another a full product suite with its own tiering): the directory is a sharing-scope signal, not a resolution barrier.

A package earns a wider home by observed use; its name never changes:

GateMoveTrigger
tier 0 → 1solution-local → packages/<eco>/A consumer OUTSIDE the origin solution imports it. A directory move plus a glob-membership change; resolution is unchanged because the package name does not move
tier 1 → 2shared packages/ → derived libraryIt clears the derived-library bar — stable API, docs, ownership — per LIBRARIES.md
tier 2 → 3derived library → externally-ownedIt nears 1.0: the standalone repo becomes the source of truth and this monorepo consumes published releases like any third-party dependency — per the tier-3 roster in LIBRARIES.md. First occupant: rust-path-opener

Default rule. Default a new cluster into solutions/<name>/ rather than scattering apps into apps/ and packages flat into packages/; a package EARNS packages/ by being reached for outside its solution; a redundant solution-local package is retired/folded within its solution.

The move is mechanical: the cluster relocates wholesale, and import paths, workspace registries, and tooling references update in the same PR. Contracts on the published surfaces stay the same, so consumers notice nothing — the binary, npm artifact, plugin trees, and APIs are unchanged.

augmented_books is the reference migration for the solutions/ tier. The augmented family was adopted flat into apps/ and packages/ts/, which scattered a single-solution cluster across two repo-wide roots and forced a convergence audit (T-JCML, now superseded). The staging tier answers that: relatedness is observed in place under solutions/augmented/, and each package earns packages/ by cross-solution use rather than by up-front guess. The move itself is T-C9RD.

  • Move timing. Scheduled by the freeze-window rule; tracked on the backlog. This ADR fixes the destination, not the date.
  • The solution’s internal renames. App and subtree names inside solutions/ontological/ are owned by the ontological solution plan (solutions/ontological/docs/plans/ontological.md).
  • Adapter / op-registry internals. The registry, op model, and adapter-generation mechanics are owned by D-0007-deterministic-op-substrate, not this ADR.
  • Top-level trees vs the solution model. Resolved 2026-08-14: the solution model. Earlier revisions targeted top-level src/ / adapters/ / harnesses/ trees; consumption through published surfaces makes the cluster solution-local, so those trees are no longer planned. Substrate bridging is likewise resolved in substance: consumers get the product via its published artifacts (binary, npm, dev-link), never the source.
  • Where sdlc.yaml lives. At the repo root, intentionally. SDLC is a sufficiently important subsystem to a consuming project that its configuration should be visible at project root, not buried in a config/ directory.
  • Trigger for the lift. Superseded: the move is decided; execution is gated by the freeze window, not by triggers — see Transition rule.
  • This ADR is the architectural complement to P-0008-harness-agnostic-substrate: P0008 names the value; this ADR fixes where the substrate lives in the tree today and where it is headed.
  • Import discipline for the new trees is enforced by S-0008-apps-consume-substrate-through-published-surfaces (apps and packages reach the substrate only through an adapter); the same inward-only arrow applies cross-harness at end-state.
  • The entity directories at apps/sdlc/lib/model/entities/ (one per entity type — backlog, capability, decision, driver, milestone, principle, product, reference, standard, task, term) are substrate — declarations consumable by any harness or app, under apps/sdlc/lib/ today, solution-internal at end-state.

← Back to Decisions