Expand description
§persona-wire-core
Transport-agnostic core for the persona-wire graph engine. The crate’s
value proposition is ProjectionAsPrompt: turn an arbitrary
Specification over a small
property graph into a rendered string (Prompt / Markdown / JSON / ASCII)
by binding it to a registered template, then concatenate one or more such
renderings into a wake-time prompt context.
No MCP or CLI dependencies — persona-wire-mcp and the unified
persona-wire binary both depend on this crate and adapt their own
transport surfaces on top of the use cases exported here.
§Layer split (DDD + Hexagonal)
-
domain— Entities, Value Objects, and business rules. Pure code with no I/O.domain::graph—Node/Edge/Severity. The persistent graph entities.Node.metadatais a free-formserde_json::Value, which is what every higher layer queries against.domain::specification— composable predicate (TypeIs/MetadataEq/Reachable/And/Or/Not). The canonical Specification pattern applied to the graph: each variant is a tiny domain object; combinators (and/or/not, plusstd::ops::Not) build composite predicates at runtime.Specification::is_satisfied_byevaluates the predicate against oneNode.domain::error—WireError/WireResultshared across the crate.domain::autoversion— versioning of registered entities.domain::repository— the repository trait surface that the infrastructure layer implements.
-
application— Use cases and registries. Coordinates the domain and infrastructure layers; this is the API surface that transport adapters target.application::spec_registry::SpecRegistry— persistent registry of namedSpecificationvalues.register/get/list, JSON-serialised in thespecificationstable.application::projection_registry::NamedProjection/application::projection_registry::ProjectionRegistry— CQRS Read Model: aNamedProjectionis a(name, spec_ref, template, target_form)tuple.spec_refpoints at an entry inSpecRegistry;templateis a handlebars body;target_formis one ofPrompt/Markdown/Json/Ascii. The registry persists projections in theprojectionstable — there is no hard-coded projection list anywhere in the crate, every projection is data.application::merger::MergeStrategy— combine an overlay template into a base template (Replace/Append/Prepend/Section(name)).Sectionsubstitutes{{!-- <name> --}}markers and falls back toAppendwhen the marker is absent.- [
application::persona_pack_resolver] — read template overlays from~/persona-pack/<id>/prompt.toml(or$PERSONA_PACK_ROOT) under[extra.persona_wire.projections.<axis>]. The resolver returns only overlays (template / target_form / strategy); the source-of-truth for wiring entries stays in the graph. application::use_cases— the high-level operations (wire_init/wire_close/wire_doctor/wire_query/wire_render/wire_prompt_context/ batch creators / deleters).
-
infrastructure— Adapters bound to a concrete backend.infrastructure::storage::SqliteStorage— SQLite implementation of the repository surface (nodes/edges/specifications/projectionstables and atype_registryfor the open vocabulary).infrastructure::rendering— handlebars template engine over the query-result context. Behaves like a Mustache superset ({{var}},{{#each list}}…{{/each}},{{#if cond}}…{{/if}}, dotted paths) and emits a visible{{render-error: …}}prefix on parse failure instead of panicking or silently fallback-ing.infrastructure::adapter— Layer 6 SoT Adapter. Each axis wiring entry carries ametadata.source_uri; thePluginRegistrydispatches by scheme prefix to anArc<dyn Adapter>:file://<path>/file:<path>→FileAdapter(std::fswith~/expansion; for a directory it picks the newest mtime child).mini-app://<table>→MiniAppAdapter(external cratepersona-wire-adapter-mini-app; consumer wires it on top ofPluginRegistry::default_builder_for_wire).
§Two query axes
Wire exposes two complementary axes; both are first-class:
- Dynamic axis — caller supplies an inline
Specificationand gets the matching nodes back viawire_query. Good for ad-hoc inspection, scripts, and one-off filters. - Fixed axis — caller registers a
(spec, template, target_form)as aNamedProjectionand refers to it byspec_ref/projection_ref. Good for stable surfaces such as wake-time injection.
§Render flow (wire_render)
ProjectionRegistry.get(name)
→ NamedProjection { spec_ref, template, target_form }
│
│ spec_ref
▼
SpecRegistry.get(spec_ref)
→ Specification (TypeIs / MetadataEq / And / Or / Not / Reachable)
│
│ Specification::is_satisfied_by
▼
collect_matching_nodes(storage, spec) → Vec<Node>
│
│ context build: { count, nodes, entries, … }
▼
rendering::render(target_form, template, context)
→ String (Prompt / Markdown / JSON / ASCII)§PromptContext flow (wire_prompt_context)
Persona-scoped one-shot entry intended for wake-time auto-load:
- Read the optional
[extra.persona_wire.projections.<axis>]overlays for the persona (best-effort; missing persona-pack is silently tolerated). - Discover the persona’s axes by querying the graph with a
Specification(TypeIs("outline_node")ANDMetadataEq("persona", <persona_id>)). The axis list is therefore data, not code — adding an axis is a graph insert. - For each axis, look up the base
NamedProjectionby the conventional name<persona_id>.section.<axis>. If an overlay is present, runMergeStrategy::merge(base, overlay). Fetch the axis payload through the Layer 6 Adapter via the wiring entry’ssource_uri, then render the block. - Concatenate the rendered blocks into a single
prompt_contextstring.projection_names: Some([...])restricts the walk to an explicit subset;Nonewalks every registered axis for the persona.
No template content is hard-coded inside this crate. The set of axes, the base templates, and the optional overlays are all data managed through the regular registry / persona-pack surfaces.
§Persistence schema (SQLite, set up by
type_registry(name TEXT PK, kind TEXT, schema_json TEXT, severity_allowed TEXT)nodes(id TEXT PK, type TEXT FK→type_registry.name, sot_ref TEXT?, confidence REAL?, …, metadata TEXT)edges(id TEXT PK, src_node TEXT FK→nodes.id, tgt_node TEXT FK→nodes.id, kind TEXT FK→type_registry.name, severity TEXT?, metadata TEXT, …)specifications(name TEXT PK, expr_json TEXT, created_at INTEGER)projections(name TEXT PK, spec_ref TEXT, template TEXT, target_form TEXT, created_at INTEGER)versions(…)— autoversion ledger.workflow_runs(…)— reserved for the workflow engine layer.
The graph vocabulary is open but type-checked: any Node or Edge
must reference a row in type_registry. The default seed is loaded by
SqliteStorage::seed_default_types.
§Design rationale
The sections below collect the architecture-level decisions that were
previously drafted in docs/design/*.md while the layout was being
shaped. The design docs are now retired — this is the SoT.
§Three-layer split (Math backend / Domain Entity / thin Application)
- Math backend Graph (
domain::graph) — open-vocabulary primitives (Node / Edge / Severity / Specification / CRUD / Compute / Constraint / AutoVersion / Repository). Tenant-agnostic and persona-agnostic; used as a backend SDK. It does not know about personas, slots, sources, or projections. - Domain Entity Layer (
domain::entity) — persona-wire’s first-class vocabulary (PersonaId/Slot/Source/Wiring/Workflow/Projection). Owns invariants and behavior; uses the Math backend as a persistence SDK. Aggregate composition is documented indomain::entitymodule docs. - Application Layer (
application) — thin orchestrator that lifts Domain Entity operations onto the use-case surface that transport adapters (MCP / CLI / future RPC) target. Knowledge gravitates to layer 2; this layer stays slim.
§Slot vocabulary (was axis)
The persona-context binding identifier is now named Slot. Earlier code
used the AI-jargon word axis, which collided with three legitimately
orthogonal uses of “axis” elsewhere in the crate:
- Graph compute primitive — Traversal / Execution / Constraint axes (3 orthogonal operation kinds).
- Doctor diagnostic surface —
Axis::{Graph, Workflow}(2 health axes). - Plugin registry — Adapter / Engine / Projection plugin axes (3 orthogonal slots).
In contrast, persona context values like mailbox / mail / news are
not orthogonal axes — they are slot names. Decisive evidence (recorded
when the rename was decided) included the storage projection name
template "{persona_id}.section.{axis}" (“section の中の axis 値”
structure = axis is a name, not a classifier) and the error message
"must contain at least one axis name" (the word “name” gives the
game away).
The rename landed in two cuts:
- Domain Entity layer carries
domain::entity::Slotas a first-class Value Object ondomain::entity::wiring::Wiring. - The storage metadata key is still literally
"axis"(legacy SQLite rows).application::wiring_mapperis the single translation boundary:Slot ↔ Node.metadata["axis"]. New code routes through the mapper; readingmetadata["axis"]directly is disallowed.
§PoEAA Registry vs DDD Repository (Projection)
Projection persistence goes through
application::projection_registry::ProjectionRegistry, a PoEAA
Registry (Fowler PoEAA Ch.18) — an application-layer service that
provides named access to well-known objects as a structured alternative
to global access. A DDD Repository (Evans DDD Ch.6 / Vernon IDDD Ch.12)
was considered and not adopted: it would move persistence vocabulary
into a domain port and collapse the application service into a pass-through.
application::wiring_mapper and application::workflow_mapper are
sibling Data Mappers invoked directly from use cases against the Math
backend Node repository — Wiring and Workflow do not get their own
Registry because they are persisted as graph nodes, not as a separately
named table.
§Data Mapper — narrow reading
Fowler PoEAA’s Data Mapper (Ch.10) requires some mapper that translates
between persistence shape and Domain shape. The literal pattern uses an
independent Mapper class; persona-wire takes the narrow reading and
lets the Registry (Projection case) or the use case (Wiring / Workflow
cases) own the mapper bridge through the
application::projection_mapper / application::wiring_mapper /
application::workflow_mapper modules. Promoting these to a literal
Fowler Mapper trait is a carry that fires only when a second parallel
mapper with the same shape arrives. Until then, the free functions
intentionally do not sit behind a trait.
§DDD / BP citation table
Best-practice citations carry an honest literal / narrow / 独自整理 tag so future readers can tell which parts of the BP are quoted verbatim and which are persona-wire’s own extension.
| BP | Where applied | Tag |
|---|---|---|
| Fowler PoEAA Ch.10 — Data Mapper | Projection / Wiring / Workflow mappers | narrow — persistence-vs-domain separation literal; the independent Mapper class is replaced by Registry / use-case owners |
| Fowler PoEAA Ch.18 — Registry | ProjectionRegistry named lookup | literal |
| Evans DDD Ch.9 — Specification | domain::graph::specification — VO + is_satisfied_by + and/or/not algebra | literal |
| Vernon IDDD Ch.10 Rule 2 — Small Aggregates | Single-transaction consistency boundary, not field count | narrow — boundary axis only; field-count framing was explicitly excluded |
| Vernon IDDD Ch.10 Rule 3 — Reference by Identity | Projection references Specification via SpecName instead of embedding | literal |
| Fowler bliki 2003 — Anemic Domain Model anti-pattern | Domain Entity owns invariants; DTOs (NamedProjection) are intentionally anemic at the persistence boundary | narrow — anti-pattern scope limited to the domain layer; the DTO exclusion is an interpretive extension |
| CQRS Read Model | Projection’s (name, source-query, transform, output) 4-concern decomposition | 独自整理 — the 4-tuple shape itself is persona-wire’s framing, not literal in Greg Young’s original |
| Yaron Minsky — Make Illegal States Unrepresentable (Effective ML Revisited) | PluginDispatch enum collapses (engine, kind, config) Option triple to a Default / Custom { .. } discriminated union | narrow — the slogan and discriminated-union pattern are literal; the 2^N arithmetic phrasing is persona-wire’s gloss |
Re-exports§
pub use application::plugin_registry::AdapterInfo;pub use domain::error::WireError;pub use domain::error::WireResult;pub use infrastructure::filter::FilterCap;pub use infrastructure::filter::TailSpec;pub use infrastructure::filter::WireFilters;
Modules§
- application
- Application layer — use cases and registries.
- domain
- Domain layer — pure entities, value objects, and business rules.
- infrastructure
- Infrastructure layer — drives Storage and Rendering as adapters.