Files
NODEDC_MISSION_CORE/docs/18_APPLICATION_COMPONENT_ARCHITECTURE.md

15 KiB

Mission Core application component architecture

Status: accepted, 2026-07-26

Decision

Mission Core adopts the same dependency discipline as the NODE.DC Design Guideline without copying its application code.

The Design Guideline remains the visual platform:

@nodedc/tokens
  → @nodedc/ui-core
    → @nodedc/ui-react
      → Mission Core application

Inside Mission Core, dependencies flow in one direction:

typed contracts and data adapters
  → reusable domain components and renderers
    → feature workspaces
      → product registry and reviewed composition roots
        → App shell

A lower layer does not import a higher layer. The application owns domain meaning and data flow; the Design Guideline owns general visual controls, surfaces, interaction states, icons, and shell geometry.

This dependency direction constrains ownership, not product composition. New workspaces may have different information architectures when their operator jobs differ. Their placement and form follow docs/19_PRODUCT_SURFACE_EXTENSION_PROTOCOL.md.

Why Design Guideline code is not copied

The reusable architecture in NODEDC_DESIGN_GUIDELINE is its published package boundary, registries, validation, and one-way imports. Its living catalog is an application and is not a source template to paste into Mission Core.

Mission Core consumes @nodedc/tokens, @nodedc/ui-core, and @nodedc/ui-react. A visual primitive missing from those packages is proposed and approved in the Design Guideline first. Mission Core does not fork it.

The current file: package dependencies are suitable for the local standalone development contour, but they are not an immutable distribution contract. Before portable CI or deployment, the packages must be published, vendored with content verification, or pinned to a verified donor revision.

Internal layers

src/core

Owns typed domain contracts, parsers, API adapters, state machines, and domain-specific hooks. It may depend on React for a hook or context boundary, but it does not import visual adapters, workspaces, or the App shell.

src/components

Owns reusable Mission Core domain components such as observation sources, timelines, media adapters, and laboratory presentation/evidence frames. Components may consume core contracts and canonical Design Guideline exports. They do not select routes, own product navigation, or import workspaces.

src/workspaces

Owns domain composition. A workspace connects core data to reusable components and renderers. A new domain gets its own module or feature directory; it is not appended to the central workspace hub.

workspaces/contracts.ts is the host contract shared by workspace implementations. The central Workspaces.tsx is a composition hub for existing generic workspaces, not a home for LAB-specific adapters or evidence logic.

src/workspaces/laboratory

Owns the laboratory catalog feature. It may compose:

  • the reusable laboratory presentation components;
  • immutable LAB adapters from core/laboratory;
  • domain evidence renderers;
  • the generic spatial workspace through the explicit SpatialView contract.

Every new LAB adds a bounded result component and typed data adapter here. It does not add LAB imports, selectors, styles, or conditional evidence code to Workspaces.tsx or App.tsx.

src/composition

Owns reviewed integration imports and registries. Vendor/plugin contributions enter Core only through this layer and the public plugin SDK. Composition does not become a second business-logic layer.

src/productModel.ts

Is the typed application registry for roots, workspaces, labels, icons, and declared capabilities. Navigation and routing project from it rather than duplicating lists across the App shell.

It is a product registry, not a runtime ontology and not a substitute for evidence or domain contracts.

New workspaces and roots enter this registry only after their user job and placement are resolved. The registry must not accumulate placeholder pages for unapproved future ideas.

src/App.tsx

Owns shell-level orchestration: selected root/workspace, global panels, runtime providers, and passing typed controllers to a workspace. It must not absorb domain API calls, per-LAB renderers, or new visual primitives.

Shared LAB launch profiles — 2026-09-21

Test devices and Spatial scene are shared infrastructure, not children of the planner. DeviceWorkspace imports only the generic device host and its UI contracts; it must render without a PlanningTestProvider.

  • Direct navigation to either surface selects the direct launch profile. A page reload also starts with direct, even if the server retains a completed or interrupted planning run. Model selection remains the device host's state.
  • A successful planner start or explicit reopen passes planning through WorkspaceNavigation.openView. The shell composes PlanningConnectionWindow only for that entry. Device connection and spatial-control callbacks preserve the originating profile when opening the scene.
  • PlanningTestProvider owns run data and polling, not navigation selection. Reading /live-tests/active must never change the launch profile. An unsuccessful explicit run selection must not open a stale run's connection or scene.
  • A nonterminal planning consumer that has not bound a query session still waits for the next capture. The composition-level PlanningCaptureGuard requires an explicit return to that study or completion of the study before direct capture; it does not send scanner, network, or recording commands. It waits for a terminal server state, not merely acknowledgment of the stop request. Completed runs and runs already bound to a recording do not claim a later direct launch.

Changing presentation does not delete evidence, end acquisition, or restart an experiment. The planner continues to consume an explicitly started recording; ordinary acquisition requires neither a reference route nor a planner draft.

Regression coverage: test/workspaceLaunch.test.mjs, including independent model catalog rendering, explicit handoff, terminal-run isolation, failed selection, and both shared-scene entry paths.

Acceptance on the canonical operator service 127.0.0.1:8000: architecture checks, TypeScript, all 882 frontend tests and production build passed. In-app browser QA confirmed direct entry with retained completed evidence, catalog → XGRIDS connection, the unchanged connection-method selector and Escape, normal/expanded layouts, ordinary scene entry, and planner → direct-device navigation. No BLE discovery, provisioning, acquisition or new field run was performed during this UI acceptance.

CSS ownership

CSS follows the same feature boundary:

  • general workspace composition lives in styles/workspaces.css;
  • laboratory presentation and E30 evidence layout live in styles/laboratory.css;
  • spatial, observation, device, and shell rules remain in their named files;
  • plugin CSS is plugin-local and scoped below its contribution root.

Application CSS may arrange domain content. Canonical control geometry and states remain in the Design Guideline packages. A feature must not add selectors to a generic CSS bucket merely because that file is already imported.

Component admission

A Mission Core component is admitted only when all of the following are true:

  1. Its responsibility can be stated in one sentence.
  2. Its inputs are typed domain data or callbacks, not hidden global state.
  3. It does not duplicate a Design Guideline entity.
  4. It has explicit loading, empty, unavailable, and error behavior where relevant.
  5. It has no knowledge of unrelated workspaces or vendor implementations.
  6. Its styles are feature-scoped.
  7. Its contract or behavior is covered by a unit or architecture test.

If the entity is visually generic rather than domain-specific, it belongs in the Design Guideline and requires product-owner approval before implementation.

Laboratory feature template

The code shape of a new laboratory increment is:

core/laboratory/eNNContract.ts
  immutable API/schema adapter

workspaces/laboratory/ENNResult.tsx
  typed values for the versioned summary + admitted evidence + result projection

domain renderer module
  only when an existing renderer cannot express the evidence

LaboratoryArchiveWorkspace.tsx
  registry/selection composition only

styles/laboratory.css
  shared LAB layout; no per-run visual language

The complete engineering narrative remains in Ops. The product component receives a concise typed projection.

components/laboratory/LaboratoryPresentation.tsx owns the executable missioncore.laboratory-report/v1 anatomy. LaboratorySummary requires question, approach, principalResult, and limitation; LaboratoryResultSummary requires metrics plus proved, notProved, and decision. A bounded LAB result module cannot own the canonical summary/result DOM or CSS classes. The product UI test discovers every ENNResult.tsx automatically and rejects such a fork.

components/laboratory/LaboratoryRecordedClipPlayer.tsx exclusively owns the versioned missioncore.laboratory-recorded-clip-viewer/v1 camera transport and clock. Feature renderers may add typed overlays or a synchronized spatial scene through its slots. Source-paced spatial evidence follows exact media sequences through a bounded reusable look-ahead cache; frame-step remains an explicit capability only when the source cannot keep pace. Experiment-named players must not fetch a JPEG per playback frame, instantiate MediaSource, schedule frame timers, or declare a new Rerun receiver/blueprint. Forward buffered progression and backward loop/seek are separate shared-player transitions; a backward target cannot be treated as an ordinary rolling-buffer advance.

The shared player owns the admitted primary camera and companion camera + spatial presentations. A feature mode may not disable playback, unmount the camera, or create a second viewer configuration merely to show 3D/PLAN. LaboratoryEvidenceViewer owns the reusable stacked chrome layout: one header, one visual stage and one transport/timeline rail joined into a seamless surface. LaboratoryRecordedClipPlayer reuses the canonical SplitPane for resizable spatial + companion-camera evidence while keeping the recorded camera owner mounted across CAMERA/3D/PLAN transitions. Feature code supplies typed actions and modes; it does not declare a feature-local splitter. M4.8 reuses one M48EvidenceModeRail projection in the report, candidate-assisted correction, independent review and adjudication surfaces. The rail is the admitted viewport overlay: a canonical GlassSurface containing three default-size IconButton controls and no local control geometry. CAMERA/3D/PLAN therefore do not return to the stacked header or fork per workflow: CAMERA visibility is independent from the nullable 3D/PLAN spatial mode, and their state transition cannot hide both channels. Candidate seed data belongs to the correction contract only; it cannot enter the blind source decoder or upgrade assisted evidence to independent truth.

Legacy/integrated diagnostic viewers may keep a result interaction inside the evidence slot only where that viewer is already the admitted result instrument. This exception does not apply automatically to a new LAB.

ADR 0039 adds a lifecycle distinction to this code shape. A canonical LAB enters through the reusable registry and must not add another experiment-named branch to App.tsx, the generic workspace or the advanced dispatcher. An experimental LAB may have a bounded adapter or renderer when the current graph cannot express its evidence, but the adapter remains feature-local, declares its classification and cannot change generic product behavior. A legacy LAB keeps its existing read-only projector and is not rewritten solely to satisfy the latest code shape.

Local ontology decision

Mission Core already has three semantic mechanisms:

  1. productModel.ts for application navigation and capability declarations;
  2. versioned TypeScript/Pydantic/JSON contracts for executable data;
  3. docs/domain-model/mission-core-experimental-vocabulary-v0alpha2.md for meanings that are not yet stable enough for Platform Ontology.

These mechanisms were sufficient before A3. Observatory modular compositions crossed the gate on 2026-09-04: planning, Worker dispatch, LAB publication and replay presentation require the same module/composition/result/layer relations, and typed single-result traversal lost the TGS member of a composed run. The bounded local package is defined by docs/domain-model/observatory-domain-ontology-v1.md. It keeps identity in the existing stores and has no Platform Ontology runtime dependency.

A formal local runtime ontology is introduced only when:

  • at least two independent runtime consumers require the same relationships;
  • typed contract traversal is demonstrably insufficient;
  • ownership, identity, lifecycle, versioning, and migration are agreed;
  • the graph answers named queries used by the product or automation;
  • promotion or synchronization with NODE.DC Platform Ontology is defined.

Other domains continue to extend the versioned local vocabulary and executable contracts until they independently satisfy these conditions. The admitted Observatory package projects existing store identities and cannot become a second authority for them.

Automated boundary gate

test/applicationArchitecture.test.mjs enforces:

  • no upward imports from core/components into workspaces or App;
  • no visual adapter imports from core;
  • no local vendor icon library or direct Design Guideline source imports;
  • LAB code and CSS remain outside the central workspace buckets.

test/laboratoryProductUi.test.mjs additionally enforces the versioned LAB report fields and shared result component across bounded LAB modules.

The next executable laboratory architecture gate extends these tests to reject new unclassified experiment branches while allowing a declared bounded experimental adapter. The gate protects core composition; it does not forbid a novel research stack.

File length is not an architectural boundary and is not enforced. Refactoring is justified by ownership, cohesion, dependency direction, lifecycle or test isolation, not by a line-count threshold.

From apps/control-station run:

npm run test:unit
npm run typecheck
npm run build

Known bounded debt

  • App.tsx remains a large shell orchestrator. Future shell behavior should preserve its orchestration ownership and extract modules only where they acquire an independent responsibility or lifecycle.
  • Workspaces.tsx still contains several established generic workspaces. New domains should respect the existing dependency direction; extraction is a design decision rather than a response to file length.
  • LaboratoryArchiveWorkspace.tsx is physically isolated. Further LAB work must preserve the feature boundary without imposing a size quota on the implementation.
  • Design Guideline dependencies are mutable local file: links until a portable package/distribution decision is implemented.

None of these debts require a new ontology or a copy of the Design Guideline application.