Transport-neutral workspace application service
Extract parsing, validation, and projection into one seam shared by the CLI, local Studio, a hosted API, and tests.
Status: Accepted
Canonical terms
Workspace · Workspace export · Identity registry · Context package · Federation snapshot
Context
The public playground (issue #68) needs to parse, validate, and project a
visitor's RML without a host filesystem, a CLI process, or an installed .NET
toolchain — a visitor's edits exist only as in-memory, session-scoped text.
Before this decision, that pipeline existed only as internal code inside
Modeller.Cli (WorkspaceLoader, WorkspaceProjection, WorkspaceGeneration),
coupled to ICliHost, CliExitCode, and physical paths. The local Studio app
already worked around the same gap by spawning the CLI as a subprocess per
request, and the website's current example gallery only works because a build
script shells out to the CLI once, ahead of time, for a fixed set of curated
examples — there was no way for a visitor's own input to reach parsing,
validation, or projection without a filesystem underneath it.
Decision
Modeller.Workspace is a new library sitting between Modeller.Parsing,
Modeller.Configuration, and Modeller.Projections and every consumer that
needs a workspace's canonical semantic model: the CLI, local Studio, a future
hosted API, and tests. It depends on none of Modeller.Cli, Modeller.Templates,
Modeller.Output, Modeller.Generation, or Modeller.Rendering — template
selection, output application, and generation execution remain a deliberately
separate, later concern (see Non-goals).
Its vocabulary:
LogicalPath— a validated value type confining a document path to the workspace boundary (never rooted, never escaping via..segments, never blank, never containing a NUL byte). Construction is the only way to obtain one, so aLogicalPathcannot become invalid after the fact.WorkspaceDocument— aLogicalPathpaired with text content. No file path, no host, no I/O.WorkspaceIdentityRegistry— the in-memory shape of a tooling-owned.modeller/identities.json: an ordered identity sequence per document.IdentityStrategy— a closed union ofEphemeral(mint identities viaRmlCompiler.EnsureIdentities, for anonymous/draft workspaces) orDurable(WorkspaceIdentityRegistry)(apply viaRmlCompiler.ApplyIdentities, where a mismatch is a diagnostic, never a silent repair) — an exhaustive pattern match rather than a boolean flag.WorkspaceConfigurationInput— a minimal, validated wrapper producing aModeller.Configuration.ConfigurationRequest; it reuses that library's layered source/profile model rather than reinventing configuration shape.WorkspaceInput— a bounded set ofWorkspaceDocuments, anIdentityStrategy, and aWorkspaceConfigurationInput. Directly constructible in memory — this directness is the in-memory adapter used by tests and the future hosted API; no separate builder or loader class is needed.ModellerWorkspace— the static seam:Analyze(WorkspaceInput, CancellationToken)resolves configuration, applies identity per strategy, and callsDefinitionParser.Parse(which already validates internally), returning anAnalyzedWorkspace(the parsed package, resolved configuration, source provenance, the identity strategy used, and the post-identity-application document text needed byExport).Project(AnalyzedWorkspace, ViewDefinition, LayoutState?, CancellationToken)validates the requested view kind againstModellerWorkspace.SupportedViewKinds— theLifecycle/RuleDecisionallowlist promoted from the CLI's own workaround forDiagramProjectorsilently returning an empty graph for its four unimplemented view kinds (issue #64) — then callsDiagramProjector.Project.Export(AnalyzedWorkspace)harvests each document's currently-effective ordered identities into a freshWorkspaceIdentityRegistry, via a newRmlCompiler.HarvestIdentitiesprimitive (the inverse ofApplyIdentities).
WorkspaceOutcome<T>— a closedSuccess/Failed/Cancelledunion. Deliberately not a(ErrorCode? Error, T Value)pair: that shape allows constructing a result that is simultaneously an error and a value, and folds cancellation into "failed" instead of making it a distinct case.
Identity invariants preserved
Readable authoring language and deterministic editing
requires that identity registry handling: materialize IDs only in memory during
loading; never let renaming or moving lose identity; never let deleting and
recreating a concept reclaim identity; and never silently repair a mismatch.
Analyze and Export satisfy each of these:
Analyzenever writes a registry itself — only its caller decides whether and where to persist aWorkspaceIdentityRegistryExportreturns.Exportonly harvests identities already present in the analyzed documents (freshly minted viaEnsureIdentities, or carried through unchanged viaApplyIdentities) — it never infers identity by matching names against a prior registry, so deleting and recreating a declaration never reclaims an old identity.- A
Durablestrategy whose registry does not cover a document, or whose identity count does not match, is a diagnostic (workspace.identity-registry.document-missing,workspace.identity-registry.out-of-sync) — the same codes and messages the CLI reported before this change — never a silent repair.
Adapters
CLI: WorkspaceLoader still owns reading .modeller/config.json, the
identity-registry file, and each declared source via ICliHost, and still
performs its existing path-safety, existence, and identity-coverage checks
in the same order as before — none of that changed, so the CLI's ReadCount
and short-circuit behavior are byte-for-byte identical to before this change.
What changed is the last step: instead of manually calling RmlCompiler.ApplyIdentities
per file and DefinitionParser.Parse directly, it builds a WorkspaceInput
from the already-validated raw documents and registry and calls
ModellerWorkspace.Analyze, translating WorkspaceOutcome<AnalyzedWorkspace>
back to the CLI's existing CliExitCode/diagnostic-text shapes. WorkspaceProjection
now calls ModellerWorkspace.Project and shares ModellerWorkspace.SupportedViewKinds
instead of declaring its own allowlist. WorkspaceGeneration (template pack
loading, output application) is unchanged — see Non-goals.
In-memory: no separate adapter type exists — a WorkspaceInput built
directly from in-memory documents, an IdentityStrategy, and a
WorkspaceConfigurationInput is itself the in-memory adapter, used by
Modeller.Workspace.Tests and intended for the future hosted API and the
public playground.
Consequences
- Two of the four near-duplicate path-confinement checks that existed before
this change (
CliApplication.IsWorkspaceRelative,WorkspaceLoader.Unsafe) now delegate toLogicalPath. The other two (Modeller.Contexts' inline check,Modeller.Parsing'sIsPackageRelative) remain as-is: they guard existing publicstring-based APIs in librariesModeller.Workspaceitself depends on, so folding them intoLogicalPathwould invert the dependency graph. This is intentional layered defense-in-depth, not an oversight. RmlCompiler.HarvestIdentitiesis a new, small, independently testable primitive — the read-back inverse ofApplyIdentities.- Template pack loading, digest verification, and output application remain
entirely within
Modeller.Cli/Modeller.Templates/Modeller.Outputfor now.
Follow-up
Issue #71 hosts this seam behind a stateless HTTP API — see Hosted workspace API for the playground.
Non-goals
- Wiring template selection or generation execution through
Modeller.Workspace.Modeller.Templates'PackSourceandModeller.Output'sIOutputFileSystemare already filesystem-neutral seams of their own; folding them in now would blur the "parse, validate, project — no filesystem" boundary this issue asks for. A futureWorkspaceGenerationRequestcomposing aWorkspaceInputwith aPackSourceis a natural extension once #64 (the remaining view kinds) and generation's own neutrality are both settled. - Adapting
Modeller.LanguageServer/local Studio's LSP bridge onto this seam. Studio's live-editing path goes through a separate LSP process today; whether it should also front ontoModellerWorkspaceis an open question for a follow-up, not resolved here.
Readable authoring language and deterministic editing
Keep a business-readable DSL as the portable source while text, structured, and diagram editors share one semantic model.
Hosted workspace API for the playground
Expose ModellerWorkspace behind a stateless, bounded HTTP API rather than a CLI subprocess or a per-user language-server process.