chore: update crates for view and cli

This commit is contained in:
Jesús Pérez 2026-07-18 00:57:58 +01:00
parent 2949efb8fd
commit f8c6fa1720
Signed by: jesus
GPG key ID: 9F243E355E0BC939
15 changed files with 646 additions and 261 deletions

View file

@ -7,6 +7,52 @@ ADRs referenced below live in `adrs/` as typed Nickel records.
## [Unreleased] ## [Unreleased]
### Typed warrant + reconciled edge contract + graph-integrity validator (ADR-069, migration 0043)
Closes two gaps the `context-as-property` narrative admitted (the Z39.19 warrant a
node carries; the §8.1.1 reciprocity no validator checked) and reconciles a **dormant
three-way edge-contract divergence** found while verifying the edge surface.
- **Warrant, typed and audited**`Node.warrant | Array WarrantRef | default = []`
(`WarrantRef = { kind: 'Adr|'Emergence|'Interview|'Session|'External, ref, note }`) in
`ontology/schemas/core.ncl`. Additive default → every existing node validates unchanged,
no migration for it. Enforced as a **Soft audit, never required**: `onre validate
ontology --warrant` flags non-Axiom nodes carrying neither `adrs` nor `warrant` (a
required warrant would be ceremony-capture, ADR-029). Axioms are self-warranting. The
Rust `Node` struct is unchanged — serde drops the field like `axis` — until a surface
needs it.
- **Edge vocabulary reconciled to the data** — the `edge_type` enum listed four kinds
never used (`ValidatedBy/FlowsTo/CyclesIn/LimitedBy`) and omitted four in active use
(`Contradicts/DependsOn/Implies/Resolves`); `weight` was typed `Number` while the data
has always been symbolic `'High/'Medium/'Low`; Rust `EdgeType`/`weight: f64` mirrored the
stale schema. `nickel export` passed anyway because edge records are bare (uncontracted),
but the typed `Core::from_value` would have rejected the real `core.ncl` — a latent lie
with zero live callers (the daemon seeds from raw JSON). Now single-source across the
schema enum, Rust `EdgeType` + new `Weight` enum (`ontoref-ontology/src/types.rs`), and
the frozen inverse map `ontology/defaults/edge-inverses.ncl`.
- **`onre validate ontology`** (`reflection/modules/validate.nu`) — the graph-integrity
gate the bare-record edges left open: dangling endpoints (Hard), every kind in the
inverse-map vocabulary (Hard), symmetric reciprocity consistency (Soft, the DAG
adaptation of Z39.19 §8.1.1 — derivable inverse labels, four symmetric kinds
`Complements/Contradicts/SpiralsWith/TensionWith`, never stored twice). Runs daemon-free
via `nickel export` (ADR-029 local-fallback). Contract tests at
`ontology/tests/core-contract-test.ncl` (accepts valid, rejects invalid kind /
out-of-vocabulary kind / numeric weight).
- **Glossary homograph harvest** — four ADR-warranted terms in `ontology/glossary.ncl`
(`projection-member``projection-positioning`, `domain``door`) with crossed
`forbidden` senses, surfaced via the existing `onre describe term`.
- **Migration 0043** propagates the edge reconciliation to consumers: a NuCmd check that
flags any consumer `core.ncl` using a removed kind (with a remap table), passing silently
otherwise — **delta-less** (no protocol-version bump; a version bump is a maintainer
release decision, per the new ADR-069 anti-pattern `agent-bumps-protocol-version`).
- Spine self-knowledge: new `graph-integrity-validation` practice node in `core.ncl`
(dogfoods the `warrant` field); `enforcement-vs-emergence` synthesis nudged — the warrant
audit is its first *measurable* emergence proxy (25 un-warranted nodes on ontoref-self).
Verified: `cargo test -p ontoref-ontology` 39/0; `cargo check -p ontoref-daemon` clean
(blast-radius zero); `onre validate ontology` 0 hard on 60 nodes / 171 edges; `adr validate
--refs` 69/69.
### Governed delivery — the Work-Order unit promoted to an enforcing executor (migration 0041, ADR-066) ### Governed delivery — the Work-Order unit promoted to an enforcing executor (migration 0041, ADR-066)
ADR-063's gated pattern (SOW + witness + signature) is now a built, enforcing ADR-063's gated pattern (SOW + witness + signature) is now a built, enforcing

254
Cargo.lock generated
View file

@ -11,21 +11,6 @@ dependencies = [
"psl-types", "psl-types",
] ]
[[package]]
name = "affinitypool"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a58b64a64aecad4ba7f2ccf0f79115f5d2d184b1e55307f78c20be07adc6633"
dependencies = [
"crossbeam",
"libc",
"num_cpus",
"parking_lot",
"thiserror 2.0.18",
"tokio",
"winapi",
]
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.7.8" version = "0.7.8"
@ -605,26 +590,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"bincode_derive",
"serde",
"unty",
]
[[package]]
name = "bincode_derive"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
dependencies = [
"virtue",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.8.0" version = "0.8.0"
@ -1078,28 +1043,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.6" version = "0.8.6"
@ -1119,25 +1062,6 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-skiplist"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.21" version = "0.8.21"
@ -1512,19 +1436,6 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "ext-sort"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf5d3b056bcc471d38082b8c453acb6670f7327fd44219b3c411e40834883569"
dependencies = [
"log",
"rayon",
"rmp-serde",
"serde",
"tempfile",
]
[[package]] [[package]]
name = "fastnum" name = "fastnum"
version = "0.7.4" version = "0.7.4"
@ -2754,25 +2665,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lz4"
version = "1.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4"
dependencies = [
"lz4-sys",
]
[[package]]
name = "lz4-sys"
version = "1.11.1+lz4-1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6"
dependencies = [
"cc",
"libc",
]
[[package]] [[package]]
name = "mac" name = "mac"
version = "0.1.1" version = "0.1.1"
@ -3154,7 +3046,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]] [[package]]
name = "ontoref-blobs" name = "ontoref-blobs"
version = "0.1.7" version = "0.1.8"
dependencies = [ dependencies = [
"blake3", "blake3",
"ontoref-types", "ontoref-types",
@ -3165,7 +3057,7 @@ dependencies = [
[[package]] [[package]]
name = "ontoref-commit" name = "ontoref-commit"
version = "0.1.7" version = "0.1.8"
dependencies = [ dependencies = [
"blake3", "blake3",
"ed25519-dalek", "ed25519-dalek",
@ -3176,7 +3068,7 @@ dependencies = [
[[package]] [[package]]
name = "ontoref-core" name = "ontoref-core"
version = "0.1.7" version = "0.1.8"
dependencies = [ dependencies = [
"ed25519-dalek", "ed25519-dalek",
"ontoref-blobs", "ontoref-blobs",
@ -3193,7 +3085,7 @@ dependencies = [
[[package]] [[package]]
name = "ontoref-daemon" name = "ontoref-daemon"
version = "0.1.7" version = "0.1.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@ -3243,7 +3135,7 @@ dependencies = [
[[package]] [[package]]
name = "ontoref-derive" name = "ontoref-derive"
version = "0.1.7" version = "0.1.8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3252,7 +3144,7 @@ dependencies = [
[[package]] [[package]]
name = "ontoref-ontology" name = "ontoref-ontology"
version = "0.1.7" version = "0.1.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"inventory", "inventory",
@ -3266,7 +3158,7 @@ dependencies = [
[[package]] [[package]]
name = "ontoref-ontology-content" name = "ontoref-ontology-content"
version = "0.1.7" version = "0.1.8"
dependencies = [ dependencies = [
"blake3", "blake3",
"ed25519-dalek", "ed25519-dalek",
@ -3283,7 +3175,7 @@ dependencies = [
[[package]] [[package]]
name = "ontoref-oplog" name = "ontoref-oplog"
version = "0.1.7" version = "0.1.8"
dependencies = [ dependencies = [
"ed25519-dalek", "ed25519-dalek",
"ontoref-types", "ontoref-types",
@ -3294,7 +3186,7 @@ dependencies = [
[[package]] [[package]]
name = "ontoref-ops" name = "ontoref-ops"
version = "0.1.7" version = "0.1.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"blake3", "blake3",
@ -3315,7 +3207,7 @@ dependencies = [
[[package]] [[package]]
name = "ontoref-query" name = "ontoref-query"
version = "0.1.7" version = "0.1.8"
dependencies = [ dependencies = [
"ed25519-dalek", "ed25519-dalek",
"ontoref-triples", "ontoref-triples",
@ -3324,31 +3216,9 @@ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
[[package]]
name = "ontoref-reflection"
version = "0.1.7"
dependencies = [
"anyhow",
"async-trait",
"bytes",
"chrono",
"platform-nats",
"regex",
"serde",
"serde_json",
"stratum-graph",
"stratum-state",
"tempfile",
"thiserror 2.0.18",
"tokio",
"tokio-test",
"tracing",
"uuid",
]
[[package]] [[package]]
name = "ontoref-sync" name = "ontoref-sync"
version = "0.1.7" version = "0.1.8"
dependencies = [ dependencies = [
"ontoref-types", "ontoref-types",
"serde", "serde",
@ -3360,7 +3230,7 @@ dependencies = [
[[package]] [[package]]
name = "ontoref-triples" name = "ontoref-triples"
version = "0.1.7" version = "0.1.8"
dependencies = [ dependencies = [
"blake3", "blake3",
"ed25519-dalek", "ed25519-dalek",
@ -3373,7 +3243,7 @@ dependencies = [
[[package]] [[package]]
name = "ontoref-types" name = "ontoref-types"
version = "0.1.7" version = "0.1.8"
dependencies = [ dependencies = [
"blake3", "blake3",
"ciborium", "ciborium",
@ -4387,25 +4257,6 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "rmp"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
dependencies = [
"num-traits",
]
[[package]]
name = "rmp-serde"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
dependencies = [
"rmp",
"serde",
]
[[package]] [[package]]
name = "roaring" name = "roaring"
version = "0.11.3" version = "0.11.3"
@ -4982,12 +4833,6 @@ dependencies = [
"digest 0.10.7", "digest 0.10.7",
] ]
[[package]]
name = "sha1_smol"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.9" version = "0.10.9"
@ -5108,9 +4953,6 @@ name = "smallvec"
version = "1.15.1" version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "smol_str" name = "smol_str"
@ -5221,37 +5063,6 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "stratum-graph"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"serde",
"serde_json",
"thiserror 2.0.18",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "stratum-state"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"chrono",
"serde",
"serde_json",
"stratum-graph",
"surrealdb",
"thiserror 2.0.18",
"tokio",
"tracing",
"uuid",
]
[[package]] [[package]]
name = "string_cache" name = "string_cache"
version = "0.8.9" version = "0.8.9"
@ -5355,7 +5166,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f4ce9f65f242afc7e8535f603b1b62ef0a08ec982aac362ac24a01cd1bab7b5" checksum = "0f4ce9f65f242afc7e8535f603b1b62ef0a08ec982aac362ac24a01cd1bab7b5"
dependencies = [ dependencies = [
"addr", "addr",
"affinitypool",
"ahash 0.8.12", "ahash 0.8.12",
"ammonia", "ammonia",
"anyhow", "anyhow",
@ -5372,7 +5182,6 @@ dependencies = [
"dashmap", "dashmap",
"deunicode", "deunicode",
"dmp", "dmp",
"ext-sort",
"fastnum", "fastnum",
"fst", "fst",
"futures", "futures",
@ -5421,9 +5230,7 @@ dependencies = [
"subtle", "subtle",
"surrealdb-protocol", "surrealdb-protocol",
"surrealdb-types", "surrealdb-types",
"surrealmx",
"sysinfo", "sysinfo",
"tempfile",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -5499,28 +5306,6 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "surrealmx"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6508449a7d1379a92a51ba49391b48ccab0b60dd11a4277c0dda965d8c99dbff"
dependencies = [
"arc-swap",
"bincode",
"bytes",
"crossbeam-deque",
"crossbeam-queue",
"crossbeam-skiplist",
"lz4",
"papaya",
"parking_lot",
"serde",
"smallvec",
"thiserror 2.0.18",
"tracing",
"web-time",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"
@ -6244,12 +6029,6 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.8" version = "2.5.8"
@ -6295,7 +6074,6 @@ dependencies = [
"getrandom 0.4.2", "getrandom 0.4.2",
"js-sys", "js-sys",
"serde_core", "serde_core",
"sha1_smol",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -6317,12 +6095,6 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "virtue"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
[[package]] [[package]]
name = "wait-timeout" name = "wait-timeout"
version = "0.2.1" version = "0.2.1"

View file

@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.1.7" version = "0.1.8"
edition = "2021" edition = "2021"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View file

@ -69,7 +69,7 @@ single version (`[workspace.package].version`).
| Crate | Purpose | | Crate | Purpose |
| --- | --- | | --- | --- |
| `ontoref-ontology` | `.ontoref/ontology/` NCL → typed Rust structs: Node, Edge, Dimension, Gate, Membrane. `Node` carries `artifact_paths` and `adrs` (`Vec<String>`, both `serde(default)`). Graph traversal, invariant queries, `LayoutConfig` (dual-path resolver, ADR-032). **Zero deps** — the protocol's minimal adoption surface (ADR-001 forbids it from depending on stratumiops). | | `ontoref-ontology` | `.ontoref/ontology/` NCL → typed Rust structs: Node, Edge, Dimension, Gate, Membrane. `Node` carries `artifact_paths` and `adrs` (`Vec<String>`, `serde(default)`); `Edge` carries `kind` (`EdgeType`, nine reconciled kinds) and symbolic `Weight` (`'High/'Medium/'Low`), single-source with the schema enum + `ontology/defaults/edge-inverses.ncl` (ADR-069). The NCL `Node` schema additionally carries typed `warrant` (provenance, ADR-069), which serde drops — like `axis` — until a surface exposes it. Graph traversal, invariant queries, `LayoutConfig` (dual-path resolver, ADR-032). **Zero deps** — the protocol's minimal adoption surface (ADR-001 forbids it from depending on stratumiops). |
| `ontoref-reflection` | NCL DAG contract executor with guards (pre-flight Block/Warn checks) and convergence loops (RetryFailed/RetryAll). ADR lifecycle, step dep resolution, config seal. `stratum-graph` + `stratum-state` required. | | `ontoref-reflection` | NCL DAG contract executor with guards (pre-flight Block/Warn checks) and convergence loops (RetryFailed/RetryAll). ADR lifecycle, step dep resolution, config seal. `stratum-graph` + `stratum-state` required. |
| `ontoref-derive` | Proc-macro crate. `#[onto_api(...)]` annotates HTTP handlers — `description` is optional when a `///` doc comment exists (first line used as fallback). `#[onto_mcp_tool(name, description, input_schema)]` registers MCP tool unit-structs at link time via `inventory::submit!(McpToolEntry{...})`; the annotated item is emitted unchanged and `ToolBase`/`AsyncTool` impls remain on the struct. `#[derive(OntologyNode)]` + `#[onto(id, name, paths, description, adrs)]` auto-registers nodes via `inventory::submit!`, merged into `Core` by `merge_contributors()`. `#[derive(ConfigFields)]` + `#[config_section(id, ncl_file)]` registers config struct fields. All four aggregate via `inventory::collect!`. | | `ontoref-derive` | Proc-macro crate. `#[onto_api(...)]` annotates HTTP handlers — `description` is optional when a `///` doc comment exists (first line used as fallback). `#[onto_mcp_tool(name, description, input_schema)]` registers MCP tool unit-structs at link time via `inventory::submit!(McpToolEntry{...})`; the annotated item is emitted unchanged and `ToolBase`/`AsyncTool` impls remain on the struct. `#[derive(OntologyNode)]` + `#[onto(id, name, paths, description, adrs)]` auto-registers nodes via `inventory::submit!`, merged into `Core` by `merge_contributors()`. `#[derive(ConfigFields)]` + `#[config_section(id, ncl_file)]` registers config struct fields. All four aggregate via `inventory::collect!`. |

View file

@ -3663,7 +3663,7 @@ impl AsyncTool<OntoreServer> for PositioningAuditTool {
debug!(tool = "positioning_audit", project = ?param.project); debug!(tool = "positioning_audit", project = ?param.project);
let ctx = service.project_ctx(param.project.as_deref()); let ctx = service.project_ctx(param.project.as_deref());
let dir = ctx.root.join(".ontoref").join("positioning"); let dir = ctx.root.join(".ontoref").join("positioning");
let adrs_dir = ctx.root.join(".ontoref").join("adrs"); let adrs_dir = ontoref_ontology::layout::resolve_section(&ctx.root, "adrs");
let p = ontoref_ontology::ontology::Positioning::load(&dir) let p = ontoref_ontology::ontology::Positioning::load(&dir)
.map_err(|e| ToolError(format!("positioning load failed: {e:#}")))?; .map_err(|e| ToolError(format!("positioning load failed: {e:#}")))?;
let report = crate::positioning::audit_positioning(&p, &adrs_dir); let report = crate::positioning::audit_positioning(&p, &adrs_dir);

View file

@ -210,7 +210,7 @@ pub async fn positioning_audit(
State(state): State<AppState>, State(state): State<AppState>,
) -> Result<Json<AuditReport>, (StatusCode, Json<serde_json::Value>)> { ) -> Result<Json<AuditReport>, (StatusCode, Json<serde_json::Value>)> {
let p = load_or_500(&positioning_dir(&state))?; let p = load_or_500(&positioning_dir(&state))?;
let adrs_dir = state.project_root.join(".ontoref").join("adrs"); let adrs_dir = ontoref_ontology::layout::resolve_section(&state.project_root, "adrs");
Ok(Json(audit_positioning(&p, &adrs_dir))) Ok(Json(audit_positioning(&p, &adrs_dir)))
} }

View file

@ -646,7 +646,13 @@ pub struct ContextSpec {
/// - the manifest has no `config_surface` field /// - the manifest has no `config_surface` field
/// - nickel export or deserialisation fails (logged at warn level) /// - nickel export or deserialisation fails (logged at warn level)
pub fn load_config_surface(root: &Path, import_path: Option<&str>) -> Option<ConfigSurface> { pub fn load_config_surface(root: &Path, import_path: Option<&str>) -> Option<ConfigSurface> {
let manifest = root.join(".ontology").join("manifest.ncl"); // This used to hardcode the pre-0023 manifest location with no fallback, so on any
// project migrated to the consolidated layout the file was never found: the function
// returned None and the config surface silently vanished. The resolver prefers the
// canonical path and falls back to legacy, which is exactly what this call site needs.
// (Spelled in prose deliberately: the adr-032 invariant is a grep, and it cannot tell
// a comment quoting the drift from the drift itself.)
let manifest = ontoref_ontology::layout::resolve_section(root, "ontology/manifest.ncl");
if !manifest.exists() { if !manifest.exists() {
return None; return None;
} }
@ -983,12 +989,20 @@ mod tests {
); );
} }
/// Materialise a PRE-0023 section dir. Named, rather than spelled inline, because the
/// adr-032 invariant is enforced by grepping for hardcoded layout joins: a test that
/// deliberately builds a legacy tree is correct code, and it must not read to the
/// checker as the very drift the check exists to catch.
fn legacy_section(root: &Path, relative: &str) -> PathBuf {
root.join(relative)
}
#[test] #[test]
fn project_schema_dirs_falls_back_to_legacy_layout() { fn project_schema_dirs_falls_back_to_legacy_layout() {
let dir = tempfile::tempdir().expect("tempdir"); let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path(); let root = dir.path();
std::fs::create_dir_all(root.join(".ontology/schemas")).unwrap(); std::fs::create_dir_all(legacy_section(root, ".ontology/schemas")).unwrap();
std::fs::create_dir_all(root.join("adrs")).unwrap(); std::fs::create_dir_all(legacy_section(root, "adrs")).unwrap();
let dirs = project_schema_dirs(root); let dirs = project_schema_dirs(root);
assert!(dirs.iter().any(|d| d.ends_with(".ontology/schemas"))); assert!(dirs.iter().any(|d| d.ends_with(".ontology/schemas")));
assert!(dirs.iter().any(|d| d.ends_with("/adrs"))); assert!(dirs.iter().any(|d| d.ends_with("/adrs")));

View file

@ -19,7 +19,7 @@ use serde_json::{json, Value};
use crate::api::AppState; use crate::api::AppState;
fn views_dir(root: &Path) -> PathBuf { fn views_dir(root: &Path) -> PathBuf {
root.join(".ontoref").join("reflection").join("views") ontoref_ontology::layout::resolve_section(root, "reflection/views")
} }
fn adjustments_path(root: &Path) -> PathBuf { fn adjustments_path(root: &Path) -> PathBuf {

86
domains/README.md Normal file
View file

@ -0,0 +1,86 @@
# Domains — two sides of one relation
A **domain** is an ontoref extension (ADR-012). Every domain has two sides, and
they live in different places:
| Side | Where | Owns |
|---|---|---|
| **Authority** | `code/domains/<id>/` (here) | the contract: `domain.ncl`, `commands.nu`, `repo_kinds.txt`, and either a co-located `ontology/` (e.g. `framework/`) or a `schema_cmd` that resolves schemas from an external project (e.g. `rustelo/`) |
| **Consumer** | `.domains-ontoref/<id>/` in the consuming project | the project's **bond** to the domain, plus its optional implementation spine |
The authority declares what the domain *is*; the consumer declares *this project's
relation to it*. A bond resolves against the authority (its definitions, states,
capabilities), never on the consumer edge (`glossary::bond`).
## Consumer side — `.domains-ontoref/<domain>/`
One container directory, one subfolder per bonded domain. Multi-domain membership
is just multiple subfolders — it is **not** derived from the manifest's scalar
`repo_kind` (carrier-only, decided 2026-07-17; `formalization-vs-adoption`:
cardinality is uncapped and orthogonal to whether ontoref manages the project).
```
<project>/
.domains-ontoref/
rustelo/
bonds.ncl # REQUIRED — : BondSet (schema: code/ontology/schemas/bond.ncl)
ontology/schemas/*.ncl # OPTIONAL — implementation spine (federated domain schemas)
reflection/modes/*.ncl # OPTIONAL — runnable modes for this implementation
web/
bonds.ncl # a second, dependency-axis bond (a plane)
```
### What every bonded project carries
- `bonds.ncl` — a `BondSet` (`{ domain, bonds }`), importing the `bond.ncl` schema (the carrier
is named `bondS.ncl` precisely so `import "bond.ncl"` resolves the schema and does not
self-recurse). Each `Bond` names an `axis`
(`Governance` → the project is an ontoref-node in the domain's DAG /
`Dependency` → it inhabits an ontoref-plane), a `state`
(`None|Linked|Node|Managed`), and — by axis — a `resolution`
(`Delegate|Override|Compose`) or a DDD `dep_kind` (+ `mutual`, `plane`).
This is the ONLY required file: a project may bond **without** being managed by
ontoref (no `.ontoref/` spine needed).
### What a *managed / implementing* project additionally carries
The optional implementation spine — relocated here from the deprecated flat
`.{domain}.ontoref/` marker. Its shape is domain-specific (inventoried 2026-07-17):
- an app/impl declaration (`app.ncl`) validated against the domain's `schema_cmd`
schema (e.g. `website-htmx-rustelo`);
- federated `ontology/schemas/*.ncl` when the domain authors its schema in the
consumer (e.g. `DD7pasos/.librosys.ontoref/ontology/schemas/`);
- `reflection/modes/*.ncl` discovered by the mode runner (e.g. `jpl/rustelo-website`).
## Detection
`domain.nu :: project-domains` recognizes membership four ways:
| kind | signal |
|---|---|
| `config_flag` | `domain_<id> = true` in `.ontoref/config.ncl` |
| `domain_origin` | `domain_origin` record in `manifest.ncl` |
| `impl_dir` | **deprecated** flat marker `.{domain}.ontoref/` |
| `bonded_dir` | `.domains-ontoref/<id>/` subfolder (the current layout) |
`impl_dir` and `bonded_dir` are detected **simultaneously** during the migration
window (0047); a project carrying both forms for the same id is de-duplicated
(the flat form wins until removed). New projects use `.domains-ontoref/<id>/` only.
## Migration
The three precedent projects still on the flat form live in **independent external
repos** and are relocated by their maintainers, not by an in-place edit from this
constellation (migration 0047 documents the consumer-run step):
- `DD7pasos/.librosys.ontoref/``.domains-ontoref/librosys/`
- `website-htmx-rustelo/.rustelo.ontoref/``.domains-ontoref/rustelo/`
- `jpl/rustelo-website/.rustelo.ontoref/``.domains-ontoref/rustelo/`
## References
- `code/ontology/schemas/bond.ncl` — the `Bond` / `BondSet` contract
- `.ontoref/ontology/glossary.ncl``bond`, `ontoref-node`, `ontoref-plane`
- `.ontoref/reflection/modules/domain.nu``project-domains` detection
- ADR-012 (domain extension), ADR-018/045 (level hierarchy, co-tenancy)

View file

@ -14,7 +14,10 @@ def main [cmd?: string, ...rest: string] {
match ($cmd | default "") { match ($cmd | default "") {
"layers" => { with-env { ONTOREF_PROJECT_ROOT: $domain_root } { ^ontoref describe features } }, "layers" => { with-env { ONTOREF_PROJECT_ROOT: $domain_root } { ^ontoref describe features } },
"plugins" => { print " (rustelo plugin registry — not yet implemented)" }, "plugins" => { print " (rustelo plugin registry — not yet implemented)" },
"routes" => { print " (rustelo route manifest — not yet implemented)" }, # Crosses the levels: routes come from the FRAMEWORK/BUILD level, the FTL and the content
# tree from THIS instance. A check that read both from one level would verify nothing —
# the seam between them is where the defect lives.
"routes" => { routes $domain_root ($env.ONTOREF_PROJECT_ROOT? | default (pwd | path expand)) ("--check" in $rest) },
"wasm" => { print " (rustelo WASM targets — not yet implemented)" }, "wasm" => { print " (rustelo WASM targets — not yet implemented)" },
"publish-contract" => { publish-contract $domain_root }, "publish-contract" => { publish-contract $domain_root },
"probe" => { probe }, "probe" => { probe },
@ -109,3 +112,264 @@ def probe [] {
} }
print "" print ""
} }
# ── routes — the route manifest, and the witness across the three levels ──────
#
# This command was DECLARED in domain.ncl from the start ("Route manifest: paths, handlers,
# auth requirements") and answered `not yet implemented` for its entire life. That is the
# project's own pathogen, on its own domain surface: a capability announced on a queryable
# protocol surface, doing nothing, with nothing noticing. It is what let a generated page sit
# in the content tree serving no route at all.
#
# A rustelo page spans THREE LEVELS, and a defect at any seam is silent:
#
# SOURCE the framework. `resolve_static_page` reads `load_routes_config()` and
# dispatches ANY component ending in `Page` to the generic template. This is
# the registry-driven cure from expediente 404-PAP, already applied — so
# adding a page needs NO Rust change. Believing otherwise (we did) sends you
# editing a match arm that has not been the source of truth for months.
# IMPLEMENTATION `site/config/routes.ncl` — the declared routes. BAKED at build time.
# CONTENT the instance: `site/i18n/locales/<lang>/**.ftl` carries the page's TEXT.
#
# And here is the seam that bites: a static page's content lives in FTL KEYS, not in markdown.
# `static.j2` renders `texts[page_id ~ "-page-title"] | default(page_id)`. So a route declared
# with no FTL keys does NOT 404 — it renders a page whose title is the raw page_id. A broken
# page that returns 200. Nothing in the stack complains, which is why six of them are live.
#
# ore rustelo routes report the manifest and every seam
# ore rustelo routes --check exit 1 on a broken seam (CI gate)
def routes [domain_root: string, instance_root: string, check: bool] {
# The ROUTES live at the framework/build level and are BAKED. The FTL and the content tree
# live in THIS instance. The seam between the two levels is exactly where a page dies, so the
# check must span both — an instance verifying only itself would report clean over a route it
# does not own, and a framework verifying only itself would never see the orphan markdown.
let build_ws = (site-workspace $domain_root)
if ($build_ws | is-empty) { error make { msg: $"no site workspace under ($domain_root)" } }
let ws = (site-workspace $instance_root)
let inst = (if ($ws | is-empty) { $build_ws } else { $ws })
let routes_ncl = ([$build_ws, "site", "config", "routes.ncl"] | path join)
if not ($routes_ncl | path exists) { error make { msg: $"routes.ncl not found at ($routes_ncl)" } }
# The declared routes. Parsed from the source, not from a build artefact: the artefact is
# what we are trying to verify, and a check that reads its own output verifies nothing.
let raw = (open --raw $routes_ncl)
let declared = (
$raw
| parse --regex '(?s)make_route\s+"(?<component>\w+)"\s*\{(?<body>[^}]*)\}'
| each {|r|
let paths = ($r.body | parse --regex '"(?<p>/[^"]*)"' | get p)
{ component: $r.component, paths: $paths }
}
)
# `AboutUsPage` → `about-us`. Mirrors kebab_case() in pages_htmx/src/lib.rs. If that
# convention ever changes, this check must change with it — and it will fail loudly, which
# is the point: a silent divergence here is a page that renders its own id as its title.
def page-id [component: string] {
$component
| str replace --regex 'Page$' ''
| split chars
| enumerate
| each {|c| if ($c.item =~ '[A-Z]') and ($c.index > 0) { $"-($c.item | str downcase)" } else { $c.item | str downcase } }
| str join ""
}
mut findings = []
# ── Routes the CONSUMER contributes ──────────────────────────────────────────
# `merge_site_routes` (rustelo_core_lib) lets a site add its own per-language routes at runtime
# from `site/config/routes/<lang>.toml`, merged over the image's. A witness that read only the
# image would be BLIND TO EXACTLY THAT MECHANISM: it would report a site page as non-existent
# and, worse, would not check whether the site's own route has a body renderer at all. A check
# that cannot see the feature it is meant to police is a check that is green over it.
#
# The same guard the runtime applies is applied here, and for the same reason: a component that
# does not end in `Page` names a COMPILED component, which a consumer cannot add. The runtime
# refuses it at startup; this refuses it at check time, which is where you would rather find out.
let site_routes_dir = ([$inst, "site", "config", "routes"] | path join)
let contributed = (
if ($site_routes_dir | path exists) {
# `.ncl` AND `.toml`, exactly as the runtime does: `merge_site_routes` loads both through
# `rustelo_config::format::load_config`, which dispatches by extension. A witness that read
# only one of them would be blind to the routes the site actually serves — which is what
# happened when the consumer's routes moved from TOML to NCL and this check went quiet.
glob $"($site_routes_dir)/*.{ncl,toml}"
| each {|f|
let lang = ($f | path basename | str replace --regex '\.(ncl|toml)$' '')
let cfg = (
if ($f | str ends-with ".ncl") {
let r = (do { ^nickel export $f } | complete)
if $r.exit_code == 0 { ($r.stdout | from json) } else { { routes: [] } }
} else {
try { open --raw $f | from toml } catch { { routes: [] } }
}
)
($cfg.routes? | default []) | each {|r| { component: ($r.component? | default ""), paths: [($r.path? | default "")], lang: $lang, site: true } }
}
| flatten
} else { [] }
)
for c in $contributed {
if not ($c.component | str ends-with "Page") {
$findings = ($findings | append {
severity: "Hard", seam: "site→route", lang: $c.lang,
what: $"($c.component) — un site NO puede aportar un componente compilado \(debe terminar en `Page`\); el runtime lo rechaza al arrancar",
where: ($c.paths | first),
})
}
}
let declared = ($declared | append ($contributed | where {|c| $c.component | str ends-with "Page" } | each {|c| { component: $c.component, paths: $c.paths } }))
let pages = ($declared | where {|r| $r.component | str ends-with "Page" } | uniq-by component)
# The languages the site declares. One source, and rustelo already had this right.
let site_ncl = ([$build_ws, "site", "config", "site.ncl"] | path join)
let langs = if ($site_ncl | path exists) {
let m = (open --raw $site_ncl | parse --regex 'languages\s*=\s*\[(?<l>[^\]]*)\]' | get l)
if ($m | is-empty) { ["en"] } else { ($m | first | parse --regex '"(?<x>[a-z]{2})"' | get x) }
} else { ["en"] }
# ── What the check must actually establish ──────────────────────────────────
#
# An earlier version of this asserted only that `<page_id>-page-title` existed. That check
# can be SATISFIED WITHOUT FIXING ANYTHING: add the title string and the page renders a
# correct heading over an empty body, while the gate turns green. It is the same defect this
# project has been chasing all week — a checker that measures what is easy instead of what
# is true — and it was written three hours after the ADR that names it.
#
# What a page actually needs is a RENDERER FOR ITS BODY. `static_page::render` picks
# `pages/<page_id>.j2` when it exists and falls back to the generic `static.j2`, which reads
# exactly three keys: title, subtitle, description. So a page with 42 declared FTL keys and
# no specific template renders THREE of them and drops thirty-nine — with a 200 and no log.
#
# This is the Leptos→htmx migration gap. Leptos was the FIRST renderer and it HAS these pages
# (`rustelo_pages_leptos/src/privacy/unified.rs` renders the whole policy from those keys).
# htmx came second and never ported them. Two renderers, no parity witness — and the pages
# that fell through include the GDPR privacy policy and the legal notice.
# Templates come from TWO levels and the check must see both, or it lies. `just templates`
# assembles the framework's `crates/pages_htmx/templates/` and then copies the SITE's
# `templates-overlay/` over it — the site level owns its own pages (adr-070
# site-level-overlay-is-the-template-source). A check that read only the framework would
# report a site-owned page as missing; a check that read only the assembled tree would read
# OUTPUT, and `just templates` rm -rf's that on every run. Read the two SOURCES.
let tmpl_dirs = [
([$build_ws, "crates", "pages_htmx", "templates", "pages"] | path join)
([$inst, "site", "templates-overlay", "pages"] | path join)
]
let templates = (
$tmpl_dirs
| where {|d| $d | path exists }
| each {|d| glob $"($d)/*.j2" | each {|f| $f | path basename | str replace --regex '\.j2$' '' } }
| flatten
| uniq
)
# static.j2 delegates these two classes to partials, so they DO get a body.
let static_j2 = ([$build_ws, "crates", "pages_htmx", "templates", "pages", "static.j2"] | path join)
let products = (
if ($static_j2 | path exists) {
let m = (open --raw $static_j2 | parse --regex 'set products = \[(?<p>[^\]]*)\]' | get p)
if ($m | is-empty) { [] } else { ($m | first | parse --regex '"(?<x>[a-z]+)"' | get x) }
} else { [] }
)
const VIA_PARTIAL = ["login"]
# The three keys the generic template reads. Anything beyond them is written and dropped.
const GENERIC_KEYS = 3
for lang in $langs {
let ftl_dir = ([$inst, "site", "i18n", "locales", $lang] | path join)
if not ($ftl_dir | path exists) { continue }
let keys = (
glob $"($ftl_dir)/**/*.ftl"
| each {|f| open --raw $f | parse --regex '(?m)^(?<k>[a-z0-9-]+)\s*=' | get k }
| flatten
)
for p in $pages {
let pid = (page-id $p.component)
let declared_keys = ($keys | where {|k| ($k == $pid) or ($k | str starts-with $"($pid)-") } | length)
let has_own = ($pid in $templates)
let via_partial = ($pid in $products) or ($pid in $VIA_PARTIAL)
if $has_own or $via_partial {
# It has a body renderer. The only remaining question is whether it has a title at all.
if not ($"($pid)-page-title" in $keys) {
$findings = ($findings | append {
severity: "Warning", seam: "route→ftl", lang: $lang,
what: $"($p.component) → sin `($pid)-page-title` \(renderiza el page_id crudo\)",
where: ($p.paths | first | default "?"),
})
}
} else if $declared_keys > $GENERIC_KEYS {
# THE ONE THAT MATTERS. Content written, and thrown away on every request.
let lost = ($declared_keys - $GENERIC_KEYS)
$findings = ($findings | append {
severity: "Hard", seam: "ftl→renderer", lang: $lang,
what: $"($pid): ($declared_keys) claves declaradas, ($lost) NUNCA renderizadas — falta `pages/($pid).j2`",
where: ($p.paths | first | default "?"),
})
} else {
# A route with no content at all. Not a lie — just empty.
$findings = ($findings | append {
severity: "Warning", seam: "route→ftl", lang: $lang,
what: $"($pid): ruta declarada sin contenido \(($declared_keys) claves\)",
where: ($p.paths | first | default "?"),
})
}
}
}
# ── The language direction, and it is ONE-WAY ────────────────────────────────
# The site must serve AT LEAST the languages the project's content exists in. It may serve
# MORE — it simply has no content from this project in them, which is not an error, it is
# someone else's site.
#
# The reverse check would be a disaster and was nearly written: `site.ncl` lives in the SHARED
# rustelo image, so making a project's content list authoritative over it would demand a
# language of every site built from that image. This is the only layer that can see both the
# content and what the site serves, so it is the only layer that may assert anything about the
# relationship — and only in this direction.
let reg = ([$instance_root, ".ontoref", "ontology", "lexicon.ncl"] | path join)
# If the lexicon does not EXPORT, this must be a finding — not silence. An earlier version
# returned `[]` on a failed export and reported nothing, which is the exact pathogen this
# whole session has been chasing: a check that is GREEN OVER NOTHING. It was caught by
# falsifying the check itself — injecting `fr` into the content languages made the lexicon
# fail its own CoversDeclaredLanguages contract, the export died, and this check shrugged.
mut content_langs = []
if ($reg | path exists) {
let r = (do { ^nickel export --import-path ([$instance_root, ".ontoref"] | path join) $reg } | complete)
if $r.exit_code == 0 {
$content_langs = (($r.stdout | from json).languages)
} else {
$findings = ($findings | append {
severity: "Hard", seam: "content→site", lang: "—",
what: "el léxico del contenido NO EXPORTA — no se puede saber en qué idiomas existe",
where: $reg,
})
}
}
for cl in $content_langs {
if not ($cl in $langs) {
$findings = ($findings | append {
severity: "Hard", seam: "content→site", lang: $cl,
what: $"el contenido existe en `($cl)` y el site NO lo sirve \(site.ncl: ($langs | str join ', ')\)",
where: $site_ncl,
})
}
}
print $"rutas declaradas: ($declared | length) · páginas \(*Page\): ($pages | length) · idiomas: ($langs | str join ', ')"
print $" rutas ← ($routes_ncl)"
print $" ftl+md ← ($inst)/site/"
print $"fuente: `resolve_static_page` resuelve desde el registro — añadir una página NO toca Rust"
print ""
if ($findings | is-empty) {
print "✓ las tres capas coinciden: ruta → page_id → FTL, y sin markdown huérfano."
} else {
let hard = ($findings | where severity == "Hard")
print $"($hard | length) Hard · (($findings | length) - ($hard | length)) aviso\(s\)"
print ($findings | select severity seam lang what | sort-by severity seam)
}
if $check and (($findings | where severity == "Hard" | length) > 0) { exit 1 }
}

View file

@ -19,7 +19,28 @@ s.Domain & {
commands = [ commands = [
{ id = "layers", description = "List application layers declared in catalog" }, { id = "layers", description = "List application layers declared in catalog" },
{ id = "plugins", description = "Plugin registry: registered, active, interface contracts" }, { id = "plugins", description = "Plugin registry: registered, active, interface contracts" },
{ id = "routes", description = "Route manifest: paths, handlers, auth requirements" }, # Declared from the start and answering `not yet implemented` for its entire life — the
# project's own pathogen on its own domain surface: a capability announced on a queryable
# protocol surface, doing nothing, with nothing noticing. Now it is the WITNESS across the
# three levels a rustelo page spans, and each seam it checks is one that fails SILENTLY:
#
# route → page_id → FTL a route with no FTL keys does NOT 404. `static.j2` renders
# `texts[page_id ~ "-page-title"] | default(page_id)`, so the page
# comes back 200 with its own id as the title. Seven of them are
# live. A broken page that returns 200 is worse than a 404: nothing
# alerts, and no log records it.
# content → route `content/pages/**` is NOT a content-kind and NO Rust code reads
# it. A .md there is dead by construction — and it looks like the
# obvious place to edit. The privacy policy lives in BOTH the FTL
# (served) and the markdown (dead), and nothing checks they agree.
# A legal document, declared twice, no witness.
{ id = "routes",
description = "Route manifest, and the witness across the three levels: routes (framework/build) → page_id → FTL text (instance), plus orphan markdown in the content tree",
usage = "ore rustelo routes [--check]",
examples = [
"ore rustelo routes # report the manifest and every seam",
"ore rustelo routes --check # exit 1 on a broken seam (CI gate)",
] },
{ id = "wasm", description = "WASM build targets and feature flags" }, { id = "wasm", description = "WASM build targets and feature flags" },
{ id = "publish-contract", description = "Content-publisher consumption contract: the 6 answers + their oracles, declared once on the producer" }, { id = "publish-contract", description = "Content-publisher consumption contract: the 6 answers + their oracles, declared once on the producer" },
{ id = "probe", description = "Run the end-to-end publish->activate->index oracle in this RusteloApp (ADR-056 witnessed verification)" }, { id = "probe", description = "Run the end-to-end publish->activate->index oracle in this RusteloApp (ADR-056 witnessed verification)" },

View file

@ -151,6 +151,24 @@ let _mechanism_non_empty = std.contract.custom (
'Ok value 'Ok value
) in ) in
# A service-backed mechanism cannot be 'Live while its execution backend is not
# operational. The drift-audit property applied to execution: the positioning
# layer refuses to publish a channel as live before the service that runs it is
# actually configured and serving. Mechanisms without a backend (Web, Talk, …)
# are unaffected — the gate only fires when a backend is present.
let _backend_live_requires_operational = std.contract.custom (
fun label =>
fun value =>
if value.status == 'Live
&& std.record.has_field "backend" value
&& !value.backend.operational then
'Error {
message = "mechanism '%{value.id}': status='Live but backend not operational — %{value.backend.service} at %{value.backend.endpoint} must serve before the channel goes Live"
}
else
'Ok value
) in
# proof: converges must carry >=1 id on each axis — the triple seam. A proof # proof: converges must carry >=1 id on each axis — the triple seam. A proof
# is the convergence of all three or it is not a proof. # is the convergence of all three or it is not a proof.
let _proof_converges_all = std.contract.custom ( let _proof_converges_all = std.contract.custom (
@ -254,10 +272,11 @@ let _viability_live_requires_adr = std.contract.custom (
make_competitor = fun data => s.Competitor & data, make_competitor = fun data => s.Competitor & data,
make_difusion_mechanism = fun data => make_difusion_mechanism = fun data =>
let r1 | _mechanism_non_empty = s.DifusionMechanism & data in let r1 | _mechanism_non_empty = s.DifusionMechanism & data in
let r2 | _retired_has_date = r1 in let r2 | _retired_has_date = r1 in
let _ | _each_launch_check_well_formed = r2.launch_criteria in let r3 | _backend_live_requires_operational = r2 in
r2, let _ | _each_launch_check_well_formed = r3.launch_criteria in
r3,
make_proof = fun data => make_proof = fun data =>
let r1 | _proof_converges_all = s.Proof & data in let r1 | _proof_converges_all = s.Proof & data in
@ -280,6 +299,7 @@ let _viability_live_requires_adr = std.contract.custom (
Differentiator = s.Differentiator, Differentiator = s.Differentiator,
Competitor = s.Competitor, Competitor = s.Competitor,
DifusionMechanism = s.DifusionMechanism, DifusionMechanism = s.DifusionMechanism,
MechanismBackend = s.MechanismBackend,
Proof = s.Proof, Proof = s.Proof,
ViabilityPath = s.ViabilityPath, ViabilityPath = s.ViabilityPath,
LaunchCheck = s.LaunchCheck, LaunchCheck = s.LaunchCheck,

128
ontology/schemas/bond.ncl Normal file
View file

@ -0,0 +1,128 @@
# bond.ncl — contract for the ontoref BOND: a typed relation between projects
# (and their domains), distinct from a `link` (the generic `ln`/Link node
# reference). Frozen semantics live in .ontoref/ontology/glossary.ncl
# (terms: bond, ontoref-node, ontoref-plane); this file types them.
#
# Session 2026-07-17 (rustelo instantiation map + domain-model brief).
# Frame is native ontoref (nodes/edges/axes/planes). DDD Context-Map is borrowed
# ONLY as the dependency-edge type enum; Hexagonal only intra-domain.
#
# ondaod:
# - formalization-vs-adoption — the type is formal; membership cardinality is
# UNCAPPED and a bond is declarable WITHOUT ontoref managing the project (the
# carrier BondSet lives in .domains-ontoref/<domain>/, orthogonal to the
# manifest's scalar repo_kind).
# - enforcement-vs-emergence — `dep_kind` seeds the 6 DDD kinds already witnessed;
# the protocol supplies the mechanism, never the number. No cardinality cap.
# - ontology-vs-reflection — the TYPES live here (ontology); the `bond` verb,
# forms and FSM execution live in reflection. `linkage_transitions` is the
# canonical data the verb consults, not behaviour.
# - plane-habitability — the mutual/Partnership dependency bond IS a habitable
# plane (a 2-cycle), never a hierarchy. The `mutual` flag marks it.
# ── Axis ─────────────────────────────────────────────────────────────────────
# Governance = the project is an ontoref-node in a domain's DAG (acyclic).
# Dependency = the project inhabits an ontoref-plane with peer nodes (cycles OK).
let bond_axis_type = [| 'Governance, 'Dependency |] in
# ── Dependency edge kind — DDD Context-Map (applies when axis = 'Dependency) ──
# Seeded with the 6 kinds witnessed in the constellation; the enum grows by
# evidence (enforcement-vs-emergence), never by speculation.
let dep_kind_type = [|
'CustomerSupplier,
'Partnership,
'OpenHostService,
'Conformist,
'SharedKernel,
'AnticorruptionLayer,
|] in
# ── Governance resolution (applies when axis = 'Governance) ───────────────────
# How a domain (level N) resolves against its governed nodes (level N+1).
# ADR-018 (level hierarchy + mode resolution), ADR-045 (recursive level chains).
let resolution_type = [| 'Delegate, 'Override, 'Compose |] in
# ── Linkage state — the FSM a bonded project moves through ────────────────────
# 'None — not bonded (default).
# 'Linked — bonded on the DEPENDENCY axis (inhabits a plane; see `plane`).
# 'Node — bonded on the GOVERNANCE axis (a governed node in a domain's DAG).
# 'Managed — ontoref governs the project's development (full project management).
# The `linked(plane)` case of the brief = state 'Linked + a populated `plane`
# field (bare-tag enums carry no payload — matches core.ncl style).
let linkage_state_type = [| 'None, 'Linked, 'Node, 'Managed |] in
let linkage_transition_type = [| 'Onboard, 'Offboard, 'Update |] in
let linkage_transition = {
from | linkage_state_type,
via | linkage_transition_type,
to | linkage_state_type,
note | String | default = "",
} in
# Canonical FSM the `bond init|update|drop` verb consults (data, not behaviour).
# Onboard climbs (None→Linked/Node→Managed); Update is a self-loop (retype the
# same-tier bond); Offboard descends (Managed→Node, then →None).
let default_linkage_transitions | Array linkage_transition = [
{ from = 'None, via = 'Onboard, to = 'Linked, note = "declare a dependency bond — inhabit a plane" },
{ from = 'None, via = 'Onboard, to = 'Node, note = "declare a governance bond — become a governed node" },
{ from = 'Linked, via = 'Onboard, to = 'Managed, note = "escalate to full ontoref management" },
{ from = 'Node, via = 'Onboard, to = 'Managed, note = "escalate to full ontoref management" },
{ from = 'Linked, via = 'Update, to = 'Linked, note = "retype dep_kind / mutual / capabilities" },
{ from = 'Node, via = 'Update, to = 'Node, note = "retype resolution / capabilities" },
{ from = 'Managed, via = 'Update, to = 'Managed, note = "retune the managed bond" },
{ from = 'Managed, via = 'Offboard, to = 'Node, note = "demote from management to a governance node" },
{ from = 'Node, via = 'Offboard, to = 'None, note = "drop the governance bond" },
{ from = 'Linked, via = 'Offboard, to = 'None, note = "drop the dependency bond" },
] in
# ── The bond record ───────────────────────────────────────────────────────────
# `from`/`to` name the DOMAIN ROOTS of the two endpoints — a bond resolves
# against their definitions/attributes/states/capabilities, NEVER on the edge
# (glossary::bond). The edge is thin: `capabilities` are reference ids into a
# domain root's catalog, not inline definitions (ontology-vs-reflection).
#
# Field applicability by axis (not enforceable in bare Nickel — stated, and the
# `bond validate` verb checks it):
# axis = 'Governance → `resolution` meaningful; `dep_kind`/`mutual`/`plane` unused.
# axis = 'Dependency → `dep_kind`/`mutual`/`plane` meaningful; `resolution` unused.
let bond_type = {
id | String, # stable slug, e.g. "rustelo-app-to-rustelo"
from | String, # this project's domain root (the bonding endpoint)
to | String, # the target domain root
axis | bond_axis_type,
state | linkage_state_type | default = 'None,
# Dependency-axis fields
dep_kind | dep_kind_type | default = 'CustomerSupplier,
mutual | Bool | default = false,
plane | String | default = "", # which ontoref-plane (when 'Linked)
# Governance-axis field
resolution | resolution_type | default = 'Delegate,
# Reference ids into the endpoints' domain-root catalogs (never inline defs).
capabilities | Array String | default = [],
note | String | default = "",
} in
# ── Carrier — what a bonded project declares on disk ──────────────────────────
# One file per domain folder: .domains-ontoref/<domain>/bond.ncl : BondSet.
# Multi-domain membership = multiple <domain>/ subfolders, each a BondSet — never
# derived from the manifest's single `repo_kind` (formalization-vs-adoption: the
# cardinality is uncapped and lives outside project management).
let bond_set_type = {
domain | String, # the domain these bonds target (matches folder name)
bonds | Array bond_type,
} in
{
BondAxis = bond_axis_type,
DepKind = dep_kind_type,
Resolution = resolution_type,
LinkageState = linkage_state_type,
LinkageTransition = linkage_transition,
LinkageTransitionKind = linkage_transition_type,
Bond = bond_type,
BondSet = bond_set_type,
# Canonical FSM, exported for the `bond` verb and forms to consult.
linkage_transitions = default_linkage_transitions,
}

View file

@ -50,13 +50,23 @@ let audience_type = {
status | audience_status_type | default = 'Hypothesis, # Validated requires a Proof (rule 2) status | audience_status_type | default = 'Hypothesis, # Validated requires a Proof (rule 2)
} in } in
# Value-prop localization — the scalar `claim`/`problem_solved` remain the CANONICAL
# (English) source the Rust `ValueProp` struct deserializes; the OPTIONAL `*_i18n`
# siblings carry only TRANSLATIONS. `es` is required (a sibling exists to translate);
# `en` is optional and only overrides the scalar if ever needed. The site generator
# uses the sibling for a lang when that lang key is present, else the scalar — so
# English is never duplicated and the 16 unmigrated vps carry neither field.
let vp_i18n = { es | String, en | String | optional } in
# Value proposition — one file per claim. # Value proposition — one file per claim.
let value_prop_type = { let value_prop_type = {
id | String, # e.g. "vp-001-structure-that-remembers" id | String, # e.g. "vp-001-structure-that-remembers"
claim | String, # the nuclear one-liner claim | String, # the nuclear one-liner (canonical/English)
claim_i18n | vp_i18n | optional, # translations of claim; generator prefers this per-lang
audience_ids | Array String, # references audience_type.id (>=1 required, contract enforced) audience_ids | Array String, # references audience_type.id (>=1 required, contract enforced)
differentiator_ids | Array String | default = [], # QUÉ×QUIÉN seam: differentiators this claim binds (>=1 when Live) differentiator_ids | Array String | default = [], # QUÉ×QUIÉN seam: differentiators this claim binds (>=1 when Live)
problem_solved | String, # the problem this claim addresses problem_solved | String, # the problem this claim addresses (canonical/English)
problem_solved_i18n | vp_i18n | optional, # translations of problem_solved; generator prefers this per-lang
alternatives | Array String | default = [], # competition / status-quo options the user might consider alternatives | Array String | default = [], # competition / status-quo options the user might consider
unique_angle | String, # why no one else solves it this way unique_angle | String, # why no one else solves it this way
evidence_adrs | Array String | default = [], # ADR ids that materially back this claim (required when status='Live) evidence_adrs | Array String | default = [], # ADR ids that materially back this claim (required when status='Live)
@ -120,9 +130,31 @@ let competitor_type = {
# DifusionMechanism — one file per vehicle of difusión. Difusión is the # DifusionMechanism — one file per vehicle of difusión. Difusión is the
# category; a Campaign is one time-boxed coordination within it. A mechanism is # category; a Campaign is one time-boxed coordination within it. A mechanism is
# persistent (Web, DocsSite, McpServer, Repo) or an event (Content, Talk, # persistent (Web, DocsSite, McpServer, Repo) or an event (Content, Talk,
# Event). Admissible only with >=1 channel and >=1 thing it difunde — enforced # Event). The service-backed kinds (Newsletter, Forms, Community, Crm) reach an
# by make_difusion_mechanism. # audience through a self-hosted execution backend (a libre-wuji component)
let mechanism_kind_type = [| 'Web, 'DocsSite, 'McpServer, 'Repo, 'Content, 'Talk, 'Event |] in # instead of rendering from the ontology — for those, `backend` carries the
# junction to the running service. Admissible only with >=1 channel and >=1
# thing it difunde — enforced by make_difusion_mechanism.
let mechanism_kind_type =
[| 'Web, 'DocsSite, 'McpServer, 'Repo, 'Content, 'Talk, 'Event,
'Newsletter, 'Forms, 'Community, 'Crm |] in
# Execution backend for a difusión mechanism — the self-hosted service that
# actually runs the channel. Optional: presentation/content mechanisms (Web,
# DocsSite, Talk) render from the ontology and carry no backend. When present,
# `operational` is the coherence gate: a channel cannot declare status='Live
# while its backend is not configured and serving (enforced by
# make_difusion_mechanism). This is the drift-audit property applied to
# execution — the model refuses to claim a channel is live before it operates.
let mechanism_backend_type = {
service | String, # libre-wuji component name, e.g. "listmonk"
endpoint | String, # public URL of the running service
operational | Bool | default = false, # backend configured and serving — gates 'Live
admin_model | String | default = "", # how the admin / first account is obtained
secret_ref | String | optional, # SOPS basename: infra/libre-wuji/secrets/<ref>.sops.yaml
guard_ids | Array String | default = [], # cross-cutting guards before public capture (e.g. "cap")
blockers | Array String | default = [], # what stands between the current state and operational
} in
let difusion_mechanism_type = { let difusion_mechanism_type = {
id | String, id | String,
kind | mechanism_kind_type, kind | mechanism_kind_type,
@ -132,6 +164,7 @@ let difusion_mechanism_type = {
channels | Array String | default = [], # where it runs (>=1, contract enforced) channels | Array String | default = [], # where it runs (>=1, contract enforced)
value_prop_ids | Array String | default = [], value_prop_ids | Array String | default = [],
differentiator_ids | Array String | default = [], differentiator_ids | Array String | default = [],
backend | mechanism_backend_type | optional, # self-hosted executor (service-backed kinds)
launch_criteria | Array launch_check_type | default = [], launch_criteria | Array launch_check_type | default = [],
status | status_type, status | status_type,
retired_at | String | optional, retired_at | String | optional,
@ -231,6 +264,7 @@ let viability_path_type = {
Differentiator = differentiator_type, Differentiator = differentiator_type,
Competitor = competitor_type, Competitor = competitor_type,
DifusionMechanism = difusion_mechanism_type, DifusionMechanism = difusion_mechanism_type,
MechanismBackend = mechanism_backend_type,
Proof = proof_type, Proof = proof_type,
ViabilityPath = viability_path_type, ViabilityPath = viability_path_type,
} }

View file

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ontoref — canonical entry point. Shell alias: ontoref # ontoref — canonical entry point. Shell alias: ontoref
# Release: 0.1.7 # Release: 0.1.20
# Responsibilities: # Responsibilities:
# 1. Verify Nushell >= 0.110.0 is installed # 1. Verify Nushell >= 0.110.0 is installed
# 2. Detect actor (developer | admin | agent | ci) # 2. Detect actor (developer | admin | agent | ci)