@workspec/c4-model
Pure loader/resolver for a WorkSpec C4 working tree. Discovers .workspec/ elements, diagrams, and
.layout/ files through a small file-source port, parses and validates every one via
@workspec/c4-schema, resolves every diagram's node/edge references against the
tree — the __system__ alias, preferred-type disambiguation, and the c4-context safety net mirror
WorkSpec Enterprise's get-diagram.ts, while c4-container lens partitioning is a deliberate
enhancement beyond Enterprise's single combined preference list (see
src/resolution/preferred-type.ts) — and returns one typed C4Model, alongside a best-effort
diagnostics array. Never throws: every failure mode degrades to a diagnostic, and the model is
always data-complete.
No DOM, no React, no IO assumptions — the root entry only knows about MemorySource. Node-only
code (FsSource, node:fs/promises) lives behind the ./fs subpath export, so importing
@workspec/c4-model itself never pulls in a node: module.
Usage
import { loadC4Model, createMemorySource } from '@workspec/c4-model';
import { createFsSource } from '@workspec/c4-model/fs';
// Server-side / CLI: read a real working tree from disk.
const model = await loadC4Model(createFsSource('/path/to/repo'));
// Browser demo: no filesystem at all.
const source = createMemorySource({
'.workspec/system/main.yaml': 'title: My System\ndescription: ...\n',
});
const memoryModel = await loadC4Model(source);
for (const diagnostic of model.diagnostics) {
console.warn(`${diagnostic.severity} ${diagnostic.code}: ${diagnostic.message} (${diagnostic.file})`);
}
The C4FileSource port
interface C4FileSource {
listFiles(dirPath: string): Promise<readonly string[]>; // non-recursive, repo-relative, [] if missing
readFile(path: string): Promise<string>;
writeFile(path: string, content: string): Promise<void>; // unused by the read-only loader today
exists(path: string): Promise<boolean>;
}
Every path is POSIX-relative to the repo root that contains .workspec/ (e.g.
.workspec/actors/architect.yaml). Two implementations: createMemorySource (Map-backed,
seedable, the root entry) and createFsSource (node:fs/promises-backed, the ./fs subpath).
Diagram resolution semantics
__system__resolves to the tree'ssystem/*.yamlsingleton slug. A diagram that uses the alias with no system file anywhere raisesno-system(error).- Bare (untyped) node slugs disambiguate via a kind-preference order per diagram type
(
PREFERRED_TYPE_BY_DIAGRAM, mirroring Enterprise'sget-diagram.tsforc4-contextandc4-component), falling back toC4_REF_KINDS' own declaration order for anything unlisted — a deterministic replacement for Enterprise's DB-row-insertion-order tie-break, which has no equivalent in a file tree.c4-containeris lens-partitioned: the logical lens prefersdomainfirst, the deployment lens preferscontainerfirst — the model exposes both resolutions (lensViews.logical/lensViews.deployment) rather than picking one. - Typed refs (
{component: diagram-editor}) resolve directly by kind + slug; a kind/slug pair that doesn't exist raisesdangling-ref. - The system node is materialized (
injected: true, id = the real system slug) whenever it's needed but not authored: everyc4-contextdiagram that never references the system (the safety net Enterprise'sget-diagram.tsalso applies), and — for ANY diagram type — a diagram whose edges reference__system__without a node entry for it, so edge endpoints always resolve to a node actually present in the view (seeinject-system-node.ts). - Edge endpoints must resolve to a node present in the diagram (
__system__resolves to the view'skind: 'system'node, which injection guarantees whenever the tree has a system). Unresolved endpoints raisedangling-edge-ref. An edgecategoryabsent from both the built-in defaults andspec.yaml'sconnectionsmap raisesunknown-category— the category itself is never rejected. .layout/<slug>.yamlfiles join their diagram by slug. A pinned node or edge-routing-hint key that matches nothing the diagram's own YAML named (as a node ref or an edge endpoint — seeauthored-diagram-refs.ts) raisesorphan-layout-node/orphan-layout-edge-hint; a layout file whose slug names no diagram at all raisesorphan-layout-file.~/-rootedlinksentries that don't resolve to a file in the tree raisedangling-link;@workspace/-rooted entries are never diagnosable standalone. A cycle among elements'~/links to one another raiseslink-cycle.
Diagnostics
DIAGNOSTIC_CODES (exported) is the full, documented catalogue — see
src/model/diagnostic-codes.ts. Every diagnostic carries file and (derived from it) slug.
line/col are present for parse-error and the five location-tied codes (dangling-ref,
dangling-edge-ref, duplicate-slug, orphan-layout-node, orphan-layout-edge-hint — located
via @workspec/c4-schema's locateYamlPath); the remaining codes are file-only by design (see the
carries-line table in that file's doc comment for the per-code rationale). Reference-shaped codes
(dangling-ref, duplicate-slug) additionally carry refSlug: the slug the offending reference
points at, distinct from slug which names the file carrying the reference.
Scripts
| Script | Does |
|---|---|
pnpm --filter @workspec/c4-model run build |
tsc --emitDeclarationOnly + tsup → dist/ |
pnpm --filter @workspec/c4-model run typecheck |
tsc -b |
pnpm --filter @workspec/c4-model run test |
vitest (unit, conformance, edge cases) |
pnpm --filter @workspec/c4-model run lint |
eslint |
Out of scope (see other slices)
- The ELK-based auto-layout engine that fills in unpinned
.layout/positions — S4. - React canvas components — S5.