Applications consume the substrate only through published surfaces
Status: open/active
Summary
Section titled “Summary”- Application code under
apps/and shared libraries underpackages/<ecosystem>/reach SDLC capability only through a published surface — thesdlcCLI, a service’s HTTP/JSON API, or an API library (generated or explicitly authored) inpackages/<ecosystem>/— never by importing the substrate (solutions/ontological/lib/, latersrc/) directly. The dependency arrow points inward, never outward into a harness, and never down into a domain from a domain-blind component.
The term
Section titled “The term”A published surface is an interface whose consumers sit outside the
substrate’s refactoring reach, so its stability is a commitment — Fowler’s
published-versus-merely-public distinction. Three surfaces are published
today: the sdlc CLI (--output json), a service’s HTTP/JSON API, and the
API libraries under packages/<ecosystem>/. A generated protocol adapter
(D-0001-project-structure, D-0007-deterministic-op-substrate: CLI /
MCP / HTTP generated from the op registry) is one way a published surface is
produced; “adapter” keeps that narrow meaning and is no longer this
Standard’s umbrella term. “Protocol” is likewise reserved for wire-level
contracts (e.g. a stdio step protocol), which two of the three surfaces are
not.
An application is a consumer of the substrate, not an extension of it.
- No deep imports. Code under
apps/**MUST NOT import substrate modules (solutions/ontological/lib/**, or the top-levelsrc/**after the lift). It consumes capability through a published surface: shelling thesdlcCLI, calling the HTTP/JSON API a service exposes, or — preferably for typed in-process access — an API library inpackages/<ecosystem>/, either generated from the op registry or explicitly authored. The seam is always a published contract, never a raw substrate import. - No reach into a harness. Neither
apps/**norpackages/**imports harness-specific surface (solutions/ontological/skills/**,solutions/ontological/.claude-plugin/**, laterharnesses/**). Applications and agent runners are siblings; one never depends on another. packages/<ecosystem>/holds two things. It holds code shared across applications — a UI kit, and the API library (generated or explicitly authored) an app uses to reach the substrate; consumption code, or any code, reused by more than one app is promoted there. It also holds domain-blind components that know nothing about SDLC — a work-scheduling engine, a graph scheduler, a workflow-authoring kit (D-VSLI-distributed-work-runner-architecture:packages/rust/foreman,packages/rust/graph-scheduler,packages/ts/flowline). A domain capability (entity CRUD, policy, audit) is still born in the substrate per S-0001-co-locate-first-promote-when-shared, not in a package. The discriminator is the dependency arrow. A domain capability imports the substrate; a domain-blind component must never be able to. If the code needs to know what a Task is, what astatusvalue means, or where the corpus lives, it is a capability and belongs in the substrate.- No domain reach-down. A domain-blind component under
packages/**MUST NOT import the SDLC substrate (solutions/ontological/lib/**) or its corpus schemas. SDLC binds in through the component’s ports — supplying its corpus behind a work-order source port, its lifecycle as registered processes, its policy as filters and sorters — so the engine never binds out. A component that reaches down for a domain type has stopped being general: a second consuming project can no longer use it, and the test that keeps it honest (“a second instance must be conceivable at all times”) has already failed.
P-0008-harness-agnostic-substrate commits the substrate to running without any one consumer. An application that imports substrate internals welds the two together: the substrate can no longer change shape without breaking the app, and the app can no longer be retargeted (web to desktop, Bun/Hono server to Tauri’s Rust backend) without dragging substrate code along. A published surface is the seam that keeps both free. That seam is what lets the dashboard SPA serve from the Bun/Hono server today and embed in a Tauri binary later (see D-0012-monorepo-tooling) with the data layer as the only thing that moves.
The no-reach-down arrow is the same commitment read from the other end. A domain-blind component is only worth its placement if a second project can adopt it; one substrate import ends that, and no test catches it, because SDLC — the one consumer present — keeps working. So the constraint is a build constraint, enforced at the import boundary, not a description of intent.
How to apply
Section titled “How to apply”- Building a view? Reach the substrate through an API library (generated or
explicitly authored, in
packages/<ecosystem>/), the JSON API the dashboard service exposes, orsdlc <noun> <verb> --output json. Do notimportfromsolutions/ontological/lib/. - Sharing code between two apps? Promote it to
packages/<ecosystem>/<name>/as a deliberate refactor (the S-0001-co-locate-first-promote-when-shared promotion rule). Confirm it is library code, not a capability that belongs in the substrate. - Building a domain-blind component? State its ports first — the seams SDLC will bind through — and keep every SDLC-shaped concern on the far side of them. The adapted instance is a separate layer that wraps the component (P-0012-rust-core-adapters-as-needed); the component itself stays buildable and testable with SDLC absent.
- Reviewers: treat any
apps/**tosolutions/ontological/lib/**import, anyapps/**orpackages/**import ofsolutions/ontological/skills/**, and anypackages/**import of the substrate or a corpus schema, as a blocking finding — the validator below catches the common TypeScript forms, but reviewers remain the backstop for anything it cannot see. - This Standard is
open/active: it is enforced by.claude/skills/project-check/check_apps_imports.ts, a pre-commit gate (wired inlefthook.ymlasproject-check-apps-imports) that scans every source file underapps/**andpackages/**and fails the commit withfile:linecitations on any import of the substrate (solutions/ontological/lib/**,src/**) or the harness surface (solutions/ontological/skills/**,solutions/ontological/.claude-plugin/**) — whether written as a relative path that climbs into those trees or via the@lib/alias.
Anti-examples
Section titled “Anti-examples”apps/dashboard/importingsolutions/ontological/lib/services/dashboard/server.tsto read leases in-process — a deep substrate import. The app should call the service’s JSON API instead.- A
packages/module importingsolutions/ontological/skills/task-work/helpers — reaching into harness-specific surface. - Implementing lease parsing inside
apps/dashboard/— a capability hiding in an application; it belongs in the substrate per S-0001-co-locate-first-promote-when-shared. - The work-runner engine importing the Task schema to decide whether a work order is dispatchable — a domain reach-down. The dispatch predicate is a filter SDLC supplies through the scheduling port; the engine sees only the domain-neutral work-order payload.
Derived from the import-discipline note in D-0001-project-structure (a
future Standard could enforce: no solutions/ontological/lib/ module imports
harness-coupled surface, and the same rule applies cross-harness). This
Standard extends that arrow to the new apps/ and packages/ trees
introduced by D-0012-monorepo-tooling.
Retitled 2026-08-14 from “…only through adapters”: the old umbrella term collided with D-0001-project-structure’s narrower meaning of adapter (a generated protocol surface), which two of the three published surfaces never were. Rule content unchanged; the file was renamed to match and referencing wikilinks swept.
Enforcement gap — Rust. The no-domain-reach-down arrow is enforced for
TypeScript by .claude/skills/project-check/check_apps_imports.ts, but its
SOURCE_EXTS set covers only .ts, .tsx, .js, .jsx, and .vue, so
every file under packages/rust/** is skipped and the tree is unguarded. The
Rust-side equivalent is a cargo-manifest dependency check — reading each
crate’s Cargo.toml for a dependency on a substrate or domain crate — rather
than an import scan, since a Rust crate declares its reach in the manifest
before any use statement appears.