persona_wire_core/lib.rs
1//! # persona-wire-core
2//!
3//! Transport-agnostic core for the `persona-wire` graph engine. The crate's
4//! value proposition is **ProjectionAsPrompt**: turn an arbitrary
5//! [`Specification`](domain::specification::Specification) over a small
6//! property graph into a rendered string (Prompt / Markdown / JSON / ASCII)
7//! by binding it to a registered template, then concatenate one or more such
8//! renderings into a wake-time prompt context.
9//!
10//! No MCP or CLI dependencies — `persona-wire-mcp` and the unified
11//! `persona-wire` binary both depend on this crate and adapt their own
12//! transport surfaces on top of the use cases exported here.
13//!
14//! ## Layer split (DDD + Hexagonal)
15//!
16//! - [`domain`] — Entities, Value Objects, and business rules. Pure code with
17//! no I/O.
18//! - [`domain::graph`] — `Node` / `Edge` / `Severity`. The persistent
19//! graph entities. `Node.metadata` is a free-form `serde_json::Value`,
20//! which is what every higher layer queries against.
21//! - [`domain::specification`] — composable predicate
22//! (`TypeIs` / `MetadataEq` / `Reachable` / `And` / `Or` / `Not`). The
23//! canonical [Specification pattern][spec-bp] applied to the graph: each
24//! variant is a tiny domain object; combinators (`and` / `or` / `not`,
25//! plus `std::ops::Not`) build composite predicates at runtime.
26//! [`Specification::is_satisfied_by`](domain::specification::Specification::is_satisfied_by)
27//! evaluates the predicate against one [`Node`](domain::graph::Node).
28//! - [`domain::error`] — `WireError` / `WireResult` shared across the crate.
29//! - [`domain::autoversion`] — versioning of registered entities.
30//! - [`domain::repository`] — the repository trait surface that the
31//! infrastructure layer implements.
32//!
33//! - [`application`] — Use cases and registries. Coordinates the domain and
34//! infrastructure layers; this is the API surface that transport adapters
35//! target.
36//! - [`application::spec_registry::SpecRegistry`] — persistent **registry**
37//! of named [`Specification`](domain::specification::Specification) values.
38//! `register` / `get` / `list`, JSON-serialised in the `specifications`
39//! table.
40//! - [`application::projection_registry::NamedProjection`] /
41//! [`application::projection_registry::ProjectionRegistry`] — **CQRS Read
42//! Model**: a `NamedProjection` is a `(name, spec_ref, template,
43//! target_form)` tuple. `spec_ref` points at an entry in `SpecRegistry`;
44//! `template` is a handlebars body; `target_form` is one of
45//! [`Prompt` / `Markdown` / `Json` / `Ascii`](application::projection_registry::TargetForm).
46//! The registry persists projections in the `projections` table — there
47//! is **no hard-coded projection list anywhere in the crate**, every
48//! projection is data.
49//! - [`application::merger::MergeStrategy`] — combine an overlay template
50//! into a base template (`Replace` / `Append` / `Prepend` /
51//! `Section(name)`). `Section` substitutes `{{!-- <name> --}}` markers
52//! and falls back to `Append` when the marker is absent.
53//! - [`application::persona_pack_resolver`] — read template overlays from
54//! `~/persona-pack/<id>/prompt.toml` (or `$PERSONA_PACK_ROOT`) under
55//! `[extra.persona_wire.projections.<axis>]`. The resolver returns only
56//! overlays (template / target_form / strategy); the source-of-truth for
57//! wiring entries stays in the graph.
58//! - [`application::use_cases`] — the high-level operations
59//! (`wire_init` / `wire_close` / `wire_doctor` / `wire_query` /
60//! `wire_render` / `wire_prompt_context` / batch creators / deleters).
61//!
62//! - [`infrastructure`] — Adapters bound to a concrete backend.
63//! - [`infrastructure::storage::SqliteStorage`] — SQLite implementation of
64//! the repository surface (`nodes` / `edges` / `specifications` /
65//! `projections` tables and a `type_registry` for the open vocabulary).
66//! - [`infrastructure::rendering`] — handlebars template engine over the
67//! query-result context. Behaves like a Mustache superset
68//! (`{{var}}`, `{{#each list}}…{{/each}}`, `{{#if cond}}…{{/if}}`,
69//! dotted paths) and emits a visible `{{render-error: …}}` prefix on
70//! parse failure instead of panicking or silently fallback-ing.
71//! - [`infrastructure::adapter`] — Layer 6 **SoT Adapter**. Each axis
72//! wiring entry carries a `metadata.source_uri`; the
73//! [`PluginRegistry`](application::plugin_registry::PluginRegistry)
74//! dispatches by scheme prefix to an `Arc<dyn Adapter>`:
75//! - `file://<path>` / `file:<path>` → `FileAdapter` (`std::fs` with
76//! `~/` expansion; for a directory it picks the newest mtime child).
77//! - `mini-app://<table>` → `MiniAppAdapter` (external crate
78//! `persona-wire-adapter-mini-app`; consumer wires it on top of
79//! [`PluginRegistry::default_builder_for_wire`](application::plugin_registry::PluginRegistry::default_builder_for_wire)).
80//!
81//! ## Two query axes
82//!
83//! Wire exposes two complementary axes; both are first-class:
84//!
85//! - **Dynamic axis** — caller supplies an inline
86//! [`Specification`](domain::specification::Specification) and gets the
87//! matching nodes back via
88//! [`wire_query`](application::use_cases::wire_query). Good for ad-hoc
89//! inspection, scripts, and one-off filters.
90//! - **Fixed axis** — caller registers a `(spec, template, target_form)` as a
91//! [`NamedProjection`](application::projection_registry::NamedProjection)
92//! and refers to it by `spec_ref` / `projection_ref`. Good for stable
93//! surfaces such as wake-time injection.
94//!
95//! ## Render flow (`wire_render`)
96//!
97//! ```text
98//! ProjectionRegistry.get(name)
99//! → NamedProjection { spec_ref, template, target_form }
100//! │
101//! │ spec_ref
102//! ▼
103//! SpecRegistry.get(spec_ref)
104//! → Specification (TypeIs / MetadataEq / And / Or / Not / Reachable)
105//! │
106//! │ Specification::is_satisfied_by
107//! ▼
108//! collect_matching_nodes(storage, spec) → Vec<Node>
109//! │
110//! │ context build: { count, nodes, entries, … }
111//! ▼
112//! rendering::render(target_form, template, context)
113//! → String (Prompt / Markdown / JSON / ASCII)
114//! ```
115//!
116//! ## PromptContext flow (`wire_prompt_context`)
117//!
118//! Persona-scoped one-shot entry intended for wake-time auto-load:
119//!
120//! 1. Read the optional `[extra.persona_wire.projections.<axis>]` overlays
121//! for the persona (best-effort; missing persona-pack is silently
122//! tolerated).
123//! 2. Discover the persona's axes by querying the graph with a
124//! `Specification` (`TypeIs("outline_node")` AND
125//! `MetadataEq("persona", <persona_id>)`). The axis list is therefore
126//! **data, not code** — adding an axis is a graph insert.
127//! 3. For each axis, look up the base
128//! [`NamedProjection`](application::projection_registry::NamedProjection)
129//! by the conventional name `<persona_id>.section.<axis>`. If an overlay
130//! is present, run `MergeStrategy::merge(base, overlay)`. Fetch the axis
131//! payload through the Layer 6 Adapter via the wiring entry's
132//! `source_uri`, then render the block.
133//! 4. Concatenate the rendered blocks into a single `prompt_context` string.
134//! `projection_names: Some([...])` restricts the walk to an explicit
135//! subset; `None` walks every registered axis for the persona.
136//!
137//! No template content is hard-coded inside this crate. The set of axes, the
138//! base templates, and the optional overlays are all data managed through
139//! the regular registry / persona-pack surfaces.
140//!
141//! ## Persistence schema (SQLite, set up by
142//! [`SqliteStorage::migrate`](infrastructure::storage::SqliteStorage::migrate))
143//!
144//! - `type_registry(name TEXT PK, kind TEXT, schema_json TEXT, severity_allowed TEXT)`
145//! - `nodes(id TEXT PK, type TEXT FK→type_registry.name, sot_ref TEXT?, confidence REAL?, …, metadata TEXT)`
146//! - `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, …)`
147//! - `specifications(name TEXT PK, expr_json TEXT, created_at INTEGER)`
148//! - `projections(name TEXT PK, spec_ref TEXT, template TEXT, target_form TEXT, created_at INTEGER)`
149//! - `versions(…)` — autoversion ledger.
150//! - `workflow_runs(…)` — reserved for the workflow engine layer.
151//!
152//! The graph vocabulary is **open** but type-checked: any `Node` or `Edge`
153//! must reference a row in `type_registry`. The default seed is loaded by
154//! [`SqliteStorage::seed_default_types`](infrastructure::storage::SqliteStorage::seed_default_types).
155//!
156//! ## Design rationale
157//!
158//! The sections below collect the architecture-level decisions that were
159//! previously drafted in `docs/design/*.md` while the layout was being
160//! shaped. The design docs are now retired — this is the SoT.
161//!
162//! ### Three-layer split (Math backend / Domain Entity / thin Application)
163//!
164//! 1. **Math backend Graph** ([`domain::graph`]) — open-vocabulary primitives
165//! (Node / Edge / Severity / Specification / CRUD / Compute / Constraint /
166//! AutoVersion / Repository). Tenant-agnostic and persona-agnostic; used
167//! as a backend SDK. It does not know about personas, slots, sources, or
168//! projections.
169//! 2. **Domain Entity Layer** ([`domain::entity`]) — persona-wire's
170//! first-class vocabulary (`PersonaId` / `Slot` / `Source` / `Wiring` /
171//! `Workflow` / `Projection`). Owns invariants and behavior; uses the
172//! Math backend as a persistence SDK. Aggregate composition is documented
173//! in [`domain::entity`] module docs.
174//! 3. **Application Layer** ([`application`]) — thin orchestrator that lifts
175//! Domain Entity operations onto the use-case surface that transport
176//! adapters (MCP / CLI / future RPC) target. Knowledge gravitates to
177//! layer 2; this layer stays slim.
178//!
179//! ### Slot vocabulary (was `axis`)
180//!
181//! The persona-context binding identifier is now named `Slot`. Earlier code
182//! used the AI-jargon word `axis`, which collided with three legitimately
183//! orthogonal uses of "axis" elsewhere in the crate:
184//!
185//! - Graph compute primitive — Traversal / Execution / Constraint axes
186//! (3 orthogonal operation kinds).
187//! - Doctor diagnostic surface — `Axis::{Graph, Workflow}` (2 health axes).
188//! - Plugin registry — Adapter / Engine / Projection plugin axes
189//! (3 orthogonal slots).
190//!
191//! In contrast, persona context values like `mailbox` / `mail` / `news` are
192//! not orthogonal axes — they are slot names. Decisive evidence (recorded
193//! when the rename was decided) included the storage projection name
194//! template `"{persona_id}.section.{axis}"` ("section の中の axis 値"
195//! structure = `axis` is a name, not a classifier) and the error message
196//! `"must contain at least one axis name"` (the word "name" gives the
197//! game away).
198//!
199//! The rename landed in two cuts:
200//!
201//! 1. Domain Entity layer carries [`domain::entity::Slot`] as a first-class
202//! Value Object on [`domain::entity::wiring::Wiring`].
203//! 2. The storage metadata key is still literally `"axis"` (legacy SQLite
204//! rows). [`application::wiring_mapper`] is the single translation
205//! boundary: `Slot ↔ Node.metadata["axis"]`. New code routes through the
206//! mapper; reading `metadata["axis"]` directly is disallowed.
207//!
208//! ### PoEAA Registry vs DDD Repository (Projection)
209//!
210//! Projection persistence goes through
211//! [`application::projection_registry::ProjectionRegistry`], a PoEAA
212//! Registry (Fowler PoEAA Ch.18) — an application-layer service that
213//! provides named access to well-known objects as a structured alternative
214//! to global access. A DDD Repository (Evans DDD Ch.6 / Vernon IDDD Ch.12)
215//! was considered and **not adopted**: it would move persistence vocabulary
216//! into a domain port and collapse the application service into a pass-through.
217//!
218//! [`application::wiring_mapper`] and [`application::workflow_mapper`] are
219//! sibling Data Mappers invoked directly from use cases against the Math
220//! backend Node repository — Wiring and Workflow do not get their own
221//! Registry because they are persisted as graph nodes, not as a separately
222//! named table.
223//!
224//! ### Data Mapper — narrow reading
225//!
226//! Fowler PoEAA's Data Mapper (Ch.10) requires *some* mapper that translates
227//! between persistence shape and Domain shape. The literal pattern uses an
228//! independent Mapper class; persona-wire takes the **narrow** reading and
229//! lets the Registry (Projection case) or the use case (Wiring / Workflow
230//! cases) own the mapper bridge through the
231//! [`application::projection_mapper`] / [`application::wiring_mapper`] /
232//! [`application::workflow_mapper`] modules. Promoting these to a literal
233//! Fowler Mapper trait is a carry that fires only when a second parallel
234//! mapper with the same shape arrives. Until then, the free functions
235//! intentionally do not sit behind a trait.
236//!
237//! ### DDD / BP citation table
238//!
239//! Best-practice citations carry an honest **literal / narrow / 独自整理**
240//! tag so future readers can tell which parts of the BP are quoted verbatim
241//! and which are persona-wire's own extension.
242//!
243//! | BP | Where applied | Tag |
244//! |---|---|---|
245//! | 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 |
246//! | Fowler PoEAA Ch.18 — Registry | `ProjectionRegistry` named lookup | **literal** |
247//! | Evans DDD Ch.9 — Specification | [`domain::graph::specification`] — VO + `is_satisfied_by` + and/or/not algebra | **literal** |
248//! | 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 |
249//! | Vernon IDDD Ch.10 Rule 3 — Reference by Identity | `Projection` references `Specification` via `SpecName` instead of embedding | **literal** |
250//! | 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 |
251//! | 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 |
252//! | 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 |
253//!
254//! [spec-bp]: https://en.wikipedia.org/wiki/Specification_pattern
255
256pub mod application;
257pub mod domain;
258pub mod infrastructure;
259
260pub use application::plugin_registry::AdapterInfo;
261pub use domain::error::WireError;
262pub use domain::error::WireResult;
263pub use infrastructure::filter::{FilterCap, TailSpec, WireFilters};