v2 registry, prompt-guided authoring and validation UX¶
Status: design, 2026-10-11. Companion to the language design; background in the research notes; sequencing in the plan.
The framing: the formal DSL is a verifiable target, not the single document an end user constructs by hand.
End-user authoring uses (1) a declared registry of components (processes, steps, adapters, emitters) whose
interfaces are published as data, so authoring needs no Python import or introspection, and (2) prompts (LLM-guided
tooling such as the viva-superpowers Claude Code skills) that guide construction, documentation and validation. The DSL is
where the constructed workflow is type-checked, contract-audited, explained and documented, and from which the runnable
document or *.composite.yaml is generated.
0. Shape¶
package repo CI (package's own env) anywhere pblang is installed execution env
pblang-describe (stage A: introspect) -> pblang registry build (stage B) pblang preflight
manifest.raw.json + interfaces.override.pblang (installed vs lock vs
-> pblang/manifest.json fingerprint; env hints)
-> pblang/interfaces.pblang
viva-catalog nightly: fetch manifest.json per module -> components.json
LLM skill or human: import -> draft .pblang -> pblang check / doc / compile -> preflight -> run
Three principles:
- The manifest JSON is canonical;
interfaces.pblangis rendered from it deterministically. Tools read JSON, humans andimportread.pblang, CI verifies they agree. - Stage A has zero pblang dependencies, so it runs inside any package environment (a pixi env, the workbench
env_worker, agit:per-SHA venv). - One diagnostics JSON format for the CLI, the language server, the workbench and the skills.
1. Component registry¶
1.1 Files shipped by a package¶
<pkg>/pblang/manifest.json generated, canonical (stage B output); committed and in the wheel
<pkg>/pblang/interfaces.pblang rendered from manifest.json; what `import` reads
<pkg>/pblang/interfaces.override.pblang hand-written; merged by component name; committed
<pkg>/pblang/manifest.raw.json stage A output; CI artifact only
They are located without importing the package, through importlib.metadata.distribution(name).files. That is the
difference from bigraph_schema/package/discover.py and the workbench's env_worker.py, which both import.
1.2 Manifest (design target; what ships is section 1.7)¶
The fields below are the target for later versions of the shared format that section 1.7 describes (shipping: pbg-interfaces/1, with its JSON Schema in docs/schemas/).
{
"schema_version": "pbg-interfaces/2",
"package": {
"name": "viva-pde-particle",
"module": "viva_pde_particle",
"version": "0.4.2",
"python": ">=3.11",
"source": "https://github.com/vivarium-collective/viva-pde-particle",
"ref": "v0.4.2",
"commit": "e5325c1"
},
"generated": {
"tool": "pblang-describe 0.1.0",
"at": "2026-10-11T00:00:00Z",
"process_bigraph": "1.8.5",
"bigraph_schema": "1.7.0",
"from_override": ["Stepper", "SplittingCoordinator"]
},
"environment": {
"install": ["pip install viva-pde-particle==0.4.2"],
"native": ["smoldyn (patched fork)", "dolfinx", "gmsh"],
"image": "ghcr.io/vivarium-collective/viva-pde-particle:0.4.2",
"platforms": ["linux-x86_64", "macos-arm64"],
"rest": { "port": 8000, "entry": "viva_pde_particle.server:app" },
"git_entry": "viva_pde_particle.pblang_entry:make_process"
},
"types": [
{ "name": "positive_float", "schema": { "_type": "float" }, "opaque": true }
],
"components": []
}
A component entry is a superset of the workbench's RegistryProcess so the workbench can adopt it unchanged:
{
"name": "GridCountsToMeshConcentration",
"address": "local:!viva_pde_particle.steps.transfer.GridCountsToMeshConcentration",
"aliases": ["GridCountsToMeshConcentration"],
"kind": "adapter",
"base": "step",
"config_schema": { "grid": "map", "mesh": "map", "species": "list[string]" },
"config_docs": {
"grid": "CartesianGrid spec (origin, size, shape)",
"mesh": "mesh spec"
},
"ports": {
"inputs": { "particle_counts": "map[array[float]]" },
"outputs": { "particle_conc": "map[overwrite[array[float]]]" }
},
"port_semantics": {
"outputs": { "particle_conc": "overwrite" },
"inputs": { "particle_counts": "read" }
},
"dynamic_ports": { "dynamic": false },
"contract": {},
"interval": { "configurable": false },
"docs": {
"summary": "Grid particle counts to P1 mesh concentration (P^T).",
"source": "steps/transfer.py:140"
},
"examples": [
"viva_pde_particle/composites/hybrid.py#build_mesh_hybrid_document"
],
"adapter": {},
"fingerprint": "sha256:…"
}
kind is one of process | step | adapter | emitter | visualization | draft | composite_generator | composite | type.
composite_generator entries carry parameters, emitters, builder and tags; composite entries copy the
package's *.composite.yaml fields. fingerprint is the sha256 of the canonical JSON of {address, config_schema,
ports} and is what preflight compares.
dynamic_ports has three shapes: {"dynamic": false}; {"dynamic": true, "depends_on": ["emit_positions"], "how":
"defaults", "note": "…"} (ports listed are those obtained with default config; the checker hints when those keys are
set); {"dynamic": true, "rule": "union_of_children", "children_key": "process"} (Stepper) or "adapters" plus
"pde", "particles" (SplittingCoordinator): ports are computed from the DSL edge literals once the children's
manifests are imported.
A first generated example lives in the repository: tests/fixtures/data/v2/interfaces/ holds the interface files and
viva_pde_particle.manifest.json that python -m process_bigraph_lang.v2 describe produced from viva-pde-particle's
classes in that package's own environment. They show the honest output for ports the generator cannot see
(inputs { ... } plus a // TODO: dynamic ports note on Stepper, SmoldynHybrid and SpatialRecorder), and
process_bigraph_lang/v2/check.py uses them to verify the hybrid composite without importing process code.
1.3 Stage A: pblang-describe¶
A tiny distribution (packages/pblang-describe/; deps: process-bigraph and bigraph-schema only). CLI:
python -m pblang_describe --package viva_pde_particle [--core viva_pde_particle.core:build_core] [--build-generators]
-o manifest.raw.json. Per package:
- Enumerate
Edgesubclasses of the package's own modules (thefind_edgeslogic frombigraph_schema/package/discover.py, filtered by module prefix), neverallocate_core()over all distributions. Use the package'sbuild_core()if given, else a plain core plus itsregister_typeshook. - Copy
config_schemaas authored and also runcore.accesson it, recordingconfig_schema_errors[](this is whereintandanyare caught at publish time). - Ports by a ladder, recording which rung succeeded in
dynamic_ports.how:DraftProcessattributes →cls.inputs(cls)with the class asself→ instantiate withcore.default(config_schema)and callinputs()/outputs()→ override file →nullwith the exception text. Rungs that disagree markdynamic: true;depends_oncomes from an AST scan ofinputs/outputsforself.config[...]keys (the techniqueprocess_bigraph/contract_drift.pyalready uses). port_semantics:overwriteif the type containsoverwrite[,setif the resolved schema replaces, elseaccumulate;readfor inputs. An optional class attributeport_semantics = {...}overrides.contract:resolve_contract(...)serialized, with theaudit_contract/completenessgrade.kind: emitter (plusEmitterContractfields when viva-emitters is present), visualization, adapter (class attributeadapter), draft, else by MRO.- Composite generators and
*.composite.yaml, restricted to the package. - Never abort on one class: per-component
errors[], overallstatus: ok|partial.
1.4 Stage B: pblang registry build and verify¶
pblang registry build <pkg-dir> reads manifest.raw.json, parses interfaces.override.pblang with the normal grammar,
merges by name (override wins per field: ports, docs, adapter, dynamic_ports, contract rows), fills git ref and commit,
computes fingerprints, writes manifest.json and renders interfaces.pblang with a header naming the manifest hash.
pblang registry verify rebuilds in a temporary directory and diffs; it is the CI gate.
1.5 CI and the catalog¶
- Per repo: a reusable
publish-interfaces.ymlpublished from viva-catalog and scaffolded by pbg-template. On push to main and on tag: install the package in its own env, run stage A,pipx run process-bigraph-lang registry build, verify; bot-commitpblang/*on main if stale; attach the manifest and interface file as release assets. The wheel includespblang/*as package data. - viva-catalog nightly: for each
modules.jsonentry fetch<source>/<ref>/<package>/pblang/manifest.json; aggregate intoecosystem-interfaces.jsonand a flatcomponents.jsonfor search. Packages without a manifest keep their AST-scanned entry marked"interface": "unpublished". This replaces the closed PR's idea of AST-extracted ports.
1.6 Versioning and staleness¶
import pkg;with an optional@range. Resolution order: explicit paths frompblang.toml→ the installed distribution's manifest (viaimportlib.metadata, no import) → the catalog cache (pblang registry fetch).- Lock file
<workflow>.pblang.lock.json(pblang-lock/1): package version, ref, commit, manifest sha, and component fingerprints.pblang lockwrites it;pblang checkwarns on drift. pblang preflightin the execution env compares the installed version to the lock, then runs stage A for only the referenced classes and compares fingerprints, reporting a port-level diff when an interface is stale.
1.7 What ships today: the shared interface manifest (pbg-interfaces/1), one per environment¶
Decided 2026-10-12 (see the stage two plan). Sections 1.1 to 1.6 remain the target; what exists is smaller, and two decisions shape it.
A manifest describes an environment, not a package. describe must import the classes inside the environment
that will run the document, and environments differ (compose-api's shared image carries process-bigraph 1.0.5,
viva-pde-particle's image 1.8.x and its own core).
The format is shared, not this DSL's. It is named for the process-bigraph ecosystem (pbg-interfaces), carries no
.pblang syntax, and has a published JSON Schema:
pbg-interfaces-1.schema.json
(https://biosimulations.github.io/process-bigraph-lang/schemas/pbg-interfaces-1.schema.json, which each manifest
names in $schema). Types stay in bigraph-schema's own string or dict forms and addresses in process-bigraph's
local:!module.Class form, exactly as the classes declare them, and the component keys follow the workbench's registry
(name, address, kind, description, config_schema, inputs, outputs). So any tool that reads a bigraph-schema
type can use a manifest as it is: a core's access, the workbench, compose-api, a prompt-driven skill, another
checker. Readers ignore keys they do not know, so a producer may add more.
- Producer.
process_bigraph_lang/v2/describe_env.pyis stage A in its smallest form: one file, standard library only, importing nothing from this package, so it runs as a plain script in any image or env:python describe_env.py viva_pde_particle --env-name viva-pde-particle --image <ref> > manifest.json. It imports every module under the given roots (a failed import is recorded, never fatal), keeps the step and process classes each module defines, and shares its per-class code withdescribe_module. compose-api runs it in CI inside each environment it offers and serves the result atGET /registry/environments/{name}/manifest. - Consumers here.
Pblang.addManifest/setManifests(main thread) and the language server'spblang/setManifestsnotification (SET_MANIFESTS_NOTIFICATION; a host editor sends it when the environment is known or changes, and every open document is re-checked); the CLI's--interfaces. A component with a known address replaces the earlier one. The earlier, pblang-onlypblang-manifest/0(config: {key: {type, default}},doc) is still read; anything else is rejected.
| Key | Content |
|---|---|
$schema, schema_version |
the schema's URL; "pbg-interfaces/1" |
environment |
{name, image, python, process_bigraph, bigraph_schema, packages {dist: version}, roots, generated_by} (from describe_env.py) |
components[] |
one per step or process class, sorted by address |
.name, .kind, .address, .module |
class name; process or step (by MRO); local:!module.Class; the defining module |
.description |
the first paragraph of the class docstring, when there is one |
.config_schema |
the class's config_schema as authored, key by key (null for a value that is not a type expression); a default is in the type |
.inputs, .outputs |
{port: type} from inputs() / outputs() called with the class as self; null when they need an instance |
.dynamic_ports |
true when the ports need an instance (config-dependent or children's) |
errors[] |
{module, error} per module or class that failed (from describe_env.py) |
Not yet in version 1, compared with section 1.2: units and contracts, port semantics, adapter metadata, fingerprints. Section 1.2 is the design target for later versions of this same format.
2. Adapters as first-class entries¶
An adapter is a step whose purpose is to map one face to another (grid particle counts → mesh concentration field).
Metadata, declared as a class attribute adapter = {…} or in the override file:
{
"from": {
"port": "particle_counts",
"layout": "grid",
"quantity": "count",
"units": "molecule"
},
"to": {
"port": "particle_conc",
"layout": "mesh_p1",
"quantity": "concentration",
"units": "uM"
},
"shared_config": ["grid", "mesh", "species"],
"method": "transpose interpolation P^T",
"exact": false,
"conserves": ["mass"],
"cost": "O(n_grid)"
}
Non-adapter ports carry _layout and _quantity rows in the contract's documentary port dicts (free-form per
ProcessContract, ignored by existing code). A controlled vocabulary lives in docs/vocab/faces.json (layouts
scalar, grid, mesh_p1, mesh_dg0, positions, per_species_map; quantities count, concentration, amount, rate, flux,
field, positions, time; x- extensions). viva-pde-particle's DESIGN.md §13 component contract maps directly onto it.
pblang suggest-adapter scores adapters by layout and quantity match (required), unit convertibility (pint), and
core.resolve on both sides, and returns ranked candidates with the shared config to fill and an insertable DSL snippet.
A check (W230) fires when an adapter's shared config literal differs from the producer's.
3. Prompt-guided authoring loop¶
3.1 Tool surface¶
Every command takes --json; exit 0 clean, 1 diagnostics with errors, 2 usage or internal error.
| Command | Output |
|---|---|
pblang registry search <q> [--kind] [--tag] [--input-type] [--output-type] [--face face.json] |
ranked components; face mode is find_candidates semantics over manifests, no import |
pblang registry show <ref> |
full entry, rendered declaration, a minimal instantiation snippet, dynamic_ports explanation |
pblang registry fetch / build / verify / sources |
lock-style records, verify diff |
pblang check <file> [--level parse\|scope\|types\|wiring\|ground\|env] |
diagnostics document (§4.5) |
pblang explain <CODE> |
{code, title, why, fix, example_before, example_after, links}; the same text the LSP shows on hover |
pblang suggest-adapter <file> --from a.port --to b.port |
candidates with why, shared_config, snippet and LSP edits |
pblang fmt <file> |
canonical text |
pblang compile <file> --to yaml\|document\|template [--params k=v] [--exec-plan local\|rest\|git] -o out |
the artifact plus a source map out.pblang.map.json (document path → DSL range) |
pblang decompile <x.composite.yaml\|doc.json> -o file.pblang |
best-effort text plus a list of what could not be expressed |
pblang doc <file> --format md\|json\|html |
Markdown narrative plus graph JSON (§3.3) |
pblang preflight <file> [--python <exe>] [--suggest-env] |
§5 |
pblang lock <file> |
lock file |
3.2 Skill flow (/viva-compose, in viva-superpowers)¶
1. Orient: `pblang registry sources`, `pblang registry search --json <intent keywords>`; with --from, `pblang decompile`.
2. Select: for each role (engine, adapter, emitter) `registry show`; prefer kind=adapter for face gaps; never invent ports.
3. Draft: write <slug>.pblang (file-level `///` intent, `param` for anything a study would vary, `emit` for observables).
4. Fix loop (bounded): `pblang check --json`; apply `fix.edits` when present, else `pblang explain CODE`;
wiring warnings -> `pblang suggest-adapter`; groundness -> ask the user or `?(default)`; stop at zero errors.
5. Document: `pblang doc -o <slug>.md`; present the DSL and the doc to the user; wait for approval.
6. Materialize: `pblang lock`; `pblang compile --to yaml -o composites/<slug>.composite.yaml`.
7. Preflight: `pblang preflight --json`; report missing packages with the manifest's install hints; hand off to the run skill.
Hard gates: never hand-edit the generated .composite.yaml; never silence a diagnostic by widening a type.
Sibling skills: /viva-publish-interfaces (runs stage A and B in a package repo and opens the override file for anything
with ports: null or dynamic: true) and /viva-adapt (a thin wrapper on suggest-adapter).
3.3 Documentation generation¶
pblang doc merges three sources: /// doc comments, registry docs and contracts per component, and the wiring graph
from the compiled model. Markdown sections: Overview; Parameters (name, type, default, units, doc); Components (name,
kind, package@version, interval, summary); Stores (type, units, writers → readers with overwrite/accumulate badges);
Wiring narrative per store; Open sites; Diagnostics summary; Provenance (lock entries, pblang version, manifest hashes).
Graph JSON {nodes, stores, edges, groups} feeds a small adaptor for loom and bigraph_viz. The user reviews the
.pblang and the Markdown; the .composite.yaml is a build artifact with a header naming its source and a CI check
that regenerates and diffs it.
4. Validation and error UX¶
4.1 Layers and code families¶
Codes carry a family prefix and a stable slug (W200 wire-type-mismatch).
| Layer | Where | Family | Examples (message → fix hint) |
|---|---|---|---|
| Parse | Langium and CLI | P0xx |
P001 expected '}' |
| Scope and imports | Langium and Python resolver | S0xx |
S010 package 'viva_smoldyn' not found in any registry source → fetch hint; S011 lock pins 0.3.1, resolved 0.4.0; S020 unknown component → did-you-mean; S021 unknown port, noting dynamic_ports.depends_on; S022 unknown config key |
| Types | Python core seeded with manifest types |
T1xx |
T100 unknown type 'int' → "bigraph-schema has 'integer'"; T110 value 3 is not a float; T120 config 'species' expects list[string]; T140 opaque type checked loosely (info) |
| Wiring and contract | Python | W2xx |
W200 wire type mismatch via core.resolve_schemas; W201 unit mismatch; W210 port bound twice; W211 required input unbound; W220 two accumulating writers on store; W221 overwrite writer plus another writer; W223 step publishes a whole array with accumulate semantics → "did you mean overwrite[...]"; W230 adapter shared config differs; W240 emit path matches no store; W241 observable never written |
| Groundness | Python (open_sites, admits_why, registry_dependent_addresses) |
G3xx |
G300 required site unfilled; G301 filler rejected (text from admits_why); G302 non-portable address → local:!… from the manifest |
| Env preflight | Python, execution env | E4xx |
E400 not installed + install lines; E401 version differs from lock; E402 interface stale with a port diff; E403 address unresolvable in core; E404 native dependency import failed |
| Runtime | wrapper around Composite |
R5xx |
process-bigraph exceptions mapped through the source map to a DSL range |
4.2 Additive vs overwrite¶
The registry records port_semantics per port; rendered interfaces show it. The checker builds the store graph
statically (like process_bigraph/composite_audit.py) and emits W220, W221 and W223; pblang explain W223
carries the one-paragraph explanation of bigraph-schema's apply semantics with before and after snippets.
4.3 Reuse of bigraph-schema and process-bigraph¶
Type names through core.access on a validation core (rejecting on exception instead of passing strings through);
wire types through core.resolve_schemas; units and ranges through units_compatible and range_subsumes; sites
through admits_why and required_open_sites; addresses through registry_dependent_addresses and
unresolvable_addresses; contract grades copied into manifests at publish time.
4.4 TypeScript and Python¶
Decided 2026-10-12: the checker is TypeScript, inside the Langium services, because its inputs are data (the
document and header manifests) and a browser must validate without a round trip. Python does what needs an
environment: describe, saved documents with numpy, equivalence through a core, groundness, preflight, running.
A browser-based authoring tool therefore needs a Python-backed API only for those environment-bound operations.
The earlier plan (Langium for syntax only, Python for the type algebra, the language server shelling out to
pblang check on save) is superseded by that decision. Manifests reach the checker as data: through --interfaces in
the CLI, addManifest/setManifests in the API, and pblang/setManifests in a language server (section 1.7).
4.5 Diagnostics JSON (pblang-diagnostics/1)¶
{
"version": "pblang-diagnostics/1",
"file": "/abs/hybrid.pblang",
"ok": false,
"summary": { "error": 1, "warning": 1, "info": 0, "hint": 1 },
"diagnostics": [
{
"code": "W200",
"slug": "wire-type-mismatch",
"severity": "error",
"layer": "wiring",
"message": "wire type mismatch: conv.particle_conc is map[overwrite[array[float]]] but store 'conc' is map[float]",
"range": {
"start": { "line": 41, "character": 6 },
"end": { "line": 41, "character": 32 }
},
"path": "cell/conv/outputs/particle_conc",
"related": [
{ "file": "…", "range": {}, "message": "store 'conc' declared here" }
],
"fix": {
"hint": "change the store type, or insert an adapter",
"edits": [{ "range": {}, "newText": "map[array[float]]" }],
"command": "pblang suggest-adapter hybrid.pblang --from conv.particle_conc --to pde.external_conc"
},
"explain": "pblang explain W200"
}
]
}
Ranges are LSP zero-based; edits are LSP TextEdits so the skill and the editor apply the same fix. JSON Schema at
docs/schemas/diagnostics.schema.json.
5. Execution without one giant environment¶
Interfaces being data removes the package from the authoring loop. For running, the manifest environment field maps
to options:
| Option | Manifest fields | Use |
|---|---|---|
| Local env with everything installed | install |
preflight reports E4xx with install lines; --suggest-env writes a merged requirements or pixi fragment |
rest: process servers |
rest.port, image |
compile --exec-plan rest --endpoint pkg=URL rewrites addresses; one container per package image via compose-api; face conformance at bind |
git: per-SHA venv |
git_entry, source, commit |
needs a make_process(name, config) entry convention; defer |
| Whole-composite image | each package's image |
not realistic; prefer rest: |
First step: the environment field plus pblang preflight. Then rest:, which already exists and matches the
workbench and compose-api direction.
6. Integration points¶
- vivarium-workbench: consume
manifest.jsonfor installed packages (fall back to stage A insideenv_workerfor unpublished workspace packages, so there is one registry model);RegistryProcessgainsmanifest_ref,dynamic_ports,port_semantics,adapter,fingerprint; published/introspected/stale badges; a workflow view hosting the existing Monaco and Langium worker plus a diagnostics panel; parameter forms fromcompile --to yaml. - viva-catalog:
modules.jsongainsmanifest_path; nightlyecosystem-interfaces.jsonandcomponents.json; hosts the reusable workflow. - viva-superpowers:
/viva-compose,/viva-publish-interfaces,/viva-adapt;docs/conventions/interfaces.md;composites.mddocumentsrequires.packagesand the generated-file header. - pbg-template: scaffold
pblang/, the override stub, the workflow and schema references. - Upstream asks (process-bigraph, bigraph-schema):
python -m process_bigraph.describe(adopt stage A),Edge.port_semantics(), anEdge.adaptermarker,Edge.ports_depend_on, JSON-safeProcessContract.to_dict(),find_candidatesover manifests, CompositeSpecrequires.packages,core.access(strict=True), bigraph-schema#188, process-bigraph#213. - This repo:
process_bigraph_lang/registry/(manifest models, resolver, search, render, stage B),process_bigraph_lang/check/(diagnostics model, checks, preflight),process_bigraph_lang/cli.py(pblang),packages/pblang-describe/,docs/schemas/*.json,docs/vocab/faces.json.
7. Roadmap¶
| Phase | Scope | Validation |
|---|---|---|
| 1 | design docs and the round-trip prototype in this repo (see the plan) | corpus round-trips; snapshots reviewed |
| 2 | DSL v2 grammar (Langium), compile/decompile/doc/check CLI, lock, LSP forwarding of Python diagnostics |
round-trip viva-smoldyn and viva-vcell-fvsolver yamls; compiled documents run under Composite |
| 3 | publish real interfaces: viva-pde-particle (overrides for Stepper, SplittingCoordinator, SmoldynHybrid; adapter metadata on the transfer steps), viva-smoldyn, viva-emitters; catalog aggregation and reusable workflow; pbg-template scaffold | the catalog serves ports; registry search --face finds the grid-counts → mesh adapter; verify green in CI |
| 4 | authoring loop and workbench integration | the pde-particle hybrid rebuilt from DSL via the skill with no hand-edited yaml runs in the workbench; error-UX review against the pain-point list |
| 5 | execution without one env: environment, preflight --suggest-env, --exec-plan rest with compose-api |
the hybrid runs with Smoldyn in a separate container; preflight catches a deliberately stale interface |
| 6 | upstreaming and hardening | describe, markers and strict access land upstream; heuristics leave stage A |
Risks: bigraph-schema version skew (the toolchain floor must be ≥ 1.7 for matching and audit_contract;
feature-detect); dynamic ports are a long tail (override files are the escape hatch; check degrades to hints, never
silence); custom Node types with Python apply behaviour cannot be data (validate loosely and say so); TypeScript and
Python validators drifting (a shared fixture corpus run by both suites); generated-file drift (verify gates on both
sides).