feat: #[onto_mcp_tool] catalog, OCI credential vault layer, validate ADR-018 mode hierarchy
Some checks failed
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (push) Has been cancelled

ontoref-derive: #[onto_mcp_tool] attribute macro registers MCP tool unit-structs in
  the catalog at link time via inventory::submit!; annotated item is emitted unchanged,
  ToolBase/AsyncTool impls stay on the struct. All 34 tools migrated from manual wiring
  (net +5: ontoref_list_projects, ontoref_search, ontoref_describe,
  ontoref_list_ontology_extensions, ontoref_get_ontology_extension).

  validate modes (ADR-018): reads level_hierarchy from workflow.ncl and checks every
  .ncl mode for level declared, strategy declared, delegate chain coherent, compose
  extends valid. mode resolve <id> shows which hierarchy level handles a mode and why.
  --self-test generates synthetic fixtures in a temp dir for CI smoke-testing.

  validate run-cargo: two-step Cargo.toml resolution — workspace layout first
  (crates/<check.crate>/Cargo.toml), single-crate fallback by package name or repo
  basename. Lets the same ADR constraint shape apply to workspace and single-crate repos.

  ontology/schemas/manifest.ncl: registry_topology_type contract — multi-registry
  coordination, push targets, participant scopes, per-namespace capability.

  reflection/requirements/base.ncl: oras ≥1.2.0, cosign ≥2.0.0, sops ≥3.9.0, age
  ≥1.1.0, restic declared as Hard/Soft requirements with version_min, check_cmd, and
  install_hint (ADR-017 toolchain surface).

  ADR-019: per-file recipient routing for tenant isolation without multi-vault. Schema
  additions: sops.recipient_groups + sops.recipient_rules in ontoref-project.ncl.
  secrets-bootstrap generates .sops.yaml from project.ncl in declarative mode. Three
  new secrets-audit checks: recipient-routing-coherent, recipient-routing-coverage,
  no-multi-vault. Adoption templates: single-team/, multi-tenant/, agent-first/.
  Integration templates: domain-producer/, mode-producer/, mode-consumer/.

  UI: project_picker surfaces registry badge (⟳ participant) and vault badge
  (⛁ vault_id · N, green=declarative / amber=legacy) per project card. Expanded panel
  adds collapsible Registry section with namespace, endpoint, and push/pull capability.
  manage.html gains Runtime Services card — MCP and GraphQL toggleable without restart
  via HTMX POST /ui/manage/services/{service}/toggle.

  describe.nu: capabilities JSON includes registry_topology and vault_state per project.
  sync.nu: drift check extended to detect //! absence on newly registered crates.
  qa.ncl: six entries — credential-vault-best-practice (layered data-flow diagram),
  credential-vault-templates (paths A/B/C), credential-vault-troubleshooting (15 named
  errors), integration-what-and-why (ADR-042 OCI federation), integration-how-to-implement,
  integration-troubleshooting.

  on+re: core.ncl + manifest.ncl updated to reflect OCI, MCP, and mode-hierarchy nodes.
  Deleted stale presentation assets (2026-02 slides + voice notes).
This commit is contained in:
Jesús Pérez 2026-05-12 04:46:15 +01:00
parent 6721daf440
commit 82a358f18d
Signed by: jesus
GPG key ID: 9F243E355E0BC939
159 changed files with 50359 additions and 387 deletions

View file

@ -4,6 +4,7 @@
[build]
# Number of parallel jobs for compilation
jobs = 4
target-dir = "/Volumes/Devel/ontoref/target"
# Code generation backend
# codegen-backend = "llvm"

2
.gitignore vendored
View file

@ -2,6 +2,8 @@ CLAUDE.md
.claude
logs
logs-archive
.ncl-cache
data
utils/save*sh
.fastembed_cache
presentaciones

View file

@ -85,9 +85,16 @@ let d = import "../ontology/defaults/core.ncl" in
"adrs/adr-010-protocol-migration-system.ncl",
"adrs/adr-011-mode-guards-and-convergence.ncl",
"adrs/adr-012-domain-extension-system.ncl",
"adrs/adr-013-vcs-abstraction-layer.ncl",
"adrs/adr-014-runtime-service-toggles.ncl",
"adrs/adr-015-mcp-tool-inventory-auto-derive.ncl",
"adrs/adr-016-component-lift-out-pattern.ncl",
"adrs/adr-017-registry-credential-vault-model.ncl",
"adrs/adr-018-level-hierarchy-mode-resolution-strategy.ncl",
"adrs/adr-019-per-file-recipient-routing-tenant-isolation.ncl",
"CHANGELOG.md",
],
adrs = ["adr-001", "adr-002", "adr-003", "adr-004", "adr-005", "adr-006", "adr-007", "adr-008", "adr-009", "adr-010", "adr-011", "adr-012"],
adrs = ["adr-001", "adr-002", "adr-003", "adr-004", "adr-005", "adr-006", "adr-007", "adr-008", "adr-009", "adr-010", "adr-011", "adr-012", "adr-013", "adr-014", "adr-015", "adr-016", "adr-017", "adr-018", "adr-019"],
},
d.make_node {
@ -214,11 +221,12 @@ let d = import "../ontology/defaults/core.ncl" in
name = "Ontoref Daemon",
pole = 'Yang,
level = 'Practice,
description = "HTTP daemon for NCL export caching, file watching, actor registry, and MCP surface. Provides notification barrier, HTTP API (11 pages), MCP server (29 tools, stdio + streamable-HTTP), Q&A NCL persistence, quick-actions catalog, passive drift observation, unified auth/session management, per-file ontology version counters (GET /projects/{slug}/ontology/versions), and annotated API catalog (GET /api/catalog). API catalog populated at link time via #[onto_api] proc-macro + inventory — zero runtime overhead. Launched via ADR-004 NCL pipe bootstrap: nickel export config.ncl | ontoref-daemon.bin --config-stdin. Graph, search, and api_catalog UI pages carry browser-style panel navigation (back/forward history stack). File artifact paths open in external tabs: card.repo (Gitea source URL) for most files, card.docs (cargo docs) for .rs files — no inline file loading. card_repo/card_docs injected into Tera context from insert_brand_ctx; | safe filter required for URL values inside <script> blocks.",
description = "HTTP daemon for NCL export caching, file watching, actor registry, MCP surface, and GraphQL API. Provides notification barrier, HTTP API, MCP server (stdio + streamable-HTTP), GraphQL endpoint (async-graphql 8, Apollo Federation v2, WebSocket subscriptions), Q&A NCL persistence, quick-actions catalog, passive drift observation, unified auth/session model (ADR-005), runtime service toggles (ADR-014), and annotated API catalog (GET /api/catalog). API catalog populated at link time via #[onto_api] proc-macro + inventory — zero runtime overhead. Launched via ADR-004 NCL pipe bootstrap: nickel export config.ncl | ontoref-daemon.bin --config-stdin. Optional services (MCP, GraphQL) are feature-gated and runtime-toggleable via PUT /api/services/:service or UI manage page — changes take effect immediately without restart.",
invariant = false,
artifact_paths = [
"crates/ontoref-daemon/",
"crates/ontoref-daemon/src/api_catalog.rs",
"crates/ontoref-daemon/src/graphql/mod.rs",
"crates/ontoref-daemon/templates/pages/api_catalog.html",
"crates/ontoref-derive/",
"install/ontoref-daemon-boot",
@ -233,6 +241,8 @@ let d = import "../ontology/defaults/core.ncl" in
"crates/ontoref-daemon/src/ui/login.rs",
"crates/ontoref-daemon/src/ui/search_bookmarks_ncl.rs",
"justfiles/ci.just",
"justfiles/build.just",
"adrs/adr-014-runtime-service-toggles.ncl",
],
},
@ -479,6 +489,71 @@ let d = import "../ontology/defaults/core.ncl" in
],
},
d.make_node {
id = "mcp-surface",
name = "MCP Server Surface",
pole = 'Yang,
level = 'Practice,
description = "Model Context Protocol server exposing ontology state, reflection modes, ADRs, backlog, config, Q&A, and search bookmarks as structured tools. Two transports: stdio (for AI assistants that launch daemon as subprocess) and streamable-HTTP at POST /mcp (for remote MCP clients). Authentication uses the same unified session model as the REST API (ADR-005) — Bearer token from POST /sessions. Runtime-toggleable without restart (ADR-014). Connect: (1) stdio — run `ontoref-daemon --mcp-stdio`; tool list auto-discovered by the AI client. (2) streamable-HTTP — configure MCP client with url=http://127.0.0.1:7891/mcp, auth=Bearer <session-token>. Session token obtained via POST /sessions with project key. ONTOREF_TOKEN env var injected automatically by CLI. Available tools include: ontoref_help, ontoref_list_projects, ontoref_set_project, ontoref_search, ontoref_get, ontoref_get_node, ontoref_get_adr, ontoref_get_mode, ontoref_status, ontoref_describe, ontoref_guides, ontoref_list_adrs, ontoref_list_modes, ontoref_constraints, ontoref_backlog_list, ontoref_backlog, ontoref_action_list, ontoref_action_add, ontoref_validate_adrs, ontoref_validate, ontoref_impact, ontoref_qa_list, ontoref_qa_add, ontoref_bookmark_list, ontoref_bookmark_add, ontoref_api_catalog, ontoref_notify.",
artifact_paths = [
"crates/ontoref-daemon/src/mcp/mod.rs",
"crates/ontoref-daemon/src/api.rs",
],
},
d.make_node {
id = "graphql-surface",
name = "GraphQL API Surface",
pole = 'Yang,
level = 'Practice,
description = "GraphQL endpoint over the ontology graph, impact traversal, project state, backlog, ADRs, cross-project config comparison, and notification stream. Built with async-graphql 8 (Apollo Federation v2 entity resolution built-in). Endpoints: GET /graphql (GraphiQL IDE), POST /graphql (queries/mutations), GET /graphql/ws (WebSocket subscriptions). Auth: if graphql.read_token_hash is set in config.ncl, all endpoints require Authorization: Bearer <token>. Generate token: `just generate-graphql-token` — prints TOKEN and the Argon2id HASH to add to config.ncl. Pre-auth GraphiQL: GET /graphql?token=<token> — pre-injects the Bearer header and WebSocket connectionParam so the IDE works without manual header setup. Runtime-toggleable without restart (ADR-014). Key queries: ontologyNode(slug, id), impactGraph(slug, nodeId), project(slug), backlog(slug), adrs(slug), crossProjectSummaries, crossProjectConflicts. Key mutations: emitNotification, proposeBacklogStatus, ackNotifications, ackAllNotifications. Subscription: notifications(project, eventType) — streams live notifications over WebSocket.",
artifact_paths = [
"crates/ontoref-daemon/src/graphql/mod.rs",
"crates/ontoref-daemon/Cargo.toml",
"adrs/adr-014-runtime-service-toggles.ncl",
],
},
d.make_node {
id = "registry-credential-vault",
name = "Registry Credential Vault",
pole = 'Yang,
level = 'Practice,
description = "Per-project sops multi-recipient credential vault stored as OCI artifact in ZOT (ADR-017). Each actor role has its own age keypair — the sops DEK is encrypted separately per recipient, enabling revocation without key rotation. vault_key is decrypted into RESTIC_PASSWORD or KOPIA_PASSWORD for the operation duration only; never written to disk. The daemon is structurally excluded: it holds no .kage, cannot decrypt sops files even if it reads them, and cannot be used as a credential amplifier. DOCKER_CONFIG is isolated to a per-call tmpdir; no ambient ~/.docker/config.json is ever consulted. src-vault OCI artifact is cosign-signed on every push and verified before any pull — tamper-evident and substitution-resistant. Access logs are co-located in the vault artifact as an append-only jsonl layer. Two-level authorization gate: assert-actor-authorized validates ONTOREF_ACTOR→role binding + scope.bound_actor + scope.ops; assert-target-in-scope validates the OCI ref against scope.namespaces glob — both fire before any oras call, no cache hit bypasses. Vault lock OCI artifact (src-vault/<id>:lock) coordinates concurrent edits with TTL 60min and admin-only force-unlock auditable. Impact analysis on secrets-close diffs sops files since last snapshot and maps changes to RegistryEntry IDs before push. Per-file recipient routing via project.ncl::sops.recipient_groups + recipient_rules enables tenant/agent isolation in a single vault using sops creation_rules — admin operates as super-role, clientA/clientB/agents decrypt only their own files. cosign 2+ compatibility via vault.cosign.signing_config_path (Rekor-less) when tlog=false. cosign_password 4th field in access.sops.yaml enables non-interactive CI signing. 14/14 named-error tests cover the helper contract. 3 sops adoption templates (single-team, multi-tenant, agent-first) and 3 integration templates (domain-producer, mode-producer, mode-consumer) provide copy-paste starting points. FAQ entries with data-flow diagrams in reflection/qa.ncl.",
invariant = false,
artifact_paths = [
"justfiles/secrets.just",
"justfiles/_secrets_lib.sh",
"reflection/modules/vault.nu",
"reflection/modules/secrets.nu",
"reflection/requirements/base.ncl",
"reflection/tests/test_secrets.nu",
"install/resources/schemas/ontoref-project.ncl",
"install/resources/templates/sops/",
"install/resources/templates/integration/",
"reflection/migrations/0016-registry-credential-vault.ncl",
"reflection/qa.ncl",
"adrs/adr-019-per-file-recipient-routing-tenant-isolation.ncl",
],
adrs = ["adr-017", "adr-019"],
},
d.make_node {
id = "level-hierarchy-resolution",
name = "Level Hierarchy and Mode Resolution",
pole = 'Yang,
level = 'Practice,
description = "Three-level specialization hierarchy with per-mode resolution strategy (ADR-018). Level 1 = ontoref base (generic protocol), Level 2 = project domain (e.g. provisioning-domain), Level 3 = domain instance (e.g. libre-daoshi, libre-wuji). Level identity is declared in manifest.ncl (level.index: Base|Domain|Instance, level.name, level.parent). Each mode at Level 2+ declares strategy: Override (complete implementation, traversal stops), Delegate (no implementation here, traverse to parent), Merge (accumulate bottom-up, lower wins conflicts), Compose (partial inheritance via extends field). Implicit absence at Level 2+ is treated as Delegate with a Soft validation warning from ore validate modes. Strategy transitions are FSM events in state.ncl — moving from Delegate to Override is an observable, git-tracked architectural decision, not a silent refactor. ore mode resolve makes traversal observable: reports which level answered a mode invocation, the strategy applied, the source file, and the reason.",
invariant = false,
artifact_paths = [
"ontology/schemas/manifest.ncl",
"reflection/schema.ncl",
"reflection/migrations/0017-level-hierarchy-strategy.ncl",
"adrs/adr-018-level-hierarchy-mode-resolution-strategy.ncl",
],
adrs = ["adr-018"],
},
d.make_node {
id = "ci-pipelines",
name = "CI/CD Pipelines",
@ -493,6 +568,15 @@ let d = import "../ontology/defaults/core.ncl" in
],
},
d.make_node {
id = "read-tensions-first",
name = "Read Tensions First (ondaod)",
pole = 'Spiral,
level = 'Practice,
description = "Before architectural analysis or recommendation, read .ontology/core.ncl tensions; identify which the question engages; describe the synthesis state and direction of motion rather than collapse the Spiral by picking one pole. Default reasoning (human or agent) is Yang — choose, decide, recommend. This Practice surfaces that bias and asks the analyst to characterize the continuous flow first. Operationalizes the ontology-vs-reflection tension; balances against formalization-vs-adoption by choosing minimal structural enforcement (one Practice node, qa entry as canonical content, terse CLAUDE.md addendum) over heavier ADR schema constraints that would impose ceremony before adoption matures. Full procedure and forbidden patterns: reflection/qa.ncl::ontoref-dao-discipline.",
artifact_paths = [".ontology/core.ncl", "reflection/qa.ncl"],
},
],
edges = [
@ -639,5 +723,47 @@ let d = import "../ontology/defaults/core.ncl" in
{ from = "config-surface", to = "adopt-ontoref-tooling", kind = 'Complements, weight = 'Medium,
note = "Consumer projects adopting ontoref can annotate their config structs with #[derive(ConfigFields)] to participate in the coherence registry." },
# Registry Credential Vault edges
{ from = "registry-credential-vault", to = "domain-extension-system", kind = 'DependsOn, weight = 'High,
note = "Provisioning domain uses registry credentials for OCI artifact push/pull — the vault is what makes ontoref OCI discovery possible in domain projects." },
{ from = "registry-credential-vault", to = "adopt-ontoref-tooling", kind = 'Complements, weight = 'High,
note = "Migration 0016 propagates the credential vault pattern to consumer projects — adding sops config to project.ncl and credential_sops to registry entries." },
{ from = "registry-credential-vault", to = "protocol-not-runtime", kind = 'Complements, weight = 'Medium,
note = "Vault belongs to the reflection layer, not the protocol. Protocol declares the credential_sops pattern; vault.nu and secrets.nu implement it operationally." },
{ from = "registry-credential-vault", to = "unified-auth-model", kind = 'Complements, weight = 'Medium,
note = "Both use age encryption. Auth model secures the daemon API; credential vault secures OCI registry access. Independent mechanisms with the same cryptographic foundation." },
# Level Hierarchy and Mode Resolution edges
{ from = "level-hierarchy-resolution", to = "domain-extension-system", kind = 'Complements, weight = 'High,
note = "Domain extensions (Level 2) now formally declare their hierarchy position via level.index=Domain and mode strategy fields — making the implicit traversal explicit." },
{ from = "level-hierarchy-resolution", to = "manifest-self-description", kind = 'ManifestsIn, weight = 'High,
note = "level field in manifest_type is the ADR-018 mechanism for level identity — manifest.ncl is the source of truth for hierarchy position, queryable via ore describe project." },
{ from = "level-hierarchy-resolution", to = "reflection-modes", kind = 'Complements, weight = 'High,
note = "Mode strategy (Override/Delegate/Merge/Compose) is declared on _ModeBase — every mode file now carries its resolution contract alongside its DAG steps." },
{ from = "level-hierarchy-resolution", to = "protocol-migration-system", kind = 'Complements, weight = 'High,
note = "Migration 0017 propagates level identity and strategy declarations to all consumer projects — applied this session to provisioning, libre-daoshi, libre-wuji." },
{ from = "level-hierarchy-resolution", to = "dag-formalized", kind = 'ManifestsIn, weight = 'Medium,
note = "Level hierarchy is a DAG of specialization: each instance knows its domain parent, each domain knows the base. Traversal is graph traversal, not implicit lookup." },
# MCP and GraphQL surface edges
{ from = "mcp-surface", to = "ontoref-daemon", kind = 'DependsOn, weight = 'High,
note = "MCP server is compiled into ontoref-daemon; runtime-toggleable via ServiceFlags (ADR-014)." },
{ from = "mcp-surface", to = "unified-auth-model", kind = 'DependsOn, weight = 'High,
note = "MCP Bearer auth uses the same POST /sessions session token as the REST API." },
{ from = "graphql-surface", to = "ontoref-daemon", kind = 'DependsOn, weight = 'High,
note = "GraphQL endpoint compiled into ontoref-daemon; runtime-toggleable via ServiceFlags (ADR-014)." },
{ from = "graphql-surface", to = "unified-auth-model", kind = 'DependsOn, weight = 'High,
note = "GraphQL auth uses Argon2id token from config.graphql.read_token_hash — separate from project keys." },
{ from = "graphql-surface", to = "mcp-surface", kind = 'Complements, weight = 'Medium,
note = "GraphQL and MCP are complementary surfaces: MCP for structured tool invocation, GraphQL for ad-hoc graph queries and subscriptions." },
# Dao discipline — surface the read-tensions-first Practice from the named tensions it operationalizes
{ from = "ontology-vs-reflection", to = "read-tensions-first", kind = 'ManifestsIn, weight = 'High,
note = "The named Spiral between contract definition and operational mechanism is the primary tension this Practice asks analysts to engage. Yang reasoning (pick a side) collapses it; this Practice asks for synthesis-state characterization." },
{ from = "formalization-vs-adoption", to = "read-tensions-first", kind = 'ManifestsIn, weight = 'Medium,
note = "This Practice is itself in tension with adoption friction — minimal structural enforcement is chosen so the discipline doesn't become ceremony. Heavier mechanisms (ADR schema constraints, schema pole field) deferred until adoption proves the discipline lands." },
{ from = "read-tensions-first", to = "dag-formalized", kind = 'Complements, weight = 'Medium,
note = "If protocol knowledge is DAG-formalized then analyses ABOUT the protocol must reference the DAG nodes — specifically the named Tension nodes — rather than reason from outside it. read-tensions-first extends DAG formalization from project state to project reasoning." },
],
}

View file

@ -3,6 +3,7 @@ let m = import "../ontology/defaults/manifest.ncl" in
m.make_manifest {
project = "ontoref",
repo_kind = 'Framework,
level = { index = 'Base, name = "ontoref-base" },
description = "Protocol specification and tooling layer for structured self-knowledge in software projects. Provides schemas, Nushell automation, and Rust crates so projects can describe what they are, record architectural decisions, track operational state, and execute formalized procedures as typed, queryable artifacts.",
content_assets = [
@ -265,19 +266,26 @@ m.make_manifest {
},
m.make_capability {
id = "daemon-api",
name = "Daemon HTTP + MCP Surface",
summary = "HTTP UI (11 pages), 33 MCP tools, annotated API catalog, SSE notifications, per-file versioning, and NCL export cache.",
rationale = "Agents and developers need a queryable interface to ontology state without spawning nickel on every request. The NCL export cache reduces full-sync from ~2m42s to <30s. MCP tools give agents structured access to every capability without screen-scraping the CLI. ADR-002 records the architectural decision to extract the daemon; ADR-007 covers the #[onto_api] catalog pattern; ADR-008 covers the config override layer.",
how = "crates/ontoref-daemon uses axum for HTTP. #[onto_api(...)] proc-macro + inventory::submit! registers every route at link time; GET /api/catalog aggregates via inventory::collect!. NclCache (DashMap<PathBuf, CachedExport>) keyed on path + mtime. File watcher (notify) triggers cache invalidation and drift detection after 15s debounce. MCP over stdio and streamable-HTTP.",
name = "Daemon HTTP + MCP + GraphQL Surface",
summary = "HTTP UI, annotated REST API catalog, MCP server (stdio + streamable-HTTP), GraphQL endpoint (queries/mutations/subscriptions), SSE notifications, per-file versioning, and NCL export cache.",
rationale = "Agents and developers need a queryable interface to ontology state without spawning nickel on every request. The NCL export cache reduces full-sync from ~2m42s to <30s. MCP gives agents structured tool invocation. GraphQL gives ad-hoc graph traversal and live subscriptions. Both surfaces are optional features runtime-toggleable without restart.",
how = "START daemon: `just build-daemon` (compiles with db,nats,ui,mcp,graphql features) then `ontoref-daemon-boot` or `ontoref-daemon-boot --dry-run`. Default bind: http://127.0.0.1:7891. --- MCP (stdio): run `ontoref-daemon --mcp-stdio`; the AI client auto-discovers the tool list. MCP (HTTP): configure client with url=http://127.0.0.1:7891/mcp; auth=Bearer <session-token>. Get session token: POST /sessions {\"project\":\"<slug>\",\"key\":\"<project-key>\"}. CLI auto-injects ONTOREF_TOKEN. --- GRAPHQL: endpoint is http://127.0.0.1:7891/graphql (POST for queries/mutations, GET for GraphiQL IDE, /graphql/ws for subscriptions). If auth is enabled (graphql.read_token_hash in config.ncl), all requests require Authorization: Bearer <token>. Generate token: `just generate-graphql-token` prints TOKEN + HASH; add HASH to config.ncl graphql.read_token_hash. GraphiQL with pre-auth: GET /graphql?token=<TOKEN> — injects Bearer header and WS connectionParam automatically. --- TOGGLE services: PUT /api/services/mcp {\"enabled\":false} (daemon admin Bearer) or UI manage page toggle buttons. --- AUTH: all surfaces use the same session token from POST /sessions. GraphQL has a separate read_token_hash (simpler for read-only agent access without a project key).",
artifacts = [
"crates/ontoref-daemon/",
"crates/ontoref-daemon/src/mcp/mod.rs",
"crates/ontoref-daemon/src/graphql/mod.rs",
"GET /api/catalog",
"GET /projects/{slug}/ontology",
"GET /projects/{slug}/config/coherence",
"MCP: ontoref_guides, ontoref_api_catalog, ontoref_validate, ontoref_impact",
"GET /services",
"PUT /services/:service",
"POST /graphql",
"GET /graphql (GraphiQL IDE)",
"GET /graphql/ws (WebSocket subscriptions)",
"POST /mcp (streamable-HTTP)",
"MCP stdio: ontoref-daemon --mcp-stdio",
"justfiles/build.just (generate-graphql-token, generate-admin-key)",
],
adrs = ["adr-002", "adr-004", "adr-007", "adr-008"],
nodes = ["ontoref-daemon", "api-catalog-surface", "config-surface", "unified-auth-model"],
adrs = ["adr-002", "adr-004", "adr-005", "adr-007", "adr-008", "adr-014"],
nodes = ["ontoref-daemon", "mcp-surface", "graphql-surface", "api-catalog-surface", "config-surface", "unified-auth-model"],
},
m.make_capability {
id = "reflection-modes",
@ -506,6 +514,46 @@ m.make_manifest {
impact = "ontoref-daemon (db/nats features) and ontoref-reflection (nats feature) cannot build. Build with --no-default-features to work without it.",
provision = "git clone at ../../../stratumiops relative to this repo root.",
},
m.make_requirement {
id = "oci-registry",
name = "OCI Registry (ZOT or compatible)",
env = 'Both,
kind = 'Service,
version = "",
required = true,
impact = "Without a reachable OCI registry, ontoref cannot push or pull domain artifacts, mode artifacts, or src-vault credential archives. OCI discovery — the mechanism by which actors find published modes and domains — is fully blocked. Credential distribution (ADR-017) is also blocked: src-vault:latest cannot be fetched, so no actor can resolve registry credentials.",
provision = "ZOT recommended: https://zotregistry.dev — docker run ghcr.io/project-zot/zot-linux-amd64. Compatible with any OCI Distribution Spec v1.1 registry. Endpoint declared in registry_provides.registries[].endpoint in .ontology/manifest.ncl.",
},
m.make_requirement {
id = "oras",
name = "oras CLI",
env = 'Both,
kind = 'Tool,
version = "1.2.0",
required = true,
impact = "All OCI artifact operations fail: domain-push, domain-pull, mode-push, mode-pull, src-vault push/pull. OCI discovery and credential distribution both blocked.",
provision = "brew install oras # or: https://oras.land/docs/installation",
},
m.make_requirement {
id = "cosign",
name = "cosign",
env = 'Both,
kind = 'Tool,
version = "2.0.0",
required = true,
impact = "src-vault OCI artifacts cannot be signed on push or verified on pull. An unsigned vault artifact cannot be trusted — a substituted artifact with malicious credentials would be undetectable.",
provision = "brew install cosign # or: https://docs.sigstore.dev/cosign/system_config/installation/",
},
m.make_requirement {
id = "sops",
name = "sops",
env = 'Both,
kind = 'Tool,
version = "3.8.0",
required = true,
impact = "Registry credential resolution blocked. access.sops.yaml cannot be decrypted — oras calls have no credentials. All registry operations requiring auth fail.",
provision = "brew install sops # or: https://github.com/getsops/sops/releases",
},
],
critical_deps = [
@ -535,6 +583,30 @@ m.make_manifest {
},
],
# ADR-017 — registry credential vault declaration. credential_sops paths are
# relative to the synced src-vault root at ~/.config/ontoref/vaults/ontoref/src-vault/.
# This entry is the canonical shape that consumer manifests follow via migration 0016.
registry_provides = m.make_registry_provides {
participant = "ontoref",
registries = m.make_registries_config {
default = "primary",
registries = [
m.make_registry_entry {
id = "primary",
endpoint = "reg.librecloud.online",
role = 'primary,
tls = true,
namespaces = {
own = ["domains/ontoref/", "modes/ontoref/"],
prefixes = ["domains/ontoref/", "modes/ontoref/"],
},
credential_sops = "registry/ro.sops.yaml", # RO — pull/list (cdci, ontoref, agent)
credential_sops_rw = "registry/rw.sops.yaml", # RW — push (admin, developer)
},
],
},
},
layers = [
m.make_layer {
id = "protocol",

File diff suppressed because one or more lines are too long

View file

@ -8,4 +8,24 @@ s.make_project {
"/Users/Akasha/Development/ontoref/ontology",
],
keys = [],
# ADR-017 — registry credential vault. ontoref participates as a publisher in the
# libre-wuji registry under domains/ontoref/ and modes/ontoref/. Until the vault is
# bootstrapped (`./ontoref secrets bootstrap`), Layer 2 operations error nominally.
# This block is the canonical reference shape that consumer projects copy via
# migration 0016.
sops = {
enabled = true,
vault_id = "ontoref",
vault_backend = 'restic,
registry_endpoint = "reg.librecloud.online",
actor_key_bindings = {
developer = "developer",
ci = "cdci",
agent = "ontoref",
admin = "admin",
},
# master_key_path resolved from ~/.config/ontoref/config.ncl::vault.master_key_path.
# Override here only if this project requires a different .kage than the global default.
},
}

View file

@ -7,6 +7,133 @@ ADRs referenced below live in `adrs/` as typed Nickel records.
## [Unreleased]
### MCP tool catalog via proc-macro, validate ADR-018 mode hierarchy, UI vault/registry context
**Session 2026-05-12**
**`#[onto_mcp_tool]` proc-macro — link-time MCP catalog registration (`ontoref-derive`)**
- New attribute macro that registers an MCP tool unit-struct in the catalog at link time via
`inventory::submit!(McpToolEntry{...})`. The annotated item is emitted unchanged — `ToolBase`
and `AsyncTool` impls remain on the struct. Required keys: `name`, `description`, `input_schema`.
- All 34 MCP tools migrated from manual `catalog()` wiring to `#[onto_mcp_tool]`. The
`catalog()` function is now generated automatically by `inventory::collect!`.
- Tool count: 29 → 34 (net: `ontoref_list_projects`, `ontoref_search`, `ontoref_describe`,
`ontoref_list_ontology_extensions`, `ontoref_get_ontology_extension` added).
**`validate modes` — ADR-018 level hierarchy and strategy compliance (`reflection/modules/validate.nu`)**
- `validate modes [--check]` — reads `reflection/defaults/workflow.ncl::level_hierarchy` and
checks every `.ncl` mode file against the declared levels: level declared, strategy declared,
delegate chain coherent, compose `extends` references valid.
- `mode resolve <id>` — prints which hierarchy level handles the given mode id and why.
- `validate modes --self-test` — generates synthetic mode fixtures in a temp dir and runs checks
against them; fast CI-safe smoke test for the validator itself.
**`validate run-cargo` — two-step Cargo.toml resolution**
- Resolves `Cargo.toml` in workspace layout first (`crates/<check.crate>/Cargo.toml`), then
falls back to single-crate root when `[package].name` matches or `check.crate` equals the
repo basename. Lets the same ADR constraint shape apply to workspace repos (stratumiops) and
single-crate projects without per-repo special cases.
**UI — vault and registry context in project_picker**
- Each project card in the project picker now surfaces OCI state inline:
- Registry badge (`⟳ <participant>`) when `manifest.ncl::registry_provides` is declared.
- Vault badge (`⛁ <vault_id> · N`) coloured green (declarative) or amber (legacy) when
`project.ncl::sops.enabled` is true.
- Expanded project details panel: collapsible **Registry** section listing namespaces, endpoint,
and push/pull capability per namespace.
**UI — runtime service toggles in manage page**
- `manage.html` gains a **Runtime Services** card: MCP and GraphQL can be toggled on/off via
HTMX `POST /ui/manage/services/{service}/toggle` without restarting the daemon.
- `insert_mcp_ctx` helper injects MCP and GraphQL runtime state + daemon version into every
Tera template context that needs it.
**Manifest schema — registry topology contract (`ontology/schemas/manifest.ncl`)**
- New `registry_topology_type` contract: declares multi-registry coordination (which registries
the project participates in, push targets, participant scopes, and per-namespace capability).
- `manifest_type` gains optional `registry_topology` field.
**Requirements — OCI tooling declared in `reflection/requirements/base.ncl`**
- `oras ≥ 1.2.0` (Hard), `cosign ≥ 2.0.0` (Hard), `sops ≥ 3.9.0` (Hard), `age ≥ 1.1.0`
(Hard), `restic` (Soft) added as named requirements with `check_cmd`, `version_extract`,
and `install_hint`. Surfaces in `describe requirements` and `ontoref_guides`.
**describe.nu + sync.nu expansion**
- `describe capabilities` JSON output now includes `registry_topology` and `vault_state` per
project when declared.
- `sync diff --docs` drift check extended to detect `//!` absence on new crates added this
session.
---
### Per-file recipient routing for tenant isolation (ADR-019)
**Session 2026-05-03** — credential vault model extended to support per-file
recipient routing, closing the multi-tenant gap without multi-vault.
**Schema additions** (`install/resources/schemas/ontoref-project.ncl`)
- `sops.recipient_groups` (record of group → list of age public keys, optional)
- `sops.recipient_rules` (array of `{ path, groups }` records, optional)
**Bootstrap behaviour**
- When `recipient_rules` declared, `secrets-bootstrap` generates `<vault_dir>/.sops.yaml`
with sops `creation_rules` mapping each path_regex to the union of declared groups.
- Default `registry/ro+rw.sops.yaml` pair is skipped in declarative mode — the
operator defines files matching their tenancy scheme (e.g. `clientA-ro.sops.yaml`).
- Legacy mode (no `recipient_rules`) keeps `SOPS_AGE_RECIPIENTS` env-var path.
**Operational locks**
- `secrets-add-key` and `secrets-remove-key` error in declarative mode and
direct the operator to edit `project.ncl::sops.recipient_groups` plus `secrets-rekey`.
- `secrets-rekey` regenerates `.sops.yaml` from `project.ncl` then runs `sops updatekeys`
on every `*.sops.yaml` in the vault.
**Adoption templates** (`install/resources/templates/sops/`)
- `single-team/` — one team, no tenant separation (legacy / Path A)
- `multi-tenant/` — clientA/clientB groups + path-routed credentials
- `agent-first/` — admin/developer/agents with `agent-readonly.sops.yaml` for AI access
**Integration templates** (`install/resources/templates/integration/`)
- `domain-producer/` — contract.ncl + example.json + manifest.ncl scaffold
- `mode-producer/` — provisioning.ncl + domains.lock.ncl + manifest.ncl scaffold
- `mode-consumer/` — cabling.ncl scaffold for binding pulled modes to workspace values
**Audit checks** (`secrets-audit`) — three new checks for ADR-019 constraints:
- `recipient-routing-coherent` — every group referenced by a rule must be declared;
every rule must resolve to ≥ 1 recipient
- `recipient-routing-coverage` — every `*.sops.yaml` in the vault must match at
least one declared rule
- `no-multi-vault` — rejects `sops.vaults =` declaration (multi-vault not implemented)
**FAQ entries** (`reflection/qa.ncl`) — six entries with diagrams:
- `credential-vault-best-practice` — layered model with data-flow diagram
- `credential-vault-templates` — three adoption paths (A/B/C)
- `credential-vault-troubleshooting` — 15 named errors and recovery steps
- `integration-what-and-why` — federated OCI artifacts (ADR-042) overview with diagram
- `integration-how-to-implement` — producer / consumer / both, with templates
- `integration-troubleshooting` — push/pull/invoke common failures
**ADR-019: Per-File Recipient Routing for Tenant Isolation in lieu of Multi-Vault**
Documents the decision, alternatives (multi-vault explicitly rejected), and 6
machine-checkable constraints. Each constraint is wired to either `secrets-audit`
(NuCmd), `Grep`, or `FileExists` — verifiable from CLI without a custom validator.
**Tests** — `reflection/tests/test_secrets.nu` covers 14/14 named errors of the
helper contract end-to-end (`sops-decrypt-failed` excluded — requires real age
keypair plumbing).
**Bug fixes**
- `_find-capabilities-ncl`: glob malformation when walking up to filesystem root
- cosign `--tlog-upload=false` deprecated in cosign 2+ → use `--signing-config`
with `rekorTlogUrls`/`rekorTlogConfig` stripped
- cosign `sign` needed `DOCKER_CONFIG` to fetch manifest before signing
- Cache hit in `domain-pull` / `mode-pull` bypassed authorization gate (reordered)
- Per-file `(...)` interpolation traps in nu strings inside error messages
- `add-key` / `remove-key` only mutated `access.sops.yaml` instead of all sops
files in the vault
---
### VCS abstraction layer and agent workspace orchestration
#### `'Framework` RepoKind

385
Cargo.lock generated
View file

@ -197,6 +197,53 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "askama"
version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b8246bcbf8eb97abef10c2d92166449680d41d55c0fc6978a91dec2e3619608"
dependencies = [
"askama_macros",
"itoa",
"percent-encoding",
"serde",
"serde_json",
]
[[package]]
name = "askama_derive"
version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f9670bc84a28bb3da91821ef74226949ab63f1265aff7c751634f1dd0e6f97c"
dependencies = [
"askama_parser",
"memchr",
"proc-macro2",
"quote",
"rustc-hash",
"syn 2.0.117",
]
[[package]]
name = "askama_macros"
version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0756b45480437dded0565dfc568af62ccce146fb6cfe902e808ba86e445f44f"
dependencies = [
"askama_derive",
]
[[package]]
name = "askama_parser"
version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0af3691ba3af77949c0b5a3925444b85cb58a0184cc7fec16c68ba2e7be868"
dependencies = [
"rustc-hash",
"unicode-ident",
"winnow 1.0.2",
]
[[package]]
name = "async-channel"
version = "2.5.0"
@ -210,10 +257,101 @@ dependencies = [
]
[[package]]
name = "async-nats"
version = "0.46.0"
name = "async-graphql"
version = "8.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df5af9ebfb0a14481d3eaf6101e6391261e4f30d25b26a7635ade8a39482ded0"
checksum = "34a42d615ca2f3557491397f6ed2169dd38fbce9bf5352a5a9f17216cdfeb40f"
dependencies = [
"askama",
"async-graphql-derive",
"async-graphql-parser",
"async-graphql-value",
"async-trait",
"asynk-strim",
"base64",
"blocking",
"bytes",
"futures-util",
"http",
"indexmap",
"mime",
"multer",
"num-traits",
"pin-project-lite",
"regex",
"rustc-hash",
"serde",
"serde_json",
"serde_urlencoded",
"static_assertions_next",
"tempfile",
"thiserror 2.0.18",
"tokio",
]
[[package]]
name = "async-graphql-axum"
version = "8.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42398787bd4bf3257c294d6743b228e0b437762cbc686cff1ab4ccab014ed30f"
dependencies = [
"async-graphql",
"axum",
"bytes",
"futures-util",
"serde_json",
"tokio",
"tokio-stream",
"tokio-util",
"tower-service",
]
[[package]]
name = "async-graphql-derive"
version = "8.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdaa2cb6cc70518033cf543e6b735ed03daa71c1ec81eac5ad09fa05e61faa9f"
dependencies = [
"async-graphql-parser",
"darling",
"heck",
"proc-macro-crate",
"proc-macro2",
"quote",
"strum",
"syn 2.0.117",
"thiserror 2.0.18",
]
[[package]]
name = "async-graphql-parser"
version = "8.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae405016a111a09cd484a973024dcc139ac398a89cb1f4602bebab817298276"
dependencies = [
"async-graphql-value",
"pest",
"serde",
"serde_json",
]
[[package]]
name = "async-graphql-value"
version = "8.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15eb1573843563b4518cc43001002512c206d119974b17476a3cbf62d9a1ae61"
dependencies = [
"bytes",
"indexmap",
"serde",
"serde_json",
]
[[package]]
name = "async-nats"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31811585c7c5bc2f60f8b80d5a6b0f737115611dac47567d7f7d94562ebb180b"
dependencies = [
"base64",
"bytes",
@ -221,20 +359,19 @@ dependencies = [
"memchr",
"nkeys",
"nuid",
"once_cell",
"pin-project",
"portable-atomic",
"rand 0.8.5",
"rand 0.10.1",
"regex",
"ring",
"rustls-native-certs 0.7.3",
"rustls-native-certs",
"rustls-pki-types",
"rustls-webpki 0.102.8",
"rustls-webpki",
"serde",
"serde_json",
"serde_nanos",
"serde_repr",
"thiserror 1.0.69",
"thiserror 2.0.18",
"time",
"tokio",
"tokio-rustls",
@ -268,6 +405,12 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.89"
@ -279,6 +422,16 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "asynk-strim"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52697735bdaac441a29391a9e97102c74c6ef0f9b60a40cf109b1b404e29d2f6"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "atomic-polyfill"
version = "1.0.3"
@ -330,6 +483,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"axum-core",
"base64",
"bytes",
"form_urlencoded",
"futures-util",
@ -348,8 +502,10 @@ dependencies = [
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sha1",
"sync_wrapper",
"tokio",
"tokio-tungstenite",
"tower",
"tower-layer",
"tower-service",
@ -504,6 +660,19 @@ dependencies = [
"generic-array 0.14.7",
]
[[package]]
name = "blocking"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]]
name = "blowfish"
version = "0.9.1"
@ -1456,6 +1625,16 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-lite"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "futures-macro"
version = "0.3.32"
@ -1572,7 +1751,7 @@ dependencies = [
"i_overlay",
"log",
"num-traits",
"rand 0.8.5",
"rand 0.8.6",
"robust",
"rstar 0.12.2",
"serde",
@ -2396,7 +2575,7 @@ dependencies = [
"p256",
"p384",
"pem",
"rand 0.8.5",
"rand 0.8.6",
"rsa",
"serde",
"serde_json",
@ -2606,9 +2785,9 @@ dependencies = [
[[package]]
name = "mio"
version = "1.1.1"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"log",
@ -2616,6 +2795,23 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "multer"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
dependencies = [
"bytes",
"encoding_rs",
"futures-util",
"http",
"httparse",
"memchr",
"mime",
"spin",
"version_check",
]
[[package]]
name = "ndarray"
version = "0.17.2"
@ -2643,7 +2839,7 @@ dependencies = [
"noisy_float",
"num-integer",
"num-traits",
"rand 0.8.5",
"rand 0.8.6",
]
[[package]]
@ -2672,7 +2868,7 @@ dependencies = [
"ed25519-dalek",
"getrandom 0.2.17",
"log",
"rand 0.8.5",
"rand 0.8.6",
"signatory",
]
@ -2736,7 +2932,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83"
dependencies = [
"rand 0.8.5",
"rand 0.8.6",
]
[[package]]
@ -2760,7 +2956,7 @@ dependencies = [
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"rand 0.8.6",
"smallvec",
"zeroize",
]
@ -2881,6 +3077,8 @@ version = "0.1.0"
dependencies = [
"anyhow",
"argon2",
"async-graphql",
"async-graphql-axum",
"axum",
"axum-server",
"bytes",
@ -2958,12 +3156,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-probe"
version = "0.2.1"
@ -3189,7 +3381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared 0.11.3",
"rand 0.8.5",
"rand 0.8.6",
]
[[package]]
@ -3280,6 +3472,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]]
name = "pkcs1"
version = "0.7.5"
@ -3499,7 +3702,7 @@ dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand 0.9.2",
"rand 0.9.4",
"ring",
"rustc-hash",
"rustls",
@ -3565,9 +3768,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha 0.3.1",
@ -3576,9 +3779,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.2"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
@ -3586,9 +3789,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20",
"getrandom 0.4.2",
@ -3884,7 +4087,7 @@ dependencies = [
"http-body-util",
"pastey",
"pin-project-lite",
"rand 0.10.0",
"rand 0.10.1",
"rmcp-macros",
"schemars",
"serde",
@ -4078,7 +4281,7 @@ dependencies = [
"borsh",
"bytes",
"num-traits",
"rand 0.8.5",
"rand 0.8.6",
"rkyv",
"serde",
"serde_json",
@ -4123,34 +4326,21 @@ dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.103.9",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
dependencies = [
"openssl-probe 0.1.6",
"rustls-pemfile",
"rustls-pki-types",
"schannel",
"security-framework 2.11.1",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
dependencies = [
"openssl-probe 0.2.1",
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework 3.7.0",
"security-framework",
]
[[package]]
@ -4184,10 +4374,10 @@ dependencies = [
"log",
"once_cell",
"rustls",
"rustls-native-certs 0.8.3",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki 0.103.9",
"security-framework 3.7.0",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
@ -4201,19 +4391,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.102.8"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"rustls-pki-types",
"untrusted 0.9.0",
]
[[package]]
name = "rustls-webpki"
version = "0.103.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"aws-lc-rs",
"ring",
@ -4324,19 +4504,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.11.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework"
version = "3.7.0"
@ -4670,6 +4837,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_assertions_next"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766"
[[package]]
name = "storekey"
version = "0.11.0"
@ -4765,6 +4938,27 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "subtle"
version = "2.6.1"
@ -4862,7 +5056,7 @@ dependencies = [
"pin-project-lite",
"quick_cache",
"radix_trie",
"rand 0.8.5",
"rand 0.8.6",
"rayon",
"reblessive",
"regex",
@ -4937,7 +5131,7 @@ dependencies = [
"hex",
"http",
"papaya",
"rand 0.8.5",
"rand 0.8.6",
"regex",
"rstest",
"rust_decimal",
@ -5103,7 +5297,7 @@ dependencies = [
"percent-encoding",
"pest",
"pest_derive",
"rand 0.8.5",
"rand 0.8.6",
"regex",
"serde",
"serde_json",
@ -5218,9 +5412,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.50.0"
version = "1.52.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"
dependencies = [
"bytes",
"libc",
@ -5235,9 +5429,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.6.1"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
@ -5338,7 +5532,7 @@ dependencies = [
"futures-sink",
"http",
"httparse",
"rand 0.8.5",
"rand 0.8.6",
"ring",
"rustls-pki-types",
"tokio",
@ -5388,7 +5582,7 @@ dependencies = [
"serde_spanned",
"toml_datetime 0.6.11",
"toml_write",
"winnow",
"winnow 0.7.15",
]
[[package]]
@ -5400,7 +5594,7 @@ dependencies = [
"indexmap",
"toml_datetime 1.0.0+spec-1.1.0",
"toml_parser",
"winnow",
"winnow 0.7.15",
]
[[package]]
@ -5409,7 +5603,7 @@ version = "1.0.9+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
dependencies = [
"winnow",
"winnow 0.7.15",
]
[[package]]
@ -5617,7 +5811,7 @@ dependencies = [
"http",
"httparse",
"log",
"rand 0.9.2",
"rand 0.9.4",
"rustls",
"rustls-pki-types",
"sha1",
@ -5644,7 +5838,7 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe"
dependencies = [
"rand 0.9.2",
"rand 0.9.4",
"serde",
"web-time",
]
@ -5729,9 +5923,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.22.0"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
dependencies = [
"getrandom 0.4.2",
"js-sys",
@ -6405,6 +6599,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"

View file

@ -7,7 +7,7 @@ edition = "2021"
license = "MIT OR Apache-2.0"
[workspace.dependencies]
tokio = { version = "1.50", features = ["full"] }
tokio = { version = "1.52", features = ["full"] }
async-trait = "0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
@ -23,7 +23,9 @@ tempfile = "3"
tokio-test = "0.4"
axum = { version = "0.8", features = ["json"] }
tower-http = { version = "0.6", features = ["cors", "trace", "fs"] }
notify = { version = "8.2", default-features = false, features = ["macos_fsevent"] }
notify = { version = "8.2", default-features = false, features = [
"macos_fsevent",
] }
dashmap = "6.1"
clap = { version = "4", features = ["derive"] }
hostname = "0.4"

32
Dockerfile Normal file
View file

@ -0,0 +1,32 @@
ARG IMAGE_BASE=reg.librecloud.online/lamina
ARG RUST_VERSION=1.89
# Pull nickel binary from the lamina nickel layer — named stage makes ARG expansion reliable
FROM ${IMAGE_BASE}/nickel:${RUST_VERSION} AS nickel-layer
# Builder: lamina rust layer provides cargo, rustc, sccache, cargo-chef
FROM ${IMAGE_BASE}/rust:${RUST_VERSION} AS builder
WORKDIR /build
ENV CARGO_TARGET_DIR=/build/target
# stratumiops source injected via --build-context stratumiops=../stratumiops
# Resolves to /stratumiops — matches ../../../stratumiops relative path from crates/ontoref-daemon/
COPY --from=stratumiops . /stratumiops
COPY Cargo.toml Cargo.lock ./
COPY crates ./crates
RUN cargo build --release --bin ontoref-daemon
# Runtime: minimal base + nickel (for NCL export at runtime) + daemon binary
FROM alpine:3.21
RUN apk add --no-cache ca-certificates
COPY --from=nickel-layer /usr/local/bin/nickel /usr/local/bin/nickel
COPY --from=builder /build/target/release/ontoref-daemon /usr/local/bin/ontoref-daemon
EXPOSE 7891
ENTRYPOINT ["ontoref-daemon"]

2
JUST.md Normal file
View file

@ -0,0 +1,2 @@
Hay que tener en cuenta que existe una descoordinación real y absoluta entre los Justfiles de los proyectos.
Esto debería de ser armonizado o sincronizado: elementos comunes en un template o un modo específico ampliable según cada proyecto.

View file

@ -36,8 +36,8 @@ crates/ Rust implementation — typed struct loaders and mode executo
| --- | --- |
| `ontoref-ontology` | `.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. Zero deps. |
| `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-daemon` | HTTP UI (11 pages), actor registry, notification barrier, MCP (33 tools), search engine, search bookmarks, SurrealDB, NCL export cache, per-file ontology versioning, annotated API catalog, Agent Task Composer. |
| `ontoref-derive` | Proc-macro crate. `#[onto_api(...)]` annotates HTTP handlers — `description` is optional when a `///` doc comment exists (first line used as fallback). `#[derive(OntologyNode)]` + `#[onto(id, name, paths, description, adrs)]` auto-registers nodes via `inventory::submit!` at link time, merged into `Core` by `merge_contributors()`. `#[derive(ConfigFields)]` + `#[config_section(id, ncl_file)]` registers config struct fields. All three aggregate via `inventory::collect!`. |
| `ontoref-daemon` | HTTP UI (11 pages), actor registry, notification barrier, MCP (34 tools), search engine, search bookmarks, SurrealDB, NCL export cache, per-file ontology versioning, annotated API catalog, Agent Task Composer. |
| `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 in the catalog 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!` at link time, merged into `Core` by `merge_contributors()`. `#[derive(ConfigFields)]` + `#[config_section(id, ncl_file)]` registers config struct fields. All four aggregate via `inventory::collect!`. |
`ontoref-daemon` caches `nickel export` results (keyed by path + mtime), reducing full sync
scans from ~2m42s to <30s. The daemon is always optional every module falls back to direct
@ -55,7 +55,8 @@ automatically.
**Q&A Knowledge Store** — accumulated Q&A entries persist to `reflection/qa.ncl` (typed NCL,
git-versioned). Not localStorage. Any actor — developer, agent, CI — reads the same store.
**MCP Server** — 29 tools over stdio and streamable-HTTP. Categories: discovery, retrieval, project
**MCP Server** — 34 tools over stdio and streamable-HTTP, all registered at link time via
`#[onto_mcp_tool]` (no manual catalog wiring). Categories: discovery, retrieval, project
state, ontology, backlog, validation, Q&A, bookmarks, API surface. Representative subset:
| Tool | What it does |
@ -167,6 +168,19 @@ work as both `ore prov <cmd>` and standalone `prov <cmd>`. `ore help` and `descr
surface the active domain automatically. New domains require only three files — no changes to the Nu
dispatcher. ([ADR-012](adrs/adr-012-domain-extension-system.ncl))
**Mode Hierarchy Validation** — `validate modes [--check]` reads `reflection/defaults/workflow.ncl::level_hierarchy`
and checks every `.ncl` mode file for: level declared, strategy declared, delegate chain coherent,
compose `extends` references valid. `mode resolve <id>` prints which hierarchy level handles the
given mode and why. `validate modes --self-test` generates synthetic fixtures in a temp dir for
fast CI smoke-testing of the validator itself. ([ADR-018](adrs/adr-018-workflow-layer-model.ncl))
**Project Picker — vault and registry badges** — each project card surfaces OCI state inline:
registry participant badge (`⟳ <participant>`) when `registry_provides` is declared; vault badge
(`⛁ <vault_id> · N`) coloured green (declarative) or amber (legacy) when `sops.enabled` is true.
Expanded project panel shows a collapsible **Registry** section with namespace, endpoint, and
push/pull capability. The manage page adds **Runtime Services** toggles — MCP and GraphQL can be
switched without a daemon restart via HTMX `POST /ui/manage/services/{service}/toggle`.
**VCS Abstraction Layer** — `reflection/modules/vcs.nu` exposes a uniform API over jj and git:
`detect`, `show-committed`, `restore-file`, `remote-url`, `current-branch`, `uncommitted-files`,
`commit-count`. All ontoref modules consume `vcs.nu` — never hardcoded `^git`. Detection is
@ -227,12 +241,62 @@ full ontology enrichment in 8 phases.
`ONTOREF_PROJECT_ROOT` is set by the consumer wrapper — one ontoref checkout serves multiple projects.
## Credential vault and registry federation
ontoref ships a credential model for projects publishing or consuming OCI artifacts
(domain contracts and integration modes) on a self-hosted registry like ZOT.
The model is layered, declarative, and avoids ambient docker config:
- **Layer 0** — master age private key (`.kage`) per actor, declared in
`~/.config/ontoref/config.ncl::vault.master_key_path` (override per-project
in `<project>/.ontoref/project.ncl::sops.master_key_path`)
- **Layer 1**`access.sops.yaml` per project, multi-recipient encrypted; carries
`zot_username`, `zot_password`, `vault_key`, `cosign_password`
- **Layer 2** — operation credentials (RO/RW per registry entry) under
`src-vault/registry/`, referenced by `manifest.ncl::registry_provides[].credential_sops*`
Tenant isolation within a single vault uses sops `creation_rules` driven by
`sops.recipient_groups` + `sops.recipient_rules` in `project.ncl` — different
clients/agents get disjoint recipient sets per file, all in one vault. Multi-vault
is explicitly out of scope ([ADR-019](adrs/adr-019-per-file-recipient-routing-tenant-isolation.ncl)).
**Adoption** — copy a template from `install/resources/templates/sops/`:
| Template | When to use |
|---|---|
| `single-team/` | One team, no tenant separation |
| `multi-tenant/` | Multiple clients with isolated credentials |
| `agent-first/` | AI agents (MCP) read a single read-only credential |
For integration artifacts (publishing domain contracts or consuming someone's mode),
templates in `install/resources/templates/integration/`: `domain-producer/`,
`mode-producer/`, `mode-consumer/`.
**Day-to-day**:
```sh
ore secrets bootstrap # create vault for a new project (admin only)
ore secrets sync # pull latest src-vault from ZOT
ore secrets open # acquire OCI lock + edit access.sops.yaml
ore secrets close # impact report → push → release lock
ore secrets describe # full inventory: groups, rules, scopes, ops
ore secrets audit # 6 ADR-017 + ADR-019 constraint checks
```
See FAQ entries in `reflection/qa.ncl` for diagrams, troubleshooting, and the
15 named errors. ADRs: [017](adrs/adr-017-registry-credential-vault-model.ncl)
(vault model) and [019](adrs/adr-019-per-file-recipient-routing-tenant-isolation.ncl)
(per-file recipient routing).
## Prerequisites
- [Nushell](https://www.nushell.sh/) >= 0.110.0
- [Nickel](https://nickel-lang.org/) (for schema evaluation)
- Rust toolchain (for building crates)
- [Just](https://just.systems/) (for CI recipes)
- [age](https://github.com/FiloSottile/age) + [sops](https://github.com/getsops/sops) (credential vault, ADR-017/019)
- [oras](https://oras.land/) + [cosign](https://github.com/sigstore/cosign) ≥ 2 (OCI artifact federation)
- [restic](https://restic.net/) or [kopia](https://kopia.io/) (vault snapshots)
To build `ontoref-daemon` and `ontoref-reflection` with NATS/SurrealDB support, the
stratumiops repo must be checked out at `../../../stratumiops`. Without it, build without

View file

@ -0,0 +1,98 @@
let d = import "adr-defaults.ncl" in
d.make_adr {
id = "adr-014",
title = "Runtime Service Toggles — AtomicBool Flags for MCP and GraphQL",
status = 'Accepted,
date = "2026-04-26",
context = "ontoref-daemon exposes optional services (MCP, GraphQL) compiled in via Cargo feature flags. Once compiled, these services were always active for the lifetime of the process. Two scenarios require disabling them at runtime without restart: (1) security incident — temporarily disable a surface while investigating a compromise; (2) operator choice — enable graphql during a debug session, disable it in production. The alternative of restarting the daemon is disruptive because it drops all in-memory sessions, actors, and notification queues. Compile-time feature flags solve the binary presence problem but cannot address runtime availability.",
decision = "Introduce ServiceFlags (pub struct in api.rs) holding one AtomicBool per toggleable service, gated by the corresponding feature flag. ServiceFlags::new() initialises all flags to true (enabled). AppState holds Arc<ServiceFlags> shared across all clones. The toggle check lives in a route_layer middleware on the sub-router for each service — not inside the service handlers themselves — so the check is enforced regardless of which handler a request reaches. Two toggle surfaces are provided: (1) REST API: PUT /api/services/:service {\"enabled\": bool} — daemon admin Bearer required; (2) UI: POST /ui/manage/services/:service/toggle — AdminGuard (cookie session). Both surfaces return the new state. The manage page shows each compiled-in service with an HTMX toggle button; the navbar badges reflect runtime state on every page render.",
rationale = [
{
claim = "AtomicBool is the correct primitive for a hot-path toggle",
detail = "A toggle check runs on every request to /mcp/* and /graphql/*. AtomicBool::load(Relaxed) is a single CPU instruction with no lock, no allocation, and no blocking — identical cost to a null pointer check. RwLock<bool> would introduce lock contention under concurrent requests with no benefit, since the only invariant needed is 'consistent within a single request', which Relaxed ordering satisfies.",
},
{
claim = "Middleware layer placement enforces the toggle at the router boundary",
detail = "Placing the AtomicBool check inside each handler would require every handler to duplicate the guard, and a new handler added to the sub-router would silently bypass the toggle. A route_layer on the sub-router wraps all handlers uniformly — the check cannot be missed regardless of how many handlers the sub-router gains in the future.",
},
{
claim = "Arc<ServiceFlags> (not Arc<AtomicBool> per flag) is the correct sharing primitive",
detail = "AtomicBool is not Clone. Wrapping the whole ServiceFlags struct in one Arc means: (1) the middleware closure captures one Arc clone instead of N; (2) new flags can be added to ServiceFlags without changing the sharing structure; (3) the UI and REST handlers access all flags via one field on AppState.",
},
{
claim = "Two toggle surfaces (REST + UI) serve distinct operator workflows",
detail = "The REST endpoint (daemon admin Bearer) is suitable for scripted automation and CLI pipelines (curl -X PUT). The UI endpoint (AdminGuard cookie) enables toggle from the browser manage page without exposing the raw admin token. Both require daemon admin credentials — this is not a project-level operation.",
},
{
claim = "Toggle is volatile — it does not survive daemon restart",
detail = "ServiceFlags are initialised to enabled=true on every startup regardless of the pre-restart state. This is intentional: a flag disabled for incident response should not silently persist across restarts, which would hide the disabled state from new operators. If persistent toggle state is needed it belongs in config.ncl as a compiled-in default, not in volatile memory.",
},
],
consequences = {
positive = [
"MCP and GraphQL can be disabled in under 1ms without dropping in-memory state",
"Toggle surfaces require daemon admin credentials — the same identity used for project management",
"New optional services added to the daemon gain toggle support by adding one AtomicBool field to ServiceFlags",
"Middleware placement means the toggle cannot be bypassed by adding new handlers to the sub-router",
],
negative = [
"Toggle state is lost on restart — disabling a service for incident response must be re-applied after restart or captured in config",
"ServiceFlags is not Clone (AtomicBool is not Clone) — AppState clones the Arc, not the flags; callers that pattern-match on AppState fields must be aware of this",
],
},
alternatives_considered = [
{
option = "Restart daemon with different feature flags or config",
why_rejected = "Restart drops SessionStore (all active logins), actor registry, notification queue, and NATS subscriptions. A 1-second outage is acceptable for planned maintenance but not for rapid incident response.",
},
{
option = "RwLock<bool> per service in AppState",
why_rejected = "RwLock introduces lock contention on every request. The toggle check does not need mutual exclusion with writes — a store and a load never run concurrently in a way that would corrupt state. Relaxed AtomicBool is sufficient and faster.",
},
{
option = "Dynamic axum Router rebuild — swap out the sub-router entirely",
why_rejected = "axum Router is not live-rebuildable without replacing the entire tower Service. This would require Arc<RwLock<Router>>, a custom Service wrapper, and would still incur a lock per request. The middleware approach achieves the same result with orders of magnitude less complexity.",
},
],
related_adrs = ["adr-002", "adr-005"],
ontology_check = {
decision_string = "runtime toggle AtomicBool service mcp graphql middleware",
invariants_at_risk = [],
verdict = 'Safe,
},
constraints = [
{
id = "c-014-1",
claim = "ServiceFlags::new() must initialise all AtomicBool flags to true — a compiled-in service is always enabled at startup",
scope = "crates/ontoref-daemon/src/api.rs",
severity = 'Hard,
rationale = "Starting with flags disabled would silently hide services from operators who have not read this ADR. Explicit runtime toggle (documented in UI and REST API) is the correct mechanism to disable a service.",
check = { tag = "Grep", pattern = "AtomicBool::new(true)", paths = ["crates/ontoref-daemon/src/api.rs"], must_be_empty = false },
},
{
id = "c-014-2",
claim = "Any new optional service added to the daemon must add an AtomicBool to ServiceFlags and a route_layer toggle middleware on its sub-router",
scope = "crates/ontoref-daemon/src/api.rs",
severity = 'Hard,
rationale = "A service without a toggle violates the operator contract established by this ADR. Future services must be consistently operable.",
check = { tag = "Grep", pattern = "pub struct ServiceFlags", paths = ["crates/ontoref-daemon/src/api.rs"], must_be_empty = false },
},
{
id = "c-014-3",
claim = "Service toggle endpoints require daemon admin credentials — project-level auth is insufficient",
scope = "crates/ontoref-daemon/src/api.rs, crates/ontoref-daemon/src/ui/handlers.rs",
severity = 'Soft,
rationale = "Service availability is daemon-wide, not per-project. Allowing a project admin to disable MCP or GraphQL for all projects would be a privilege escalation.",
check = { tag = "Grep", pattern = "AdminGuard", paths = ["crates/ontoref-daemon/src/ui/handlers.rs"], must_be_empty = false },
},
],
}

View file

@ -0,0 +1,88 @@
let d = import "adr-defaults.ncl" in
d.make_adr {
id = "adr-015",
title = "MCP Tool Catalog via #[onto_mcp_tool] Proc-Macro + Inventory",
status = 'Accepted,
date = "2026-04-26",
context = "ontoref-daemon exposes 33 MCP tools via the rmcp ToolRouter in crates/ontoref-daemon/src/mcp/mod.rs. The authoritative tool implementation lives in each tool struct's `ToolBase`/`AsyncTool` impls (name, description, schema). However, the agent-facing tool catalog returned by `ontoref_help` was a hand-typed JSON literal (~100 lines, mod.rs:1129-1226) duplicating every tool's name, description, and parameter shape. This is the same failure mode ADR-007 documents as the original motivation for `#[onto_api]`: the previous-session bug where `insert_mcp_ctx` listed 15 tools while the router had 27. ADR-007 fixed that drift for the HTTP API surface (inventory-collected `ApiRouteEntry`) but did not extend the pattern to MCP. As of 2026-04-26 the manifest claim 'daemon exposes 33 MCP tools' is also a hand-maintained string in `.ontology/manifest.ncl`. With the rate of MCP tool additions through 2026-Q1 (qa, bookmarks, actions, config, ontology extensions all added in separate sessions), drift was becoming inevitable.",
decision = "Every MCP tool struct in ontoref-daemon must carry `#[onto_mcp_tool(name, description, category, params)]`. The proc-macro (in `crates/ontoref-derive`) emits `inventory::submit!(ontoref_ontology::McpToolEntry{...})` at link time and leaves the annotated struct unchanged — the existing `ToolBase` and `AsyncTool` impls are untouched. A new pure function `ontoref_daemon::mcp::catalog()` walks `inventory::iter::<McpToolEntry>()`, sorts by name, and returns `Vec<&'static McpToolEntry>`. `HelpTool::invoke` now serializes `catalog()` instead of holding a hand-typed JSON literal. `McpToolEntry` lives in `ontoref-ontology` next to `ApiRouteEntry` and reuses `ApiParam` for parameter metadata — both are protocol surfaces, the type is generic. `tool_router()`'s compile-time `with_async_tool::<T>()` list is left as-is; the Rust type system requires the type list at compile time and a separate macro architecture would be needed to derive it from inventory (out of scope for this ADR).",
rationale = [
{
claim = "Same drift class as ADR-007 — same fix pattern",
detail = "ADR-007 cites the insert_mcp_ctx (15 listed) vs router (27 actual) drift as proof that hand-maintained registries decay. The HelpTool JSON literal had identical mechanics: every new tool struct required two coordinated edits (with_async_tool registration + JSON entry) with no compiler enforcement that they stayed in sync. Applying the same inventory linker pattern that fixed it for HTTP routes closes the equivalent gap for MCP.",
},
{
claim = "Co-location of annotation and implementation",
detail = "`#[onto_mcp_tool]` sits directly on the tool struct, immediately above its `ToolBase` impl. Adding a tool that compiles but has no inventory entry requires actively skipping the attribute — a much higher-friction error than forgetting to update a JSON list 100 lines away in the same file. Removing a tool struct removes its inventory entry automatically.",
},
{
claim = "Zero new dependencies",
detail = "ontoref-derive and inventory are already workspace dependencies (introduced by ADR-007). McpToolEntry mirrors ApiRouteEntry's shape and reuses ApiParam — no schema duplication. The change is additive: existing #[onto_api] macros are unaffected.",
},
{
claim = "Preserves runtime cost guarantees",
detail = "inventory::iter walks a linker-built linked list — no HashMap, no Arc, no allocation. catalog() is a pure function returning &'static references. HelpTool's per-call cost is unchanged (still O(n) over the tool list, no cache invalidation, no daemon state required).",
},
],
consequences = {
positive = [
"Adding a tool struct without #[onto_mcp_tool] makes the tool invisible to ontoref_help — drift is detectable on first agent invocation",
"Removing a tool struct removes its catalog entry automatically; no orphaned help entries",
"Manifest claim '33 MCP tools' is verifiable at runtime via catalog().len() rather than asserted by hand",
"Future tool tiering (essential vs extended, mentioned but deferred from this ADR) becomes a single-field addition to McpToolEntry",
"Same proc-macro infrastructure as #[onto_api] — no new linker semantics, no platform-specific concerns introduced",
],
negative = [
"Per-tool params are stringly-typed in the `params = \"name:type:constraint:desc; ...\"` attribute argument — a typo in `required` vs `optional` is not caught at compile time (mirrors the same limitation in #[onto_api])",
"tool_router()'s with_async_tool::<T>() list still requires manual maintenance — the inventory does not eliminate it. A drift between router registrations and inventory entries is possible (caught only at runtime by the regression test, not at compile time)",
"33 tool structs require a one-time annotation pass — non-trivial diff size, but mechanical and reviewable",
],
},
alternatives_considered = [
{
option = "Generate the help JSON from ToolBase::name() + description() directly via reflection",
why_rejected = "ToolBase exposes name and description but not parameter metadata in a structured form (input_schema returns a JsonObject blob, not the per-param required/values/note shape that ontoref_help emits). Walking the JSON schema and reverse-engineering the per-param hints is brittle. The inventory entry lets us declare the agent-facing param documentation in a stable, typed shape next to the ToolBase impl.",
},
{
option = "Replace tool_router()'s with_async_tool::<T>() list with macro-driven iteration over inventory",
why_rejected = "rmcp's ToolRouter requires the tool type at compile time (generic associated types over each tool's Parameter/Output types). Driving registration from inventory entries — which are runtime values of `&'static McpToolEntry` — would require either a build.rs that emits the registration list or a macro that takes the full type list as input. Either is feasible but is a larger architectural change with no immediate reliability win beyond what the inventory already provides for the help/catalog surface. Deferred.",
},
{
option = "Skip the macro and write an `inventory::submit!` block manually next to each tool",
why_rejected = "Eliminates the macro infrastructure but loses the validation that #[onto_mcp_tool] applies (key spelling check, param string parsing reuse). Manual blocks also bypass the file!() capture for source-file traceability that ApiRouteEntry already uses.",
},
],
constraints = [
{
id = "onto-mcp-tool-on-all-tools",
claim = "Every MCP tool struct in ontoref-daemon must carry #[onto_mcp_tool(...)]",
scope = "ontoref-daemon (crates/ontoref-daemon/src/mcp/mod.rs)",
severity = 'Hard,
check = { tag = 'Grep, pattern = "#\\[onto_mcp_tool", paths = ["crates/ontoref-daemon/src/mcp/mod.rs"], must_be_empty = false },
rationale = "catalog() is only as complete as the set of annotated tool structs. Unannotated tools are invisible to agents calling ontoref_help — equivalent to undocumented tools and a re-introduction of the ADR-007 drift class.",
},
{
id = "mcp-catalog-router-parity",
claim = "The number of #[onto_mcp_tool] annotations must equal the number of with_async_tool::<T>() calls in tool_router()",
scope = "ontoref-daemon (crates/ontoref-daemon/src/mcp/mod.rs)",
severity = 'Soft,
check = { tag = 'NuCmd, command = "let a = (open crates/ontoref-daemon/src/mcp/mod.rs | lines | where ($it | str starts-with '#[ontoref_derive::onto_mcp_tool') | length); let b = (open crates/ontoref-daemon/src/mcp/mod.rs | lines | where ($it | str contains 'with_async_tool::<') | length); if $a != $b { error make {msg: $'annotation count ($a) != router registration count ($b)'} } else { 'ok' }" },
rationale = "Without compile-time enforcement, a tool could be registered with the router but lack the annotation (or vice versa). This soft constraint catches the divergence at validate time. Future work (deferred alternative above) can lift this to compile time.",
},
],
related_adrs = ["adr-007", "adr-001"],
ontology_check = {
decision_string = "Use #[onto_mcp_tool] proc-macro + inventory linker registration as the single source of truth for the MCP tool catalog; HelpTool renders from catalog() instead of a hand-typed JSON literal",
invariants_at_risk = ["protocol-not-runtime"],
verdict = 'Safe,
},
}

View file

@ -0,0 +1,101 @@
let d = import "defaults.ncl" in
d.make_adr {
id = "adr-016",
title = "Component Lift-Out Pattern: Four-Criterion Gate for Standalone Extraction",
status = 'Accepted,
date = "2026-05-01",
context = "Ontoref itself was extracted from stratumiops (ADR-001). The same pattern recurred in the provisioning project: buildkit-launcher (build substrate) and backup-manager (backup orchestration) grew inside provisioning but serve a broader class of consumers — vapora, workspace infras, CI pipelines, and third-party projects. No formal criterion existed for deciding when a component is ready to become a standalone peer project. Without a criterion, the decision is arbitrary and either premature (decomposition overhead before consumer plurality) or delayed (host coupling accretes, extraction becomes expensive).",
decision = "A component is extracted as a standalone peer project when it passes all four criteria: (1) Orthogonal concern — the component's core domain is not the host project's core domain; (2) Consumer plurality — at least two distinct callers exist or are immediately planned; (3) Release cadence divergence — the component's evolution is not gated by the host project's release cycle; (4) Config path-agnostic — the component can receive its config from any caller without importing host infrastructure (workspace crates, host config loaders, host schema registries). The extracted project registers in ontoref before any code moves. The host project retains extension-side artifacts (schemas, defaults, component declarations) that allow workspace infras to declare directives for the extracted tool.",
rationale = [
{
claim = "Orthogonality is the primary criterion",
detail = "A build substrate (lian-build) and a backup orchestrator (cloudatasave) do not belong to provisioning's core domain (workspace lifecycle management). Provisioning calls them; it does not own their concerns. Orthogonality is the structural signal that extraction is correct, not premature.",
},
{
claim = "Consumer plurality prevents premature extraction",
detail = "A component with a single caller has no demonstrated need for independent lifecycle. Two or more distinct callers (e.g. provisioning + vapora, or provisioning + a workspace CI pipeline) prove the component's value is general, not host-specific. ADR-001 precedent: ontoref serves stratumiops, typedialog, vapora, kogral.",
},
{
claim = "Config path-agnostic is the coupling criterion",
detail = "If a component imports the host's config loader, schema registry, or workspace crates to function, it cannot be independently deployed or tested. Severing this import at extraction time is not simplification — it is restoration of the correct dependency direction. The extracted project has its own config infrastructure; callers pass directives in the extracted project's own NCL vocabulary.",
},
{
claim = "Ontoref registration before code move establishes ontological identity",
detail = "Creating the .ontology/ and ADRs in the new project before any code moves ensures the project exists as a declared concept before it exists as an implementation. This prevents the common failure mode where a project's identity is derived post-hoc from accumulated code rather than declared from design.",
},
{
claim = "Host retains extension-side artifacts",
detail = "The host project (provisioning) retains the extension schemas, defaults, and component declarations that allow workspace infras to configure the extracted tool. This is not coupling — it is the host's side of the protocol contract. The extracted project defines its capabilities in its own ontology; provisioning extensions expose those capabilities to workspace consumers.",
},
],
consequences = {
positive = [
"Decisions to extract are justified, not arbitrary — four criteria provide a checkable gate",
"Extracted projects are immediately usable by non-provisioning callers (vapora, CI, workspace infras)",
"Host projects remain focused on their core domain; extension schemas handle the integration surface",
"The pattern is self-documenting: ADR-001 (ontoref from stratumiops) and ADR-016 instances (lian-build, cloudatasave from provisioning) form a corpus of precedent",
"Ontoref registration before code move ensures the project has identity before it has implementation",
],
negative = [
"Extraction requires a deliberate severing of host infrastructure imports — this is work that must be done correctly, not deferred",
"Two registration surfaces per project: the extracted project's own .ontoref/ and the host's extension schemas",
"Consumer projects (workspace infras) must import from the extracted project's schema vocabulary, not from the host's workspace schema",
],
},
alternatives_considered = [
{
option = "Extract only when a second consumer exists and requests it",
why_rejected = "Reactive extraction means coupling has already accreted. By the time a second consumer requests the component, host infrastructure imports are deep. Proactive evaluation at the four-criterion gate is cheaper than reactive untangling.",
},
{
option = "Keep all tools inside provisioning as internal workspace crates, expose via provisioning CLI",
why_rejected = "This routes all callers through provisioning's release cycle and binary. vapora and workspace CI pipelines cannot use the tool without depending on provisioning. The four-criterion test exists precisely to identify when this constraint becomes incorrect.",
},
{
option = "Publish extracted crates to crates.io immediately",
why_rejected = "Published crates require stable API contracts before the consumer relationship is proven. Path-based standalone project references allow simultaneous development of the extracted project and its consumers without publication ceremony.",
},
],
constraints = [
{
id = "ontology-before-code",
claim = "An extracted project must have .ontology/core.ncl, .ontology/state.ncl, and adrs/adr-001 committed before any code from the host is moved",
scope = "any project applying the lift-out pattern",
severity = 'Hard,
check = {
tag = 'FileExists,
path = ".ontology/core.ncl",
present = true,
},
rationale = "Ontological identity precedes implementation. A project that exists only as code has no declared purpose, axioms, or constraints — the extraction is indistinguishable from a rename.",
},
{
id = "no-host-infrastructure-import",
claim = "The extracted project must not import workspace crates, config loaders, or schema registries from the host project",
scope = "extracted project source code",
severity = 'Hard,
check = {
tag = 'Grep,
pattern = "platform_config|stratum-config|provisioning-config",
paths = ["src/", "crates/"],
must_be_empty = true,
},
rationale = "Importing host infrastructure is the definition of host coupling. If the import cannot be severed, the component fails criterion 4 and must not be extracted until the coupling is resolved.",
},
],
related_adrs = ["adr-001-protocol-as-standalone-project"],
ontology_check = {
decision_string = "components are extracted as standalone peer projects when they pass a four-criterion gate; ontoref registration precedes code move; host retains extension-side schemas",
invariants_at_risk = ["protocol-not-runtime", "self-describing"],
verdict = 'Safe,
},
}

View file

@ -0,0 +1,217 @@
let d = import "defaults.ncl" in
d.make_adr {
id = "adr-017",
title = "Registry Credential Vault Model: src-vault, Multi-Recipient sops, and Actor-Scoped Access",
status = 'Accepted,
date = "2026-05-01",
context = "The provisioning registry (zot OCI) became the primary coordination hub for domain and mode artifacts (ADR-016 consequences, migration 0015). This introduced registry credentials as a first-class concern: every project that pushes or pulls artifacts needs credentials, and those credentials must be distributed across actors (developers, CI pipelines, the ontoref daemon, AI agents, ops tooling) with different access levels. The naive model — environment variables or ambient ~/.docker/config.json — has three compounding failure modes: (1) credentials are ambient and unscoped, so an operation against project A can use credentials that belong to project B; (2) there is no distribution mechanism — adding a new team member or CI key requires manual redistribution of a shared secret; (3) there is no audit trail for credential access or changes. A supply-chain attack via misconfigured registry namespace endpoints is the concrete risk: if the daemon proxied registry calls, any actor with MCP access could redirect them to an arbitrary endpoint using ambient credentials.",
decision = "Registry credentials are managed through a per-project src-vault stored as an OCI artifact in the same ZOT registry it protects. The src-vault uses sops with age multi-recipient encryption: each actor role has its own age keypair, and credential files are encrypted for the exact set of recipients that need access. The vault backend (restic or kopia) handles versioning and local copies; the ZOT registry handles distribution. Each project has a local access.sops.yaml in ~/.config/ontoref/vaults/<project-slug>/ containing three fields: zot_username, zot_password, and vault_key. All three are encrypted by the actor's master age private key (.kage), which lives at an external path the actor controls (hardware key, encrypted disk, or declared path in config.ncl) — never inside the vault directory. At operation time, sops decrypts access.sops.yaml in memory: zot_username and zot_password are used to pull the src-vault OCI artifact via a DOCKER_CONFIG tmpdir that is deleted immediately after; vault_key is passed as RESTIC_PASSWORD or KOPIA_PASSWORD env var for the duration of the vault operation and never written to disk. No plaintext credential of any kind persists beyond the operation scope. Credential resolution runs exclusively in the ontoref CLI — the daemon is structurally excluded. All oras invocations use an isolated DOCKER_CONFIG tmpdir; no ambient ~/.docker/config.json is consulted. Access logs are appended to a jsonl file stored as a layer in the same src-vault OCI artifact and mirrored locally in ~/.config/ontoref/vaults/<project-slug>/logs/access.jsonl.",
rationale = [
{
claim = "src-vault in ZOT closes the distribution gap structurally",
detail = "Storing the vault as an OCI artifact in the same registry that protects means distribution is the same operation as registry access. Any actor with registry RO credentials can pull the vault; only admin can push. No separate key distribution channel is needed. When a developer joins, their public key is added to src-vault and they can immediately pull their copy — no coordinator required.",
},
{
claim = "Multi-recipient sops eliminates shared secrets",
detail = "Each actor has its own age keypair. The sops DEK is encrypted separately for each recipient public key. Removing a recipient requires only updating .sops.yaml and running sops updatekeys — no key redistribution. The private key of the removed actor becomes useless for future vault operations the moment updatekeys completes. This is the only model that supports revocation without rotation of all other credentials.",
},
{
claim = "Daemon structural exclusion is not a policy, it is an architectural property",
detail = "The daemon process has no age private key and no access to project .kage files. It cannot decrypt sops files even if it reads them. The daemon surfaces only the declarative topology from manifest.ncl (registry_provides, credential_sops paths as strings). Credential resolution happens in the CLI process of the authenticated user. This cannot be bypassed without modifying the daemon to acquire keys — which would be an explicit, reviewable change, not a configuration drift.",
},
{
claim = "Lock state in ZOT with admin-only write ACL makes the lock structural",
detail = "The src-vault namespace has write ACL restricted to admin credentials in ZOT config. The lock artifact (src-vault/<project>:lock) can only be pushed or deleted with admin credentials. Other roles can read the lock state but cannot modify it. This converts a cooperative lock into a structurally enforced one — bypassing the lock requires admin credentials, which is an auditable action, not an accident.",
},
{
claim = "Audit log co-located with vault enables tamper-evident access history",
detail = "The audit.jsonl layer is part of the same OCI artifact as the vault content. Every push of src-vault:<project>:latest includes the updated log. Since only admin can push, and the log is append-only within a vault session, the log cannot be modified without admin credentials and a vault open/close cycle — both of which are themselves logged. This does not prevent a malicious admin from editing the log, but it makes any edit detectable via the OCI manifest digest history.",
},
{
claim = "Role scope enforcement at two independent layers",
detail = "Scope is enforced first by the CLI (pre-check against scopes/<role>.ncl before calling oras) and second by the ZOT ACL (the token itself carries the permissions issued by the registry). These layers are independent: a misconfigured scope file does not grant registry permissions, and a misconfigured registry ACL does not bypass the CLI pre-check. The two layers must be kept in sync via secrets-audit in CI.",
},
{
claim = "credential_env is excluded by design, not by convention",
detail = "Environment variables are visible in ps aux, inherited by all child processes, logged by CI systems unless explicitly masked, and cannot be scoped to a specific operation. Excluding credential_env from the schema means it is impossible to accidentally use it — there is no field to populate. The only credential reference fields are credential_sops (current) and credential_oidc (declared for future OIDC workload identity support).",
},
{
claim = "vault_key in sops ensures the restic/kopia key is never plaintext at rest",
detail = "The restic/kopia encryption key (vault_key) lives inside access.sops.yaml, encrypted by the actor's age master key. It is decrypted into RESTIC_PASSWORD or KOPIA_PASSWORD env var only for the duration of the vault operation — never written to a key file on disk. This means the only plaintext secrets on disk are the actor's .kage (which the actor is responsible for protecting) and the sops-encrypted access.sops.yaml (unreadable without the .kage). The pattern is: one master key protects everything else; everything else is encrypted at rest.",
},
{
claim = "restic and kopia are equivalent vault backends behind a thin abstraction",
detail = "Both restic and kopia provide encrypted, versioned snapshots with content-addressed storage. The choice between them depends on operational preference (kopia has a richer UI and faster incremental backups; restic has broader ecosystem support). The vault-backend.nu abstraction wraps init, backup, restore, and snapshot-list — switching backends requires changing vault_backend.tool in config.ncl and re-initializing the local repo. The OCI artifact in ZOT is backend-agnostic.",
},
],
consequences = {
positive = [
"Credential distribution is the same operation as registry access — no separate channel",
"Revoking a developer's access requires one command (secrets-remove-key) with no coordination",
"The daemon cannot be used as a credential amplifier regardless of MCP actor permissions",
"Every vault open, edit, and close is logged in the artifact that contains the credentials",
"Adding a new role (e.g. backup-agent) requires only a new keypair and a .sops.yaml update",
"Local copy in ~/.config/ontoref/vaults/<slug>/ provides offline fallback for admin",
"Bootstrap creates the first consistent vault state before any live registry interaction",
"impact analysis on secrets-close identifies services affected by credential changes before confirming",
],
negative = [
"Bootstrap requires admin to collect all public keys before first vault push — cannot be fully automated",
"sops updatekeys must run after every recipient change — forgetting it leaves old recipients in files",
"Zot ACL and scope NCL files must be kept in sync manually — drift produces security theater",
"Loss of .kage means access.sops.yaml cannot be decrypted — vault_key and ZOT credentials are unrecoverable without the master key; admin must keep the .kage backed up independently",
"ore secrets open/close adds a ritual to any secrets change session — acceptable overhead, but real",
"Direct sops invocations bypass the lock and audit log — CI audit detects but does not prevent",
],
},
alternatives_considered = [
{
option = "Environment variables (REGISTRY_TOKEN, DOCKER_CONFIG) per CI job",
why_rejected = "Visible in process list, inherited by subprocesses, logged by CI systems, cannot be scoped to a specific registry endpoint. No revocation without rotating all consumers. The primary attack surface for supply-chain credential leakage.",
},
{
option = "Shared age keyring — one private key distributed to all developers",
why_rejected = "Revocation requires rotating the shared key and redistributing to all remaining members — coordination cost scales with team size. No per-actor audit trail. A leaked key compromises all actors simultaneously.",
},
{
option = "HashiCorp Vault or similar external secrets manager",
why_rejected = "Introduces a network dependency for every credential resolution. Requires operating a separate service with its own HA, backup, and auth model. The OCI registry is already the coordination hub — using it as the vault distribution backend reuses existing infrastructure and auth.",
},
{
option = "Daemon resolves credentials on behalf of CLI actors",
why_rejected = "The daemon is a long-lived process accessible to multiple actors (developer, agent, CI via MCP). Giving it credential resolution capability means any actor with daemon access can trigger registry operations using credentials they do not personally hold. The structural exclusion of the daemon from credential resolution is a load-bearing architectural property.",
},
{
option = "Single credential file per registry (no RO/RW split)",
why_rejected = "A single credential with RW access distributed to read-only actors (cdci, ontoref, agent) violates least-privilege. A compromised CI pipeline with RW credentials can push malicious artifacts. The RO/RW split means a compromised read-only credential cannot alter the artifact namespace.",
},
],
constraints = [
{
id = "vault-access-credentials-independent",
claim = "The credentials required to pull the src-vault OCI artifact from ZOT must be stored outside the vault itself — each src-vault has its own access credentials, held in the actor's local .kage, not inside any vault it protects",
scope = "all projects with src-vault in ZOT",
severity = 'Hard,
check = {
tag = 'NuCmd,
cmd = "ore secrets audit --check bootstrap-credentials",
},
rationale = "A vault whose access credential is inside itself cannot be opened. The bootstrap credential (registry RO for the src-vault namespace) must preexist in the actor's local .kage. Two projects on the same ZOT instance have independent src-vault namespaces with independent access credentials — access to one does not imply access to the other.",
},
{
id = "no-credential-env",
claim = "RegistryEntry must not use credential_env — only credential_sops or credential_oidc are valid credential reference fields",
scope = "all manifest.ncl files declaring registry_provides",
severity = 'Hard,
check = {
tag = 'Grep,
pattern = "credential_env",
paths = [".ontology/manifest.ncl"],
must_be_empty = true,
},
rationale = "credential_env is excluded from the schema. Its presence indicates a manual edit bypassing the contract.",
},
{
id = "multi-recipient-mandatory",
claim = "Every *.sops.yaml credential file must include at minimum the admin and the bound_actor recipients for its access class",
scope = "all projects with registry_provides.registries[].credential_sops declared",
severity = 'Hard,
check = {
tag = 'NuCmd,
cmd = "ore secrets audit --check recipients",
},
rationale = "A credential file encrypted for a single recipient is indistinguishable from a shared secret. Multi-recipient is the mechanism that enables revocation without rotation.",
},
{
id = "uses-registry-declared",
claim = "Any domain or mode that pushes or pulls from a registry must declare uses_registry referencing the RegistryEntry id in manifest.ncl",
scope = "domain and mode NCL declarations",
severity = 'Hard,
check = {
tag = 'NuCmd,
cmd = "ore secrets audit --check registry-deps",
},
rationale = "Without the explicit dependency declaration, secrets-close cannot compute the impact of credential changes. Undeclared dependencies produce silent breakage after a credential rotation.",
},
{
id = "ore-secrets-exclusive-wrapper",
claim = "All sops operations on registry credential files must go through ore secrets — direct sops invocations bypass lock state and audit log",
scope = "CI pipeline and developer workflow",
severity = 'Soft,
check = {
tag = 'NuCmd,
cmd = "ore secrets audit --check lock-compliance",
},
rationale = "ore secrets enforces lock state verification before any sops edit. Direct sops invocations are detectable in CI audit but not preventable at the filesystem level. The constraint is soft because enforcement is post-hoc.",
},
{
id = "docker-config-isolation",
claim = "Every oras invocation must use DOCKER_CONFIG pointing to a tmpdir containing only the credential for the target registry endpoint — no ambient ~/.docker/config.json",
scope = "nulib/cli/integration.nu and any code calling oras",
severity = 'Hard,
check = {
tag = 'Grep,
pattern = "DOCKER_CONFIG",
paths = ["provisioning/core/nulib/"],
must_be_empty = false,
},
rationale = "Ambient credentials allow operations against project A to use credentials that belong to project B if both resolve to the same registry hostname. Isolation is enforced by constructing the config at call time and deleting it immediately after.",
},
{
id = "vault-key-never-plaintext",
claim = "vault_key must never be written to disk in plaintext — it is decrypted from access.sops.yaml into RESTIC_PASSWORD or KOPIA_PASSWORD for the duration of the operation only",
scope = "nulib/platform/vault-backend.nu and all ore secrets callers",
severity = 'Hard,
check = {
tag = 'Grep,
pattern = "vault_key.*save\\|vault_key.*write\\|restic.*--key-file",
paths = ["provisioning/core/nulib/"],
must_be_empty = true,
},
rationale = "Writing vault_key to a file defeats the purpose of encrypting it in sops. The env var approach (RESTIC_PASSWORD, KOPIA_PASSWORD) keeps the key in process memory only. The .kage master key is the single plaintext secret the actor manages — everything derived from it must be ephemeral.",
},
{
id = "vault-backend-abstracted",
claim = "All vault snapshot operations must use vault-backend.nu — direct restic or kopia invocations are not permitted in recipes or modules",
scope = "justfiles/secrets.just and nulib/platform/vault-backend.nu callers",
severity = 'Soft,
check = {
tag = 'Grep,
pattern = "^\\s*restic\\|^\\s*kopia",
paths = ["justfiles/", "provisioning/core/nulib/"],
must_be_empty = true,
},
rationale = "Switching between restic and kopia must require only changing vault_backend.tool in config — not hunting direct invocations across recipes and modules.",
},
{
id = "src-vault-cosign-signed",
claim = "Every push of src-vault/<project>:latest to ZOT must produce a cosign signature; every pull must verify the signature before the artifact is trusted",
scope = "justfiles/secrets.just secrets-push and secrets-sync recipes",
severity = 'Hard,
check = {
tag = 'NuCmd,
cmd = "ore secrets audit --check cosign-signature",
},
rationale = "The src-vault artifact contains access.sops.yaml with encrypted registry credentials. An unsigned artifact can be substituted without detection — a malicious actor with write access to the registry could replace it with a version encrypted for attacker-controlled recipients. COSIGN provides tamper evidence independent of ZOT ACLs: even if ACLs are misconfigured, the signature check prevents a substituted vault from being consumed. cosign is now a Hard prerequisite for ontoref (declared in requirements) for this reason.",
},
],
related_adrs = [
"adr-005-unified-auth-session-model",
"adr-012-domain-extension-system",
"adr-016-component-lift-out-pattern",
],
ontology_check = {
decision_string = "registry credentials are managed via per-project sops multi-recipient vaults stored as OCI artifacts in ZOT; the daemon is structurally excluded from credential resolution; actor-role binding is declared in project.ncl; vault backend is restic or kopia behind a thin abstraction; access logs are co-located with the vault artifact",
invariants_at_risk = ["protocol-not-runtime", "no-enforcement"],
verdict = 'Safe,
},
}

View file

@ -0,0 +1,148 @@
let d = import "defaults.ncl" in
d.make_adr {
id = "adr-018",
title = "Level Hierarchy and Mode Resolution Strategy — Observable Boundary Traversal",
status = 'Accepted,
date = "2026-05-01",
context = "ADR-012 introduced a domain extension system that enables project-specific specialization of ontoref. In practice this created a three-level hierarchy: (1) ontoref base — generic protocol and reflection operations; (2) project domain — specialization of ontoref for a specific project type (provisioning, personal, ...); (3) domain instance — a concrete project derived from a domain (a workspace, an infra, a team ontoref). The hierarchy was not formalized: no level declares its identity, no mode declares whether its implementation is complete or delegates to the level above, and no mechanism exists to determine which level answered a given operation. The result is implicit traversal — a caller invoking 'build-docs' has no way to know whether the operation ran at level 1, 2, or 3, whether it merged contributions from multiple levels, or whether it silently fell through to the base because the domain had not implemented it yet. The discussion that produced this ADR also identified that resolution strategies cannot be uniform: the correct strategy depends on the mode, the project context, and the adoption phase. A domain in early adoption may Delegate all documentation modes to the base; the same domain at maturity Overrides them with project-specific implementations. The transition between strategies must itself be observable — crossing a level boundary is an architectural event, not an implementation detail.",
decision = "Formalize the three-level hierarchy with four mechanisms: (1) Level identity declaration — each ontoref instance declares level.index (1=base, 2=domain, 3=instance), level.name, and level.parent (name of the level above; absent at level 1) in manifest.ncl. This makes level identity explicit and queryable. (2) Per-mode resolution strategy — each reflection mode declares a strategy field using one of four values: 'Override (implementation is complete at this level, traversal stops), 'Delegate (no implementation here, traverse to parent), 'Merge (accumulate fields bottom-up across all levels; lower level wins on conflicts), 'Compose (declare explicit partial inheritance via an extends field naming specific steps or fields to inherit from the parent). Strategy is required at level 2+; absent at level 1 (base is always Override by definition). Implicit absence at level 2+ is treated as 'Delegate with a Soft validation warning. (3) FSM-bound strategy transitions — when a mode's strategy is expected to change as the project matures, declare a state dimension in state.ncl whose current_state tracks the strategy value. The transition from 'Delegate to 'Override (or any other pair) is then an observable FSM event: tracked in state.ncl, visible in ore describe state, and triggerable by the same transition conditions as any other dimension. Strategy changes are architectural events, not silent refactors. (4) Observable traversal via ore mode resolve — the command 'ore mode resolve <id>' reports which level answered the operation, the strategy applied, the source file, and the reason (declared strategy or FSM state). No mode resolution is ever silent.",
rationale = [
{
claim = "Implicit traversal makes level boundaries invisible and coupling complaints undiagnosable",
detail = "The coupling discussions that preceded this ADR (provisioning workspace management appearing coupled to ontoref, credential management appearing to require ontoref) were symptoms of invisible level traversal. When it is impossible to determine whether an operation ran at level 1 or 2, it is also impossible to determine whether the coupling is load-bearing (level 2 deliberately uses level 1) or accidental (level 2 forgot to implement something and fell through). Explicit level declaration and strategy declaration make this distinction mechanically checkable.",
},
{
claim = "Per-mode strategy is the correct granularity — global strategy per level does not hold",
detail = "A domain in active use may Override its core modes (workspace management, infra deploy) while still Delegating peripheral modes (documentation generation, ADR validation) to the base. A global strategy per level would force a false choice: either all modes are overridden (blocking base adoption) or all modes delegate (making the domain invisible). Per-mode strategy reflects the actual adoption curve: domains specialize incrementally, not all at once.",
},
{
claim = "FSM-bound transitions make strategy changes architectural events",
detail = "Without FSM binding, a mode moving from 'Delegate to 'Override is a silent code change with no recorded rationale. With FSM binding, the transition requires updating current_state in state.ncl, which is a deliberate action visible to ore describe state, tracked in git history, and subject to the same transition conditions (catalyst, blocker) as any other dimension. The architectural significance of 'we now own this mode' is formally recorded.",
},
{
claim = "'Compose is necessary for surgical specialization that preserves base infrastructure",
detail = "Merge and Override are extremes: Merge accumulates everything (field-level conflicts resolved by lower level winning), Override discards everything from above. Compose covers the real case: a domain wants to keep the base's generate-api step and the base's lint-docs step, but replace the publish step with a domain-specific one. Without Compose, the domain must either fully override (losing base infrastructure it wants) or Merge (risking unintended inheritance of base steps it does not want).",
},
{
claim = "Level parent as name reference decouples identity from location",
detail = "parent = 'ontoref-base' in a domain's level declaration is a name reference, not a file path or OCI artifact coordinate. The resolution of that name to a concrete implementation is context-dependent: local checkout for development, OCI artifact for distributed domains. The ADR defines the declaration contract; resolution is implementation-dependent and may itself evolve (ADR-010 migration system handles protocol evolution).",
},
{
claim = "ore mode resolve makes the system self-describing at the operational layer",
detail = "ontoref's core identity (ADR-001, core.ncl) is self-describing: it consumes its own protocol. ore mode resolve extends this to the operational layer: the resolution mechanism is itself queryable. An agent or developer can always answer 'which level is handling this mode right now and why' without reading source code. This is the concrete expression of the 'always know where you are and when you cross the frontier' requirement.",
},
],
consequences = {
positive = [
"Level identity is always queryable — ore describe project shows level.index, level.name, level.parent",
"Mode resolution is always observable — ore mode resolve <id> shows level, strategy, source, reason",
"Strategy changes are FSM events — visible in git, queryable in state, subject to transition conditions",
"Delegate chains that break (no Override anywhere in the parent chain) are caught by ore validate modes",
"Phased domain adoption is formally supported — early phases Delegate, mature phases Override",
"Coupling complaints become diagnosable — is provisioning using ontoref base because it Delegates (load-bearing) or because it forgot to Override (accidental)?",
"Compose enables surgical specialization without full Override — base infrastructure is reusable explicitly",
],
negative = [
"All existing modes at level 2+ require strategy field addition — migration 0017 needed",
"ore mode resolve is not yet implemented — constraint delegate-chain-complete cannot be checked until it is",
"ore validate modes is not yet implemented — constraints are declared but not executable at ADR acceptance",
"Compose requires extends field with precise step/field references — mismatches produce runtime errors, not schema errors",
"FSM dimensions for strategy transitions add state.ncl surface area — domains with many modes in transition accumulate many dimensions",
],
},
alternatives_considered = [
{
option = "Implicit inheritance — current state, no declaration required",
why_rejected = "Makes level traversal invisible. Coupling is undiagnosable. Adoption phase is unknown. Boundary crossing is unobservable. This is the state that produced the architectural confusion this ADR resolves.",
},
{
option = "Global strategy per level — one strategy applies to all modes at a given level",
why_rejected = "Domains specialize incrementally. A global Override for level 2 blocks early adoption (all modes must be implemented before any work). A global Delegate for level 2 makes domains invisible (nothing is ever implemented). Per-mode strategy reflects the real adoption curve.",
},
{
option = "Override-only — levels either implement fully or do not appear",
why_rejected = "Eliminates phased adoption. A domain cannot exist in the system until it has implemented every mode — a steep entry cost that contradicts ADR-001 (voluntary coherence, no enforcement). Delegate and Compose are specifically needed for incremental adoption.",
},
{
option = "Separate level.ncl file instead of level field in manifest.ncl",
why_rejected = "manifest.ncl already holds structural self-description (requirements, capabilities, registry topology, layers). Level identity is structural metadata of the same kind. A separate file adds coordination overhead (two files to keep in sync, two files for describe to read) for no additional expressiveness.",
},
],
constraints = [
{
id = "level-declared-at-domain",
claim = "Any ontoref instance at level 2 or 3 must declare level.index, level.name, and level.parent in manifest.ncl",
scope = "all manifest.ncl files in domain and instance projects",
severity = 'Hard,
check = {
tag = 'NuCmd,
cmd = "ore validate modes --check level-declared",
},
rationale = "Level identity is the precondition for all other mechanisms in this ADR. An undeclared level cannot participate in observable traversal or FSM-bound strategy transitions.",
},
{
id = "strategy-declared-at-domain",
claim = "Every reflection mode at level 2+ must declare strategy explicitly; implicit absence is a Soft violation",
scope = "reflection/modes/*.ncl in domain and instance projects",
severity = 'Soft,
check = {
tag = 'NuCmd,
cmd = "ore validate modes --check strategy-declared",
},
rationale = "Implicit Delegate (mode exists at level 2 without strategy field) is functionally equivalent to explicit Delegate but invisible. The Soft severity allows gradual adoption — existing modes that predate this ADR are not hard-blocked, but the warning drives migration.",
},
{
id = "delegate-chain-complete",
claim = "No Delegate chain may terminate without an Override at some level — every mode invocation must resolve to a concrete implementation",
scope = "all level 2 and 3 modes with strategy = 'Delegate",
severity = 'Hard,
check = {
tag = 'NuCmd,
cmd = "ore validate modes --check delegate-chain",
},
rationale = "A Delegate chain that ends without an Override means the operation has no implementation — invoking it produces a silent no-op or an opaque error. This is the failure mode that makes implicit traversal dangerous.",
},
{
id = "strategy-state-coherent",
claim = "If state.ncl declares a dimension for a mode's strategy, its current_state must match the strategy field declared in the mode NCL",
scope = "state.ncl dimensions whose id matches the pattern '<mode-id>-strategy'",
severity = 'Soft,
check = {
tag = 'NuCmd,
cmd = "ore validate modes --check strategy-state",
},
rationale = "Strategy drift between the FSM state and the mode declaration means the state dimension has become decorative — it records an intention that is no longer reflected in the actual implementation. Soft severity allows the transition to be in-flight (state says Override, code not yet complete).",
},
{
id = "compose-extends-valid",
claim = "A mode with strategy = 'Compose must declare an extends field; every step or field reference in extends must exist in the named parent mode",
scope = "reflection/modes/*.ncl with strategy = 'Compose",
severity = 'Hard,
check = {
tag = 'NuCmd,
cmd = "ore validate modes --check compose-extends",
},
rationale = "A Compose mode with a broken extends reference silently inherits nothing from the parent — equivalent to an unintentional Override. Since Compose is specifically chosen to preserve parent infrastructure, a broken reference is always a bug.",
},
],
related_adrs = [
"adr-001-protocol-as-standalone-project",
"adr-010-protocol-migration-system",
"adr-011-mode-guards-and-convergence",
"adr-012-domain-extension-system",
],
ontology_check = {
decision_string = "three-level hierarchy (base, domain, instance) with per-mode resolution strategy (Override, Delegate, Merge, Compose); strategy transitions are FSM events in state.ncl; ore mode resolve makes traversal observable; ore validate modes enforces chain completeness",
invariants_at_risk = ["protocol-not-runtime", "no-enforcement"],
verdict = 'Safe,
},
}

View file

@ -0,0 +1,151 @@
let d = import "defaults.ncl" in
d.make_adr {
id = "adr-019",
title = "Per-File Recipient Routing for Tenant Isolation in lieu of Multi-Vault",
status = 'Accepted,
date = "2026-05-03",
context = "ADR-017 established per-project credential vaults as OCI artifacts in ZOT, encrypted with sops + age multi-recipient. The model held one recipient set per vault: every actor who could decrypt access.sops.yaml could decrypt every credential file inside the vault. Real projects (libre-wuji, the canonical multi-tenant example) need stronger separation — a single project hosts multiple clients with distinct services, AI agents that operate with restricted access, and developer/admin roles that occasionally overlap. Three concrete failure modes appear: (1) credential-of-clientB is visible to anyone who can open the vault even when only working on clientA — recipient lists are coarse and indiscriminate; (2) AI agents with read-only intent receive credentials whose blast radius exceeds their declared scope; (3) blast-radius limitation requires per-file recipient sets, which the original single-set model does not express. A naive answer is multi-vault — one vault_id per tenant or per environment — but it cascades through every layer (schema, helpers, recipes, dispatcher, migration) and creates an architectural debt without justifying a real isolation requirement (separate master keys, separate restic repos) for the typical case.",
decision = "Tenant isolation within a single project is expressed via sops creation_rules, declared in project.ncl::sops as recipient_groups (named lists of age public keys) and recipient_rules (path_regex to group-union mappings). The bootstrap recipe generates <vault_dir>/.sops.yaml from these declarations and sops natively encrypts each file with the union of declared groups. One vault_id per project remains the unit. Multi-vault is explicitly NOT implemented and remains out of scope until a project requires HARD isolation (separate master keys, separate restic repos, compliance-grade separation surviving accidental cross-decryption); such a case requires a future ADR. When recipient_rules are declared, project.ncl is the single source of truth: secrets-add-key and secrets-remove-key error out and direct the operator to edit project.ncl plus run secrets-rekey, which regenerates .sops.yaml and re-encrypts every *.sops.yaml file. Three adoption templates (single-team, multi-tenant, agent-first) ship in install/resources/templates/sops/ as copy-paste starting points; a project may adopt any pattern or none.",
rationale = [
{
claim = "Use sops creation_rules natively — do not invent a parallel routing layer",
detail = "sops already provides per-file recipient routing via creation_rules in .sops.yaml. Using it directly inherits all of sops' tooling (updatekeys, --decrypt with .sops.yaml discovery, --filename-override) and avoids a competing convention that would double the surface and break composability with sops itself.",
},
{
claim = "Preserve the single-vault structural invariants of ADR-017",
detail = "Single vault_id, single OCI artifact, single restic repo, single lock, single dispatcher subcommand surface. The schema delta is two optional fields (recipient_groups + recipient_rules). Most code paths require zero modification — additivity over existing helpers is the migration story for projects already on ADR-017.",
},
{
claim = "Project.ncl is the single source of truth for recipient sets",
detail = "Direct sops mutations via secrets-add-key and secrets-remove-key are forbidden in declarative mode (recipes error explicitly). The reason: any direct mutation diverges from project.ncl, and the next secrets-rekey would silently revert. Forcing edits through git via project.ncl makes recipient changes auditable and reproducible across machines.",
},
{
claim = "Honest trade-off: per-file routing protects against accidental cross-decryption, not against encrypted-byte visibility",
detail = "ClientA's lead with their .kage cannot decrypt clientB-*.sops.yaml files — sops rejects decryption when the recipient set excludes the actor. But all encrypted files are layers in the same OCI artifact; clientA SEES that clientB-* files exist. For HARD isolation (separate master keys, separate restic repos, compliance-grade separation surviving accidental cross-decryption), multi-vault remains the future option — a separate ADR captures that work when a real case requires it.",
},
{
claim = "Three adoption templates anchor common patterns without forcing them",
detail = "single-team, multi-tenant, and agent-first templates ship in install/resources/templates/sops/ as copy-paste starting points. The schema fields are optional with sensible defaults — a project may adopt any pattern, mix them, or skip templates entirely while still being a valid consumer of ADR-017 + ADR-019.",
},
],
consequences = {
positive = [
"Tenant isolation in a single vault, single master key, single OCI artifact — adoption cost minimal.",
"Compatible with all existing ADR-017 enforcement (assert-actor-authorized, assert-target-in-scope, vault lock, impact analysis).",
"Migration from legacy single-set mode is additive: existing projects keep working, new fields opt them into per-file routing.",
"Defense in depth: actor scope (ops + namespaces) + recipient routing — both must permit an operation.",
],
negative = [
"Operators must understand sops creation_rules ordering (first match wins) — surfacing rule conflicts requires care.",
"secrets-add-key and secrets-remove-key behavior diverges between legacy and declarative modes, introducing a mode-aware UX.",
"Cross-tenant visibility of encrypted file paths in the OCI manifest — operationally clientB knows clientA exists in the vault.",
],
},
alternatives_considered = [
{
option = "Multi-vault: project.ncl::sops.vault_id (single string) becomes sops.vaults (record of named SopsConfigs)",
why_rejected ="Cascades through every layer (schema, helpers, recipes, dispatcher), forcing a 12-component refactor and a migration for every existing project to express what one optional schema field accomplishes via sops creation_rules. The HARD isolation it provides (separate master keys, separate restic repos) is rarely required; per-file routing covers the common case while leaving the door open for a future multi-vault ADR if a project genuinely needs filesystem-level separation.",
},
{
option = "Single recipient set + role-based decryption gating in helper code",
why_rejected ="Would require a custom layer over sops and reinvent recipient routing. sops already does it natively via creation_rules. Inventing a parallel mechanism doubles the surface and breaks composition with sops tooling (sops --decrypt, updatekeys).",
},
{
option = "Externalize tenant credentials to an external secret manager (e.g. HashiCorp Vault)",
why_rejected ="Adds an external runtime dependency that contradicts the ADR-017 invariant of self-contained, distribution-via-OCI credentials. Reasonable for projects that already operate such a system, but inappropriate as the default ontoref pattern.",
},
],
constraints = [
{
id = "rules-imply-groups-defined",
claim = "Every group referenced in recipient_rules must be declared in recipient_groups",
scope = "all projects with sops.recipient_rules non-empty",
severity = 'Hard,
check = {
tag = 'NuCmd,
cmd = "ore secrets audit --check recipient-routing-coherent",
},
rationale = "An undeclared group resolves to an empty recipient list, producing files encrypted to nobody. Catch at audit time before push.",
},
{
id = "no-empty-group-on-active-rule",
claim = "A rule whose group union resolves to zero recipients is rejected at bootstrap and rekey",
scope = "all projects with sops.recipient_rules non-empty",
severity = 'Hard,
check = {
tag = 'NuCmd,
cmd = "ore secrets audit --check recipient-routing-coherent",
},
rationale = "Encrypting to zero recipients silently produces an unrecoverable file. Reject at the source rather than waiting for sops to fail at use time.",
},
{
id = "declarative-mode-locks-direct-mutations",
claim = "secrets-add-key and secrets-remove-key recipes must error when project declares recipient_rules; canonical workflow is edit project.ncl + secrets-rekey",
scope = "all projects with sops.recipient_rules non-empty",
severity = 'Hard,
check = {
tag = 'Grep,
paths = ["justfiles/secrets.just"],
pattern = "HAS_RULES.*declarative",
must_be_empty = false,
},
rationale = "Direct sops mutations would diverge from project.ncl, and the next rekey would silently revert them. Forcing the rekey path keeps git as the single source of truth.",
},
{
id = "every-vault-file-matches-a-rule",
claim = "Every *.sops.yaml under <vault_dir>/ must match at least one declared rule when recipient_rules is non-empty",
scope = "all projects with sops.recipient_rules non-empty",
severity = 'Hard,
check = {
tag = 'NuCmd,
cmd = "ore secrets audit --check recipient-routing-coverage",
},
rationale = "sops fails encryption with 'no matching creation rules found' for unmatched paths. Catch the mismatch at audit time, surface which path needs a rule (or which rule needs broadening).",
},
{
id = "multi-vault-not-implemented",
claim = "Projects must not declare a multi-vault structure (e.g. sops.vaults map). Multi-vault adoption requires a new ADR superseding or extending this one",
scope = "all projects with .ontoref/project.ncl",
severity = 'Hard,
check = {
tag = 'Grep,
pattern = "sops.vaults *=",
paths = [".ontoref/project.ncl"],
must_be_empty = true,
},
rationale = "Premature multi-vault implementation without a justifying use case generates schema and helper debt across 12+ components. The constraint is structural: any project that hits a real HARD-isolation requirement must capture it in a new ADR before adopting multi-vault.",
},
{
id = "templates-discoverable",
claim = "Three adoption templates (single-team, multi-tenant, agent-first) live under install/resources/templates/sops/ and are referenced by qa.ncl::credential-vault-templates",
scope = "ontoref protocol layer",
severity = 'Soft,
check = {
tag = 'FileExists,
paths = [
"install/resources/templates/sops/single-team/project.ncl.snippet",
"install/resources/templates/sops/multi-tenant/project.ncl.snippet",
"install/resources/templates/sops/agent-first/project.ncl.snippet",
],
},
rationale = "Without templates, adoption requires reading the schema, sops docs, and the FAQ — too high a friction. Templates are not mandatory but their absence is a soft signal of protocol decay.",
},
],
related_adrs = [
"adr-017-registry-credential-vault-model",
"adr-015-mcp-tool-inventory-auto-derive",
],
ontology_check = {
decision_string = "tenant isolation within a single credential vault is expressed via sops creation_rules driven by project.ncl::sops.recipient_groups + recipient_rules; multi-vault is explicitly out of scope; project.ncl is the single source of truth for recipient sets and direct sops mutations are forbidden in declarative mode",
invariants_at_risk = ["protocol-not-runtime"],
verdict = 'Safe,
},
}

View file

@ -0,0 +1,221 @@
let d = import "defaults.ncl" in
d.make_adr {
id = "adr-020",
title = "Three-Layer Model for Project Ontoref Instances",
status = 'Proposed,
date = "2026-05-03",
context = m%"
Multiple projects now adopt the ontoref protocol (lian-build is the explicit
external case under bl-002 / bl-008). Field experience across ontoref +
lian-build + provisioning shows that an ontoref-onboarded project's
repository carries TWO distinct ontoref-shaped layers, while a third layer
exists outside the project (in caller repositories). Without codification,
adopters re-derive the model per project, mix layers accidentally, and lose
boundaries silently:
- Layer 3 content (caller-side cabling) drifts into a project's qa.ncl,
making the FAQ a how-to-deploy guide for one specific caller.
- Layer 2 schemas live under reflection/ as 'project notes', drifting
from the binary because they're not on the contract path.
- Layer 1 architectural rationale ends up in catalog/domains/<id>/
contract.ncl, where consumers expecting a typed shape get prose.
The model has been observed (lian-build/reflection/qa.ncl::lian-build-what-
and-why), described (ontoref/reflection/qa.ncl::ontoref-three-layer-model),
and queued for codification (bl-009). This ADR commits the model with
machine-checkable constraints. Acceptance is gated on the constraints
running clean across the existing ontoref-onboarded projects (ontoref
itself, lian-build, provisioning).
"%,
decision = m%"
A project's ontoref instance has THREE distinct layers — but only the first
two live in the project's own repository:
LAYER 1 — Self-management ontoref (about the project itself)
paths .ontology/ reflection/ adrs/
audience this project's developers and maintainers
purpose describe the project to itself — axioms, FSM dimensions,
binding decisions, open questions, accepted knowledge
presence MANDATORY on every ontoref-onboarded project
LAYER 2 — Specialized domain/mode ontoref (the integration surface)
paths schemas/ catalog/{domains,modes}/
manifest.ncl::registry_provides
audience OTHER projects that want to integrate this project
purpose the contract surface other projects bind to — typed domain
artifacts, orchestration mode artifacts, registry-namespace
claim
presence OPTIONAL but BICONDITIONAL — a project either has all of
{schemas/, catalog/, registry_provides} or none. Half-Layer-2
is a contract violation.
LAYER 3 — Caller-side implementations (NOT in this project)
paths <caller>/extensions/<this-project>/
<caller>/catalog/components/<...>/ (when consuming)
<workspace>/infra/<ws>/integrations/
audience operators and CI of caller projects
presence PER CALLER, NEVER in this project's repo. Cross-references
from this project to Layer 3 are explicit pointers, never
copy-paste.
The three-layer axis is ORTHOGONAL to ADR-018's level hierarchy
(Base/Domain/Instance). A project at any level may have any combination of
Layer 1 (always) and Layer 2 (sometimes). Layer 3 is the boundary outward,
not a property of the project. The 3-layer × 3-level matrix is navigable in
both axes: 'where in the protocol hierarchy' (level) is independent of
'where in the project's repo' (layer).
Cross-layer references inside a project carry an explicit layer-N tag on
qa entries. A qa entry whose primary topic is a Layer N concern wears
the layer-N tag; satellite entries (operational how-to, troubleshooting)
inherit the layer of their anchor without re-tagging.
"%,
rationale = [
{
claim = "Audience separation requires lifecycle separation",
detail = "Layer 1 evolves with the project's decisions (ADR cadence). Layer 2 evolves with the binary (schemas in lock-step with code that produces and consumes them). Layer 3 evolves with caller infrastructure (workspace cabling, deployment changes). Three lifecycles in one namespace produces drift in two of them — observed in pre-codification cases where schemas drifted because they were treated as project notes.",
},
{
claim = "Layer 2 biconditional is what makes a federated peer detectable",
detail = "If catalog/ exists without manifest.ncl::registry_provides, the project has artifacts but no namespace claim — consumers can't resolve where to pull from. If registry_provides exists without catalog/, the project claims a namespace it doesn't fill. Either half-state breaks integration. Treating the pair as biconditional makes 'is this a federated peer?' a single boolean check.",
},
{
claim = "Layer 3 isolation is project-specific in spelling, universal in principle",
detail = "What counts as caller-specific differs per project (lian-build forbids `provisioning_workspace|vapora_|woodpecker_` per its adr-001; another project would forbid different patterns). The protocol-level constraint cannot enumerate; instead, each project carries its own constraint-bearing ADR and the protocol verifies presence of SUCH a constraint indirectly via cross-layer-tag-discipline. Project-specific spelling, ecosystem-wide presence.",
},
{
claim = "Cross-layer tag discipline makes filtering scalable",
detail = "An ontoref instance accumulates dozens to hundreds of qa entries over its life. 'Filter the qa for what other projects can integrate' (Layer 2) becomes a tag query rather than a full-text search. Tagging only anchors keeps the result set small and authoritative; satellites don't dilute the filter.",
},
{
claim = "Orthogonality with ADR-018 lets the model expand without conflict",
detail = "ADR-018's level hierarchy describes WHERE a project sits in the protocol specialization (Base = ontoref itself; Domain = a project-type domain; Instance = a concrete workspace). The three-layer model describes WHERE inside a project's repo content sits (self / integration-surface / caller-side). Different axes; no terminology collision (level vs layer); a 3x3 matrix populated empirically. Treating them as orthogonal avoids re-litigating ADR-018 every time the layer model evolves.",
},
],
consequences = {
positive = [
"ore describe project can statically answer 'is this a federated peer?' (Layer 2 biconditional) and 'is the project minimally onboarded?' (Layer 1 mandatory)",
"Adopters discover the model from documentation (qa::ontoref-three-layer-model) plus this ADR, not by osmosis from existing projects",
"qa-tag-based filtering (layer-1, layer-2) becomes a navigable surface as ecosystem corpus grows",
"The 3-layer × 3-level matrix is explicit, removing a recurring source of design ambiguity (e.g. 'should this go in .ontology or in catalog?')",
"ontoref setup gains a clear acceptance criterion: it produces a Layer 1-compliant scaffold by construction",
],
negative = [
"Existing projects must audit their structure for Layer 1 compliance — typically reflection/qa.ncl and reflection/backlog.ncl are missing on early adopters",
"Six new constraints add per-project CI overhead (4x FileExists, 2x NuCmd) — running cheap but additive across the ecosystem",
"Terminology discipline required: 'layer' (this ADR) vs 'level' (ADR-018) are different concepts and prose must not collapse them",
"Layer 2 biconditional rejects half-states that some projects might want as an interim — projects mid-Layer-2-rollout must either complete or not start until ready",
],
},
alternatives_considered = [
{
option = "Single 'ontoref content' namespace, no layering",
why_rejected = "Observed drift outcomes (Layer 3 in qa, Layer 2 schemas treated as notes, etc.) are caused precisely by absence of layering. The single-namespace alternative is what we're correcting; rejecting it is the entire decision.",
},
{
option = "Two-layer model collapsing self-management with integration-surface",
why_rejected = "lian-build's adoption explicitly experienced the schema-vs-rationale split: schemas/build_directives.ncl (Layer 2 contract) and adrs/adr-001-lian-build-as-standalone.ncl (Layer 1 rationale) have different audiences, different change cadences, different validation rules. Collapsing them produced the FAQ-vs-contract confusion that retiring lian-build/.ontology/FAQ.md addressed. The two-layer alternative re-creates that confusion.",
},
{
option = "Make Layer 3 (caller-side) optionally co-resident with Layer 2 in the producer's repo",
why_rejected = "Creates ambiguity about who owns the cabling. If a producer's repo carries `extensions/<self>/`, who maintains it when a caller's workspace evolves? The producer doesn't know about the caller's infrastructure; the caller can't depend on the producer to update the cabling on its schedule. Co-residence inverts the integration arrow.",
},
{
option = "Numbered layers (Layer 1 / 2 / 3) vs named layers ('self' / 'integration-surface' / 'caller-side')",
why_rejected = "Named layers are more descriptive but verbose; numbered layers are more precise but flat. The compromise (this ADR): use 'Layer N — descriptive name' in prose, 'layer-N' in tags. Tag economy wins; prose retains the descriptive name.",
},
],
constraints = [
{
id = "layer-1-self-ontology-core",
claim = "Every ontoref-onboarded project has .ontology/core.ncl",
scope = ".ontology/",
severity = 'Hard,
check = {
tag = 'FileExists,
path = ".ontology/core.ncl",
present = true,
},
rationale = "Layer 1's irreducible minimum: a project with no axioms, tensions, or practices has no ontoref instance. Overlaps with adr-016's lift-out check by intention — same observable, two architectural decisions resting on it.",
},
{
id = "layer-1-reflection-qa",
claim = "Every ontoref-onboarded project has reflection/qa.ncl",
scope = "reflection/",
severity = 'Hard,
check = {
tag = 'FileExists,
path = "reflection/qa.ncl",
present = true,
},
rationale = "Layer 1's accepted-knowledge surface. A project without qa.ncl cannot accumulate verified Q&A — adopters re-derive answers each time. ontoref setup must scaffold this; existing projects backfill.",
},
{
id = "layer-1-reflection-backlog",
claim = "Every ontoref-onboarded project has reflection/backlog.ncl",
scope = "reflection/",
severity = 'Hard,
check = {
tag = 'FileExists,
path = "reflection/backlog.ncl",
present = true,
},
rationale = "Layer 1's open-question surface. Projects without backlog.ncl bury unresolved alternatives in chat threads, defeating the protocol's routing mechanism (graduates_to). Required so every undecided question has a place to land.",
},
{
id = "layer-1-adrs-directory",
claim = "Every ontoref-onboarded project has an adrs/ directory",
scope = "adrs/",
severity = 'Hard,
check = {
tag = 'FileExists,
path = "adrs/",
present = true,
},
rationale = "Layer 1's binding-decisions slot. The directory may be empty for a brand-new project, but its presence is what makes 'where do ADRs go?' a settled question.",
},
{
id = "layer-2-biconditional",
claim = "A federated-peer catalog (catalog/domains/ or catalog/modes/) exists if and only if manifest.ncl declares registry_provides",
scope = "catalog/domains/, catalog/modes/, manifest.ncl, .ontology/manifest.ncl",
severity = 'Hard,
check = {
tag = 'NuCmd,
cmd = "let fed = (('catalog/domains' | path exists) or ('catalog/modes' | path exists)); let m_root = (try { open manifest.ncl | str contains 'registry_provides' } catch { false }); let m_onto = (try { open .ontology/manifest.ncl | str contains 'registry_provides' } catch { false }); let prv = ($m_root or $m_onto); if $fed == $prv { 0 } else { error make {msg: $'Layer 2 biconditional violated: federated_catalog=($fed) registry_provides=($prv) — half-states advertise integration the consumer cannot complete'} }",
expect_exit = 0,
},
rationale = "Federated-peer status is detectable by a single biconditional. The marker is catalog/domains/ or catalog/modes/ specifically — not catalog/ as a whole, because some projects (provisioning) carry catalog/components/, catalog/providers/, etc. that serve workspace composition rather than federated artifact publication. The manifest may live at root or under .ontology/ depending on project convention; bl-010 (forthcoming) tracks canonicalization of that path. Half-states (one side without the other) are the violation.",
},
{
id = "cross-layer-tag-discipline",
claim = "qa entries whose primary topic is a Layer N concern carry the layer-N tag (anchors only; satellites inherit by association)",
scope = "reflection/qa.ncl",
severity = 'Soft,
check = {
tag = 'NuCmd,
cmd = "let entries = (nickel export reflection/qa.ncl --format json | from json | get entries); let suspicious = ($entries | where {|e| (($e.answer | str contains 'LAYER 1') or ($e.answer | str contains 'LAYER 2')) and ($e.tags | where {|t| ($t | str starts-with 'layer-')} | is-empty) and ($e.id | str contains 'what-and-why')}); if ($suspicious | is-empty) { 0 } else { error make {msg: $'Anchor entries discussing layers must carry layer-N tag: ($suspicious | get id | str join \", \")'} }",
expect_exit = 0,
},
rationale = "Tag-based filtering scales as the corpus grows. Without discipline, querying for 'Layer 2 concerns' returns either too few entries (false negatives because anchors aren't tagged) or too many (false positives because every entry that mentions a layer gets tagged). Anchors-only is the sustainable middle path; this constraint enforces it for the most common drift case.",
},
],
related_adrs = [
"adr-001-protocol-as-standalone-project",
"adr-016-component-lift-out-pattern",
"adr-018-level-hierarchy-mode-resolution-strategy",
],
ontology_check = {
decision_string = "An ontoref-onboarded project's repository carries Layer 1 (mandatory self-management) and optionally Layer 2 (integration surface, biconditional with registry_provides). Layer 3 lives in caller projects, not in the producer. The three-layer axis is orthogonal to ADR-018's level hierarchy.",
invariants_at_risk = [],
verdict = 'Safe,
},
}

View file

@ -0,0 +1,184 @@
# A Two-Week Sprint for Knowledge
## From Agile to Design Thinking
---
In 1986, Hirotaka Takeuchi and Ikujiro Nonaka published "The New New Product Development Game" in the Harvard Business Review, describing how Honda, Canon, and Fuji-Xerox organized teams
to build copiers and automobiles. Jeff Sutherland read that article, combined it with lean manufacturing principles and object oriented programming, and in 1993 adapted it for software
development at Easel Corporation. Ken Schwaber formalized the method at the OOPSLA conference in 1995. By 2001, seventeen software developers gathered at a ski lodge in Snowbird, Utah,
and signed the Agile Manifesto, declaring that they were "uncovering better ways of developing software." Not knowledge or meaning. A manufacturing framework for software.
The Agile Manifesto's own language is explicit about its scope, yet the industry has applied Agile methodology to virtually everything in a technological environment — including knowledge
work, semantic systems and now artificial intelligence. And within the same breath, organizations and experts regale the industry with talk of innovation and the emergence of a new
paradigm in ways of working. Yet those ways of working are locked inside of Agile methodologies and Scrum, designed for the factory floor and the codebase — not for the messy, recursive,
deeply human work of making meaning out of complexity.
Scrum Is a Manufacturing Schedule
Let's return to the Takeuchi and Nonaka paper, where the title—"The New New Product Development Game" describes how product teams at Japanese firms developed physical goods such as the
FX-3500 copier and the Honda City car. They draw on the rugby metaphor because the team "tries to go the distance as a unit, passing the ball back and forth." The unit of delivery is a
working prototype of a material artifact. The feedback loop is a machine that works or fails.
Sutherland's 2014 book, Scrum: The Art of Doing Twice the Work in Half the Time, reinforces this with military and manufacturing case studies such as the F-86 fighter production and
Toyota. Sutherland's book rebuilds the argument with the same assumption—that the thing being built has discrete, inspectable, testable parts that, when finished, are finished.
The 2020 Scrum Guide defines the Increment as "a concrete stepping stone toward the Product Goal," one that is "additive to all prior Increments and thoroughly verified, ensuring that all
Increments work together." These words are sensible if you are building a mobile application, with a login screen and a settings page. But for knowledge systems, these words are
destructive, because the components of a semantic architecture are not additive in the way Scrum dictates. A controlled vocabulary is not a subcomponent of a taxonomy the way a login
screen is a subcomponent of an app. A taxonomy without disciplined, vocabulary control as its underpinnings does not function well, if at all. You cannot ship half a taxonomy and call it
an Increment.
You see, Scrum assumes tame problems that can be time-boxed, estimated with planning poker, decomposed into user stories, tracked on a velocity chart and declared done against a
checklist. If only knowledge were this tame. Knowledge organization is anything but tame. More than fifty-years-old, in 1973 Horst Rittel and Melvin Webber warned that applying the
engineering paradigm uniformly to all problems was "bound to fail." And yet here we are, applying a manufacturing schedule and methodologies to knowledge, expecting that knowledge will
fit into the confines of old ways of working — an artifact of the Third Industrial Revolution imposed on the demands of the Fourth.
In 2016, Klaus Schwab wrote about this historical pattern. The First Industrial Revolution, beginning around 1760, gave us steam and mechanical production. The Second, in the late
nineteenth century, gave us electricity and the assembly line — the world that produced Toyota, lean manufacturing, and the production logic that Scrum still carries as its core ethos.
The Third, beginning in the 1960s, gave us semiconductors, mainframe computing, and eventually the internet — the world in which Agile was born, by software developers, for software
development. The Fourth Industrial Revolution, Schwab argued, is fundamentally different, in kind, as it is defined by the fusion of artificial intelligence, cyber-physical systems and
knowledge technologies that blur the boundaries between the physical, digital and biological worlds.
The problems of this era are not necessarily engineering problems with stable requirements. We are wrestling with problems such as how to ground machine intelligence in meaning, how to
organize evolving knowledge and how to build systems that can explain themselves. These are wicked problems that demand new epistemologies and frameworks for problem solving. Agile may
have been the right methodology for the Third Industrial Revolution but we are no longer in the Third Industrial Revolution. The frameworks have not caught up, and the cost of that lag is
exacerbated every time an organization forces Third Industrial Revolution principles applied to Fourth Industrial Revolution work.
In other words, this is not a product problem or a platform problem—it is learning to embrace new ways of working. This is an infrastructure problem that is governed by epistemology—a
fundamental philosophy pertaining to knowledge.
Why Knowledge Cannot Be Sprinted
An ontology is, per Thomas Gruber's foundational definition, "a specification of a conceptualization." A conceptualization is a whole, not parts of a whole. Specifying half of it produces
logical incoherence, not a minimally viable product. For example, ANSI/NISO Z39.19, the standard for constructing controlled vocabularies, requires resolving synonymy, homonymy and scope
before terms enter production, because any inconsistency propagates downstream into every subsequent use. SKOS, the W3C standard for encoding knowledge organization systems, requires
that hierarchical relationships form coherent structures with transitive closure and OWL 2 requires that class axioms be satisfiable. These are preconditions, necessary for logical
systems to reason and express meaning.
In practice, I have watched organizations attempt Agile delivery of knowledge systems in two patterns and neither ends well. In the first, the team holds stand-ups, ceremonies and
retrospectives. The taxonomist brings tickets and adds fifteen terms to the product vocabulary. Tickets close and a velocity chart goes up. Nothing is integrated, because integration is
the hard part and integration does not fit into two weeks so that goes to the backlog. Six months later, the vocabulary is a collection of inconsistently scoped terms, the ontology has
orphan classes and the knowledge graph — if it exists — cannot reliably answer queries. But the burndown chart is immaculate and leadership is blindly satisfied because boxes were
checked.
In the second pattern, the team borrows from other methodologies while still calling it Agile. The taxonomist conducts multi-week domain analysis and stakeholder interviews — that is
ethnography. She does card sorting and concept modeling — that is design research. She iterates with subject matter experts — that is participatory design. She ships, eventually, a
coherent model. The sprint cadence was irrelevant to the work. Martin Fowler named this phenomenon in his 2018 keynote: "faux-agile" — the Agile Industrial Complex.
Ron Jeffries, another original signatory of the Agile Manifesto, told developers the same year, 2018, to abandon the word entirely. Dave Thomas declared in 2014 that "Agile" had become "a
magnet for anyone with points to espouse, hours to bill or products to sell." When three of the seventeen authors of a framework say it has been hollowed out, one has to wonder if Agile
is appropriate for all technology work and if it is time to revisit ways of working.
Innovation Does Not Fit in a Backlog
The deeper issue is that Agile and Scrum were never designed to inspire or foster innovation. They were designed to deliver known requirements with increasing efficiency. The Scrum
Guide's entire apparatus — product backlog, sprint planning, Definition of Done — presupposes that the problem has been formulated by a product owner who can write it in a user story. But
knowledge work, like design, is wicked. Rittel and Webber's first property of a wicked problem is that "There is no definitive formulation." The second property states that there is no
stopping rule. And the fifth property illuminates that every solution is a "one-shot operation" because there is no opportunity to learn by trial-and-error without consequence.
Given the nature of wicked problems, it would seem that Agile is ill fitted for wicked problems. And for that matter, ill fitted for the dynamic nature of knowledge, where knowledge
infrastructures are never one-shot operations, necessitate stopping rules and without a doubt defies definitive formulations.
Therefore, knowledge organization in an enterprise domain is wicked in exactly this sense. The problem of how to represent "customer" in a telecommunications company's semantic model has
no pre-existing correct answer. Its formulation depends on who is doing what with the representation, and that changes as you build. The scope of the vocabulary shifts as you interview
stakeholders. The taxonomy reorganizes as you encounter edge cases. The ontology's axioms tighten or loosen as the reasoner surfaces contradictions. Don't be mistaken—this does not bend
to the Agile principle of welcoming changing requirements, as the requirements are constituted through the work. The problem is under construction, often evolving, during the solving.
Organizations that manage all work through Scrum are, in effect, optimizing for predictability in domains that demand exploration. The results resist innovation and take on the appearance
of productivity. Velocity charts rise while the underlying intellectual architecture is flattened into bit sized deployments, uncoupled from the continuum that is knowledge.
Design Thinking Belongs in the Swamp
Donald Schön drew the distinction that "In the varied topography of professional practice, there is a high, hard ground where practitioners can make effective use of research-based theory
and technique, and there is a swampy lowland where situations are confusing 'messes' incapable of technical solution." Agile lives on the high ground while knowledge work lives in the
swamp. Schön's term for how competent practitioners navigate the swamp was "reflection-in-action" — the iterative, tacit recalibration that happens while the work is happening.
Design thinking, in the lineage of Herbert Simon, Richard Buchanan and Schön, is the methodology of the swamp. It assumes the problem is under construction. Its five phases — empathize,
define, ideate, prototype, test — are not a linear pipeline but a recursive, iterative process that expects the definition of the problem to change as understanding deepens.
Research in the Handbook of Human-Centered Artificial Intelligence confirms this alignment—integrating design thinking into AI development makes systems more user-focused, iterative and
impactful, emphasizing data governance, transparency, explainability and bias mitigation. Carnegie Mellon University's Tepper School of Business now teaches a dedicated program on design
thinking with AI, arguing that the combination addresses "complex, systemic problems" that linear frameworks cannot manage successfully. IBM's enterprise design thinking practice,
developed over two decades, has been applied across client engagements precisely it keeps human experience at the center, while technology evolves underneath — design thinking is
"timeless," while technology is "timestamped."
A 2026 study in Scientific Reports examined the relationship between higher order thinking, generative AI chatbot use and engineering creativity, finding that higher order thinking had
significantly greater importance for creative outcomes than AI tool use alone — reinforcing that cognitive depth, not tool velocity, drives innovation. Research in the Proceedings of the
Design Society further argues that generative AI is shifting the designer's role from executor to paradigm-setter, making design thinking more essential as AI handles production tasks
while humans must guide framing, ethics and meaning.
To be clear, these frameworks are not different flavors of the same methodology. Scrum and design thinking represent totally different epistemologies. Scrum assumes the problem was handed
to you while design thinking assumes the problem must be found and wrestled with.
Is AI a Knowledge Tool?
This brings us to the question the industry avoids and I, for one, have been itching to ask. Is AI a knowledge tool? How do you classify it?
A large language model generates plausible sequences of text. Returning to the Bender, Gebru, McMillan-Major, and Mitchell paper mentioned earlier, the authors defined a language model as
a system for stitching together linguistic forms according to probabilistic patterns, "without any reference to meaning." Without reference to meaning, as LLMs out-of-the-box are absent
of external grounding. They optimize for plausibility, not truth and therefore as a standalone, can be best categorized as an information-processing tool.
It becomes a knowledge tool only when grounded in a semantic layer — controlled vocabularies, taxonomies, ontologies, knowledge graphs — that provides reference and provenance. The
industry's own corrective measures and associated marketplace confirms this as fact. Retrieval-augmented generation, knowledge graphs for LLMs and what recent discourse calls "context
engineering" are, underneath the packaging, the semantic infrastructure that should have been built first. But it was bolted on after the fact to prevent hallucination that was pretty
much guaranteed, the moment the model was deployed without one.
The RAG paper from 2020 called out this problem, identifying that parametric memory is opaque and cannot be updated and that retrieval from structured sources is how provenance is
captured and encoded. But it seems the industry has a memory problem or it was too early for most to hear the message.
Ironically, provenance is a core element of knowledge management and ontology work, as provenance is a principled tenant of the discipline and problem space. Without provenance, knowledge
cannot exist. Organizations are only now discovering they need it, because their generative systems cannot explain themselves. Call it lineage, call it traces. But if you are building
for knowledge, call it provenance, as provenance includes traces, lineage, citations, the temporal and the spatial.
So if generative AI is an information processing tool, I posture that for AI to become a knowledge tool, there must be provenance and descriptive logic. And to be frank, nobody sprints
their way into provenance as it is architected and sustained.
Knowledge Is a Spiral, Not a Sprint
The irony does not escape the moment. The same Nonaka who co-authored the 1986 paper that inspired Scrum, went on to develop the SECI model of knowledge creation in 1995, describing
knowledge as a spiral between tacit and explicit forms, moving through socialization, externalization, combination and internalization.
The spiral does not have an end, as is true with spirals, emblematic of life. Each rotation creates new knowledge that becomes the substrate for the next rotation. There is no Definition
of Done because the definition of what is being done is itself being constructed. Scrum took the team structure from Nonaka's first paper and discarded his next decade of work because it
did not fit neatly with Scrum. The mechanisms of knowledge work, as Nonaka spent a decade arguing, are metaphor, tacit skill, apprenticeship, shared space and the patient conversion of
hunches into models and models back into practice.
The Ontology Pipeline™ mirrors this spiral. You start with a controlled vocabulary — a disciplined process involving agreements, as to what terms mean. You mature it into a taxonomy by
introducing hierarchy, structuring terms into parent-child relations. Thereafter, the taxonomy is matured into a thesaurus by adding associative relationships and synonymy, encoded in
SKOS. The thesaurus is baseline work from which an ontology is developed, adding formal semantics using OWL and RDF. You architect a knowledge graph, assembling components to include the
knowledge assets developed through the Ontology Pipeline™ iterative framework.
Each stage prepares the ground for the next and stages cannot be skipped or bypassed. You cannot ship the knowledge graph without an ontology, and the ontology must be logically
consistent and coherent to make sense of data. A knowledge graph without a vocabulary control risks becoming a confident liar or failing to work with natural language. Because knowledge
assets are dimensional interconnected, knowledge is indeed a spiral, unable to be contained by manufacturing logic.
The Pattern and the Cure
Organizations budget for platforms — Confluence, SharePoint, LLM subscriptions, vector databases, a RAG vendor. But they do not budget for descriptive logic and knowledge, perhaps because
the problem is not solved by a single tool and is seemingly too amorphous in an Agile, data centric world. The controlled vocabulary is cut unless it delivers value for the database.
Often, the taxonomy is cut because it does not demo well and its value sits in the plumbing, delivering its punch over time. The result is an ever-growing pile of documents with a chatbot
stapled to the front, confidently answering questions in the voice of a system that has no reference for any of the terms sprinkled throughout the documents.
Then the system hallucinates. It contradicts itself across sessions. It surfaces outdated policy as current and surfaces private outlook messages from deactivated employees. I have seen
this IRL more times than I can count. And the optics are worse when a vendor tool is responsible for revealing private chats and email transactions. Usually the executive sponsor asks why
the investment has not worked, ignoring the evidence of faux pas such as the surfacing of private communications. All in a days work, when burndown charts and velocity set the precedence
for ways of working and reward systems.
Executives ignore the fact that the system was never grounded. It was never grounded because grounding requires the slow, expensive, disciplined work of knowledge organization — work that
does not fit into a sprint, does not demo well and fails to produce a velocity chart. The organization chooses to bypass investment in the descriptive knowledge apparatus because the
lines are too blurry to clearly define according to Agile speak. Because knowledge is infrastructure, not a product or a platform. Knowledge is the gas in the tank, the fuel for meaning,
the material that justifies logical reasoning.
Design thinking gives that knowledge work a home. It is comfortable with ambiguity. It treats problem discovery as an equal to problem solving. It certainly does not demand a shippable
increment every fourteen days. Instead, design thinking demands that you understand what you are building before you build it — and that you keep revising that understanding as the work
proceeds. Library and information science, with over a century of standards for knowledge organization, provides the methods while design thinking provides the epistemology. Together,
they offer what Agile never could—a framework suited to the nature of knowledge itself.
A two-week sprint to deliver what? If you are building knowledge systems, two weeks will get you nothing coherent. Knowledge is not built fast and broken into shape. It is built
carefully, with provenance, governance and patience. Knowledge is not a product to be manufactured and no—you cannot buy off-the-shelf knowledge that perfectly matches a domain or
organization. Knowledge is nuanced, specific to each organization, domain and person.
In the spirit of VC-speak, knowledge is the moat, and that moat is not Agile.
---
By Jessica Talisman — Intentional Arrangement (Substack)

View file

@ -0,0 +1,905 @@
▐▛███▜▌ Claude Code v2.1.119
▝▜█████▛▘ Sonnet 4.6 · Claude Max
▘▘ ▝▝ /Users/Akasha/Development/ontoref
puedes sacar el texto de este contenido ?
<div class="available-content reader2-post-content"><div dir="auto"
class="body markup"><div data-component-name="SubscribeWidget"
class="subscribe-widget is-signed-up"><div class="pencraft pc-reset
button-wrapper"><div class="pencraft pc-display-flex pc-justifyContent-center
pc-reset"><button tabindex="0" type="button"
data-href="https://jessicatalisman.substack.com/subscribe?utm_medium=web&amp;
utm_source=subscribe-widget&amp;utm_content=194462271&amp;next=https%3A%2F%2F
substack.com%2F%40jessicatalisman%2Fp-194462271" class="pencraft pc-reset
pencraft buttonBase-GK1x3M buttonText-X0uSmG buttonStyle-r7yGCK
priority_primary-RfbeYt size_md-gCDS3o"><span>Upgrade to
paid</span></button></div></div></div><p><span>In 1986, Hirotaka Takeuchi and
Ikujiro Nonaka published “The New New Product Development Game” in the
</span><em>Harvard Business Review</em><span>, describing how Honda, Canon,
and Fuji-Xerox organized teams to build copiers and automobiles.</span><span
data-state="closed" style="min-width: 0px;"><a
data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1"
href="#footnote-1" target="_self" class="footnote-anchor">1</a></span><span>
Jeff Sutherland read that article, combined it with lean manufacturing
principles and object oriented programming, and in 1993 adapted it for
software development at Easel Corporation. Ken Schwaber formalized the method
at the OOPSLA conference in 1995.</span><span data-state="closed"
style="min-width: 0px;"><a data-component-name="FootnoteAnchorToDOM"
id="footnote-anchor-2" href="#footnote-2" target="_self"
class="footnote-anchor">2</a></span><span> By 2001, seventeen software
developers gathered at a ski lodge in Snowbird, Utah, and signed the Agile
Manifesto, declaring that they were “uncovering better ways of developing
software.”</span><span data-state="closed" style="min-width: 0px;"><a
data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3"
href="#footnote-3" target="_self" class="footnote-anchor">3</a></span><span>
Not knowledge or meaning. A manufacturing framework for
software.</span></p><div class="captioned-image-container"><figure><a
target="_blank" href="https://substackcdn.com/image/fetch/$s_!c-hA!,f_auto,q_
auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws
.com%2Fpublic%2Fimages%2F66254527-6290-4907-9186-3be299a56c91_664x861.jpeg"
data-component-name="Image2ToDOM" class="image-link image2
is-viewable-img"><div class="image2-
──── (0 lines hidden) ───────────────────────────────────────────────────────
paddingBottom-0 pc-alignItems-center pc-reset"><a
href="https://jessicatalisman.substack.com/p/the-semantics-of-semantics"
class="pencraft pc-reset align-center-y7ZD4w line-height-20-t4M0El
font-text-qe4AeH size-13-hZTUKr weight-medium-fw81nC reset-IxiVJZ"><div
class="pencraft pc-display-flex pc-gap-8 pc-alignItems-center pc-reset
link-HREYZo"><span class="pencraft pc-reset color-accent-BVX_7M
line-height-20-t4M0El font-text-qe4AeH size-14-MLPa7j weight-semibold-uqA4FV
reset-IxiVJZ">Read full story</span><svg xmlns="http://www.w3.org/2000/svg"
width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide
lucide-arrow-right"><path d="M5 12h14"></path><path d="m12 5 7 7-7
7"></path></svg></div></a></div></div></div><p></p></div></div></div><div
class="pencraft pc-display-contents pc-reset dark-theme"><div class="pencraft
pc-display-flex pc-flexDirection-column pc-gap-4 pc-padding-4
pc-justifyContent-center pc-boxShadow-md pc-reset outline-detail-vcQLyr
pc-borderRadius-md popover-Q5L48i " style="top: 208.797px; left:
557.645px;"><button tabindex="0" type="button" class="pencraft pc-reset
pencraft button-r00Nrm buttonBase-GK1x3M"><svg
xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
fill="none" stroke="var(--color-utility-white)" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="lucide
lucide-refresh-cw"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21
8"></path><path d="M21 3v5h-5"></path><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75
0 0 1-6.74-2.74L3 16"></path><path d="M8 16H3v5"></path></svg><div
class="pencraft pc-reset color-vibrance-primary-KHCdqV line-height-20-t4M0El
font-text-qe4AeH size-13-hZTUKr weight-medium-fw81nC
reset-IxiVJZ">Restack</div></button><button tabindex="0" type="button"
class="pencraft pc-reset pencraft button-r00Nrm buttonBase-GK1x3M"><svg
xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
fill="none" stroke="var(--color-utility-white)" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" class="lucide
lucide-share"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0
2-2v-8"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12"
x2="12" y1="2" y2="15"></line></svg><div class="pencraft pc-reset
color-vibrance-primary-KHCdqV line-height-20-t4M0El font-text-qe4AeH
size-13-hZTUKr weight-medium-fw81nC
reset-IxiVJZ">Share</div></button></div></div></div>
⏺ Here's the plain text extracted from the HTML:
---
In 1986, Hirotaka Takeuchi and Ikujiro Nonaka published "The New New Product
Development Game" in the Harvard Business Review, describing how Honda, Canon,
and Fuji-Xerox organized teams to build copiers and automobiles. Jeff
Sutherland read that article, combined it with lean manufacturing principles
and object oriented programming, and in 1993 adapted it for software
development at Easel Corporation. Ken Schwaber formalized the method at the
OOPSLA conference in 1995. By 2001, seventeen software developers gathered at
a ski lodge in Snowbird, Utah, and signed the Agile Manifesto, declaring that
they were "uncovering better ways of developing software." Not knowledge or
meaning. A manufacturing framework for software.
The Agile Manifesto's own language is explicit about its scope, yet the
industry has applied Agile methodology to virtually everything in a
technological environment — including knowledge work, semantic systems and now
artificial intelligence. And within the same breath, organizations and
experts regale the industry with talk of innovation and the emergence of a new
paradigm in ways of working. Yet those ways of working are locked inside of
Agile methodologies and Scrum, designed for the factory floor and the codebase
— not for the messy, recursive, deeply human work of making meaning out of
complexity.
Scrum Is a Manufacturing Schedule
Let's return to the Takeuchi and Nonaka paper, where the title—"The New New
Product Development Game" describes how product teams at Japanese firms
developed physical goods such as the FX-3500 copier and the Honda City car.
They draw on the rugby metaphor because the team "tries to go the distance as
a unit, passing the ball back and forth." The unit of delivery is a working
prototype of a material artifact. The feedback loop is a machine that works or
fails.
Sutherland's 2014 book, Scrum: The Art of Doing Twice the Work in Half the
Time, reinforces this with military and manufacturing case studies such as the
F-86 fighter production and Toyota. Sutherland's book rebuilds the argument
with the same assumption—that the thing being built has discrete, inspectable,
testable parts that, when finished, are finished.
The 2020 Scrum Guide defines the Increment as "a concrete stepping stone
toward the Product Goal," one that is "additive to all prior Increments and
thoroughly verified, ensuring that all Increments work together." These words
are sensible if you are building a mobile application, with a login screen and
a settings page. But for knowledge systems, these words are destructive,
because the components of a semantic architecture are not additive in the way
Scrum dictates. A controlled vocabulary is not a subcomponent of a taxonomy
the way a login screen is a subcomponent of an app. A taxonomy without
disciplined, vocabulary control as its underpinnings does not function well,
if at all. You cannot ship half a taxonomy and call it an Increment.
You see, Scrum assumes tame problems that can be time-boxed, estimated with
planning poker, decomposed into user stories, tracked on a velocity chart and
declared done against a checklist. If only knowledge were this tame. Knowledge
organization is anything but tame. More than fifty-years-old, in 1973 Horst
Rittel and Melvin Webber warned that applying the engineering paradigm
uniformly to all problems was "bound to fail." And yet here we are, applying a
manufacturing schedule and methodologies to knowledge, expecting that
knowledge will fit into the confines of old ways of working — an artifact of
the Third Industrial Revolution imposed on the demands of the Fourth.
In 2016, Klaus Schwab wrote about this historical pattern. The First
Industrial Revolution, beginning around 1760, gave us steam and mechanical
production. The Second, in the late nineteenth century, gave us electricity
and the assembly line — the world that produced Toyota, lean manufacturing,
and the production logic that Scrum still carries as its core ethos. The
Third, beginning in the 1960s, gave us semiconductors, mainframe computing,
and eventually the internet — the world in which Agile was born, by software
developers, for software development. The Fourth Industrial Revolution, Schwab
argued, is fundamentally different, in kind, as it is defined by the fusion
of artificial intelligence, cyber-physical systems and knowledge technologies
that blur the boundaries between the physical, digital and biological worlds.
The problems of this era are not necessarily engineering problems with stable
requirements. We are wrestling with problems such as how to ground machine
intelligence in meaning, how to organize evolving knowledge and how to build
systems that can explain themselves. These are wicked problems that demand new
epistemologies and frameworks for problem solving. Agile may have been the
right methodology for the Third Industrial Revolution but we are no longer in
the Third Industrial Revolution. The frameworks have not caught up, and the
cost of that lag is exacerbated every time an organization forces Third
Industrial Revolution principles applied to Fourth Industrial Revolution work.
In other words, this is not a product problem or a platform problem—it is
learning to embrace new ways of working. This is an infrastructure problem
that is governed by epistemology—a fundamental philosophy pertaining to
knowledge.
Why Knowledge Cannot Be Sprinted
An ontology is, per Thomas Gruber's foundational definition, "a specification
of a conceptualization." A conceptualization is a whole, not parts of a whole.
Specifying half of it produces logical incoherence, not a minimally viable
product. For example, ANSI/NISO Z39.19, the standard for constructing
controlled vocabularies, requires resolving synonymy, homonymy and scope
before terms enter production, because any inconsistency propagates downstream
into every subsequent use. SKOS, the W3C standard for encoding knowledge
organization systems, requires that hierarchical relationships form coherent
structures with transitive closure and OWL 2 requires that class axioms be
satisfiable. These are preconditions, necessary for logical systems to reason
and express meaning.
In practice, I have watched organizations attempt Agile delivery of knowledge
systems in two patterns and neither ends well. In the first, the team holds
stand-ups, ceremonies and retrospectives. The taxonomist brings tickets and
adds fifteen terms to the product vocabulary. Tickets close and a velocity
chart goes up. Nothing is integrated, because integration is the hard part and
integration does not fit into two weeks so that goes to the backlog. Six
months later, the vocabulary is a collection of inconsistently scoped terms,
the ontology has orphan classes and the knowledge graph — if it exists —
cannot reliably answer queries. But the burndown chart is immaculate and
leadership is blindly satisfied because boxes were checked.
In the second pattern, the team borrows from other methodologies while still
calling it Agile. The taxonomist conducts multi-week domain analysis and
stakeholder interviews — that is ethnography. She does card sorting and
concept modeling — that is design research. She iterates with subject matter
experts — that is participatory design. She ships, eventually, a coherent
model. The sprint cadence was irrelevant to the work. Martin Fowler named this
phenomenon in his 2018 keynote: "faux-agile" — the Agile Industrial Complex.
Ron Jeffries, another original signatory of the Agile Manifesto, told
developers the same year, 2018, to abandon the word entirely. Dave Thomas
declared in 2014 that "Agile" had become "a magnet for anyone with points to
espouse, hours to bill or products to sell." When three of the seventeen
authors of a framework say it has been hollowed out, one has to wonder if
Agile is appropriate for all technology work and if it is time to revisit ways
of working.
Innovation Does Not Fit in a Backlog
The deeper issue is that Agile and Scrum were never designed to inspire or
foster innovation. They were designed to deliver known requirements with
increasing efficiency. The Scrum Guide's entire apparatus — product backlog,
sprint planning, Definition of Done — presupposes that the problem has been
formulated by a product owner who can write it in a user story. But knowledge
work, like design, is wicked. Rittel and Webber's first property of a wicked
problem is that "There is no definitive formulation." The second property
states that there is no stopping rule. And the fifth property illuminates that
every solution is a "one-shot operation" because there is no opportunity to
learn by trial-and-error without consequence.
Given the nature of wicked problems, it would seem that Agile is ill fitted
for wicked problems. And for that matter, ill fitted for the dynamic nature of
knowledge, where knowledge infrastructures are never one-shot operations,
necessitate stopping rules and without a doubt defies definitive formulations.
Therefore, knowledge organization in an enterprise domain is wicked in exactly
this sense. The problem of how to represent "customer" in a
telecommunications company's semantic model has no pre-existing correct
answer. Its formulation depends on who is doing what with the representation,
and that changes as you build. The scope of the vocabulary shifts as you
interview stakeholders. The taxonomy reorganizes as you encounter edge cases.
The ontology's axioms tighten or loosen as the reasoner surfaces
contradictions. Don't be mistaken—this does not bend to the Agile principle of
welcoming changing requirements, as the requirements are constituted through
the work. The problem is under construction, often evolving, during the
solving.
Organizations that manage all work through Scrum are, in effect, optimizing
for predictability in domains that demand exploration. The results resist
innovation and take on the appearance of productivity. Velocity charts rise
while the underlying intellectual architecture is flattened into bit sized
deployments, uncoupled from the continuum that is knowledge.
Design Thinking Belongs in the Swamp
Donald Schön drew the distinction that "In the varied topography of
professional practice, there is a high, hard ground where practitioners can
make effective use of research-based theory and technique, and there is a
swampy lowland where situations are confusing 'messes' incapable of technical
solution." Agile lives on the high ground while knowledge work lives in the
swamp. Schön's term for how competent practitioners navigate the swamp was
"reflection-in-action" — the iterative, tacit recalibration that happens while
the work is happening.
Design thinking, in the lineage of Herbert Simon, Richard Buchanan and Schön,
is the methodology of the swamp. It assumes the problem is under construction.
Its five phases — empathize, define, ideate, prototype, test — are not a
linear pipeline but a recursive, iterative process that expects the definition
of the problem to change as understanding deepens.
Research in the Handbook of Human-Centered Artificial Intelligence confirms
this alignment—integrating design thinking into AI development makes systems
more user-focused, iterative and impactful, emphasizing data governance,
transparency, explainability and bias mitigation. Carnegie Mellon University's
Tepper School of Business now teaches a dedicated program on design thinking
with AI, arguing that the combination addresses "complex, systemic problems"
that linear frameworks cannot manage successfully. IBM's enterprise design
thinking practice, developed over two decades, has been applied across client
engagements precisely it keeps human experience at the center, while
technology evolves underneath — design thinking is "timeless," while
technology is "timestamped."
A 2026 study in Scientific Reports examined the relationship between higher
order thinking, generative AI chatbot use and engineering creativity, finding
that higher order thinking had significantly greater importance for creative
outcomes than AI tool use alone — reinforcing that cognitive depth, not tool
velocity, drives innovation. Research in the Proceedings of the Design Society
further argues that generative AI is shifting the designer's role from
executor to paradigm-setter, making design thinking more essential as AI
handles production tasks while humans must guide framing, ethics and meaning.
To be clear, these frameworks are not different flavors of the same
methodology. Scrum and design thinking represent totally different
epistemologies. Scrum assumes the problem was handed to you while design
thinking assumes the problem must be found and wrestled with.
Is AI a Knowledge Tool?
This brings us to the question the industry avoids and I, for one, have been
itching to ask. Is AI a knowledge tool? How do you classify it?
A large language model generates plausible sequences of text. Returning to the
Bender, Gebru, McMillan-Major, and Mitchell paper mentioned earlier, the
authors defined a language model as a system for stitching together linguistic
forms according to probabilistic patterns, "without any reference to
meaning." Without reference to meaning, as LLMs out-of-the-box are absent of
external grounding. They optimize for plausibility, not truth and therefore as
a standalone, can be best categorized as an information-processing tool.
It becomes a knowledge tool only when grounded in a semantic layer —
controlled vocabularies, taxonomies, ontologies, knowledge graphs — that
provides reference and provenance. The industry's own corrective measures and
associated marketplace confirms this as fact. Retrieval-augmented generation,
knowledge graphs for LLMs and what recent discourse calls "context
engineering" are, underneath the packaging, the semantic infrastructure that
should have been built first. But it was bolted on after the fact to prevent
hallucination that was pretty much guaranteed, the moment the model was
deployed without one.
The RAG paper from 2020 called out this problem, identifying that parametric
memory is opaque and cannot be updated and that retrieval from structured
sources is how provenance is captured and encoded. But it seems the industry
has a memory problem or it was too early for most to hear the message.
Ironically, provenance is a core element of knowledge management and ontology
work, as provenance is a principled tenant of the discipline and problem
space. Without provenance, knowledge cannot exist. Organizations are only now
discovering they need it, because their generative systems cannot explain
themselves. Call it lineage, call it traces. But if you are building for
knowledge, call it provenance, as provenance includes traces, lineage,
citations, the temporal and the spatial.
So if generative AI is an information processing tool, I posture that for AI
to become a knowledge tool, there must be provenance and descriptive logic.
And to be frank, nobody sprints their way into provenance as it is architected
and sustained.
Knowledge Is a Spiral, Not a Sprint
The irony does not escape the moment. The same Nonaka who co-authored the 1986
paper that inspired Scrum, went on to develop the SECI model of knowledge
creation in 1995, describing knowledge as a spiral between tacit and explicit
forms, moving through socialization, externalization, combination and
internalization.
The spiral does not have an end, as is true with spirals, emblematic of life.
Each rotation creates new knowledge that becomes the substrate for the next
rotation. There is no Definition of Done because the definition of what is
being done is itself being constructed. Scrum took the team structure from
Nonaka's first paper and discarded his next decade of work because it did not
fit neatly with Scrum. The mechanisms of knowledge work, as Nonaka spent a
decade arguing, are metaphor, tacit skill, apprenticeship, shared space and
the patient conversion of hunches into models and models back into practice.
The Ontology Pipeline™ mirrors this spiral. You start with a controlled
vocabulary — a disciplined process involving agreements, as to what terms
mean. You mature it into a taxonomy by introducing hierarchy, structuring
terms into parent-child relations. Thereafter, the taxonomy is matured into a
thesaurus by adding associative relationships and synonymy, encoded in SKOS.
The thesaurus is baseline work from which an ontology is developed, adding
formal semantics using OWL and RDF. You architect a knowledge graph,
assembling components to include the knowledge assets developed through the
Ontology Pipeline™ iterative framework.
Each stage prepares the ground for the next and stages cannot be skipped or
bypassed. You cannot ship the knowledge graph without an ontology, and the
ontology must be logically consistent and coherent to make sense of data. A
knowledge graph without a vocabulary control risks becoming a confident liar
or failing to work with natural language. Because knowledge assets are
dimensional interconnected, knowledge is indeed a spiral, unable to be
contained by manufacturing logic.
The Pattern and the Cure
Organizations budget for platforms — Confluence, SharePoint, LLM
subscriptions, vector databases, a RAG vendor. But they do not budget for
descriptive logic and knowledge, perhaps because the problem is not solved by
a single tool and is seemingly too amorphous in an Agile, data centric world.
The controlled vocabulary is cut unless it delivers value for the database.
Often, the taxonomy is cut because it does not demo well and its value sits in
the plumbing, delivering its punch over time. The result is an ever-growing
pile of documents with a chatbot stapled to the front, confidently answering
questions in the voice of a system that has no reference for any of the terms
sprinkled throughout the documents.
Then the system hallucinates. It contradicts itself across sessions. It
surfaces outdated policy as current and surfaces private outlook messages from
deactivated employees. I have seen this IRL more times than I can count. And
the optics are worse when a vendor tool is responsible for revealing private
chats and email transactions. Usually the executive sponsor asks why the
investment has not worked, ignoring the evidence of faux pas such as the
surfacing of private communications. All in a days work, when burndown charts
and velocity set the precedence for ways of working and reward systems.
Executives ignore the fact that the system was never grounded. It was never
grounded because grounding requires the slow, expensive, disciplined work of
knowledge organization — work that does not fit into a sprint, does not demo
well and fails to produce a velocity chart. The organization chooses to bypass
investment in the descriptive knowledge apparatus because the lines are too
blurry to clearly define according to Agile speak. Because knowledge is
infrastructure, not a product or a platform. Knowledge is the gas in the tank,
the fuel for meaning, the material that justifies logical reasoning.
Design thinking gives that knowledge work a home. It is comfortable with
ambiguity. It treats problem discovery as an equal to problem solving. It
certainly does not demand a shippable increment every fourteen days. Instead,
design thinking demands that you understand what you are building before you
build it — and that you keep revising that understanding as the work proceeds.
Library and information science, with over a century of standards for
knowledge organization, provides the methods while design thinking provides
the epistemology. Together, they offer what Agile never could—a framework
suited to the nature of knowledge itself.
A two-week sprint to deliver what? If you are building knowledge systems, two
weeks will get you nothing coherent. Knowledge is not built fast and broken
into shape. It is built carefully, with provenance, governance and patience.
Knowledge is not a product to be manufactured and no—you cannot buy
off-the-shelf knowledge that perfectly matches a domain or organization.
Knowledge is nuanced, specific to each organization, domain and person.
In the spirit of VC-speak, knowledge is the moat, and that moat is not Agile.
---
By Jessica Talisman — Intentional Arrangement (Substack)
✻ Brewed for 2m 25s
puedes generar una traducción precisa y elegante del contenido al español
⏺ ---
En 1986, Hirotaka Takeuchi e Ikujiro Nonaka publicaron "The New New Product
Development Game" en Harvard Business Review, describiendo cómo Honda, Canon y
Fuji-Xerox organizaban sus equipos para fabricar copiadoras y automóviles.
Jeff Sutherland leyó ese artículo, lo combinó con los principios de
manufactura lean y la programación orientada a objetos, y en 1993 lo adaptó al
desarrollo de software en Easel Corporation. Ken Schwaber formalizó el método
en la conferencia OOPSLA de 1995. En 2001, diecisiete desarrolladores de
software se reunieron en un albergue de esquí en Snowbird, Utah, y firmaron el
Manifiesto Ágil, declarando que estaban "descubriendo mejores maneras de
desarrollar software." No conocimiento ni significado. Un marco de manufactura
aplicado al software.
El propio lenguaje del Manifiesto Ágil es explícito sobre su alcance, y sin
embargo la industria ha aplicado la metodología ágil a prácticamente todo lo
que existe en un entorno tecnológico — incluyendo el trabajo del conocimiento,
los sistemas semánticos y ahora la inteligencia artificial. Y en el mismo
aliento, organizaciones y expertos colman la industria con discursos sobre
innovación y el surgimiento de un nuevo paradigma en las formas de trabajo.
Sin embargo, esas formas de trabajo permanecen encerradas dentro de las
metodologías ágiles y Scrum, diseñadas para la línea de producción y la base
de código — no para el trabajo profundamente humano, recursivo y desordenado
de construir significado a partir de la complejidad.
Scrum es un calendario de manufactura
Volvamos al artículo de Takeuchi y Nonaka, cuyo título — "The New New Product
Development Game" — describe cómo los equipos de producto en empresas
japonesas desarrollaban bienes físicos como la copiadora FX-3500 y el Honda
City. Los autores recurren a la metáfora del rugby porque el equipo "intenta
recorrer la distancia como una unidad, pasándose el balón de un lado al otro."
La unidad de entrega es un prototipo funcional de un artefacto material. El
ciclo de retroalimentación es una máquina que funciona o falla.
El libro de Sutherland de 2014, Scrum: The Art of Doing Twice the Work in Half
the Time, refuerza esto con casos de estudio militares y de manufactura como
la producción del caza F-86 y Toyota. El libro reconstruye el argumento sobre
la misma premisa: que aquello que se construye tiene partes discretas,
inspeccionables y verificables que, una vez terminadas, están terminadas.
La Guía Scrum de 2020 define el Incremento como "un peldaño concreto hacia el
Objetivo del Producto", uno que es "aditivo respecto a todos los Incrementos
anteriores y rigurosamente verificado, asegurando que todos los Incrementos
funcionen en conjunto." Estas palabras tienen sentido si se está construyendo
una aplicación móvil, con una pantalla de inicio de sesión y una página de
configuración. Pero para los sistemas de conocimiento, estas palabras son
destructivas, porque los componentes de una arquitectura semántica no son
aditivos de la manera en que Scrum prescribe. Un vocabulario controlado no es
un subcomponente de una taxonomía del mismo modo en que una pantalla de inicio
de sesión lo es de una aplicación. Una taxonomía sin un control de
vocabulario disciplinado como fundamento no funciona bien, o directamente no
funciona. No se puede entregar media taxonomía y llamarla un Incremento.
Scrum asume problemas dóciles que pueden enmarcarse en el tiempo, estimarse
con planning poker, descomponerse en historias de usuario, rastrearse en un
gráfico de velocidad y declararse resueltos frente a una lista de
verificación. Si tan solo el conocimiento fuera así de dócil. La organización
del conocimiento está muy lejos de serlo. Con más de cincuenta años de
antigüedad, en 1973 Horst Rittel y Melvin Webber advirtieron que aplicar el
paradigma de la ingeniería de manera uniforme a todos los problemas estaba
"condenado al fracaso." Y sin embargo aquí estamos, aplicando un calendario de
manufactura y sus metodologías al conocimiento, esperando que el conocimiento
quepa en los confines de viejas formas de trabajar — un artefacto de la
Tercera Revolución Industrial impuesto sobre las exigencias de la Cuarta.
En otras palabras, esto no es un problema de producto ni de plataforma — es el
desafío de aprender a abrazar nuevas formas de trabajar. Es un problema de
infraestructura gobernado por la epistemología: una filosofía fundamental
sobre el conocimiento.
Por qué el conocimiento no puede ejecutarse en sprints
Una ontología es, según la definición fundacional de Thomas Gruber, "una
especificación de una conceptualización." Una conceptualización es un todo, no
partes de un todo. Especificar la mitad produce incoherencia lógica, no un
producto mínimamente viable. Por ejemplo, la norma ANSI/NISO Z39.19 para la
construcción de vocabularios controlados exige resolver la sinonimia, la
homonimia y el alcance antes de que los términos entren en producción, porque
cualquier inconsistencia se propaga hacia abajo en cada uso posterior. SKOS,
el estándar del W3C para codificar sistemas de organización del conocimiento,
requiere que las relaciones jerárquicas formen estructuras coherentes con
cierre transitivo, y OWL 2 exige que los axiomas de clase sean satisfacibles.
Estas son precondiciones necesarias para que los sistemas lógicos razonen y
expresen significado.
En la práctica, he observado organizaciones intentando entregar sistemas de
conocimiento de forma ágil en dos patrones, y ninguno termina bien. En el
primero, el equipo celebra stand-ups, ceremonias y retrospectivas. El
taxonomista trae tickets y añade quince términos al vocabulario del producto.
Los tickets se cierran y el gráfico de velocidad sube. Nada se integra, porque
la integración es la parte difícil y no cabe en dos semanas, de modo que va
al backlog. Seis meses después, el vocabulario es una colección de términos
con alcances inconsistentes, la ontología tiene clases huérfanas y el grafo de
conocimiento — si existe — no puede responder consultas de manera confiable.
Pero el burndown chart es impecable y el liderazgo está ciegamente satisfecho
porque las casillas fueron marcadas.
En el segundo patrón, el equipo toma prestado de otras metodologías sin dejar
de llamarlo ágil. La taxonomista conduce análisis de dominio y entrevistas con
partes interesadas durante varias semanas — eso es etnografía. Realiza card
sorting y modelado conceptual — eso es investigación en diseño. Itera con
expertos en la materia — eso es diseño participativo. Finalmente entrega un
modelo coherente. El ritmo del sprint fue irrelevante para el trabajo. Martin
Fowler denominó este fenómeno en su conferencia magistral de 2018:
"faux-agile" — el Complejo Industrial Ágil.
Ron Jeffries, otro signatario original del Manifiesto Ágil, les dijo a los
desarrolladores ese mismo año, 2018, que abandonaran la palabra por completo.
Dave Thomas declaró en 2014 que "Ágil" se había convertido en "un imán para
cualquiera con puntos que exponer, horas que facturar o productos que vender."
Cuando tres de los diecisiete autores de un marco dicen que ha sido vaciado
de contenido, uno tiene que preguntarse si Agile es apropiado para todo el
trabajo tecnológico y si ha llegado el momento de revisar las formas de
trabajar.
La innovación no cabe en un backlog
El problema más profundo es que Agile y Scrum nunca fueron diseñados para
inspirar o fomentar la innovación. Fueron diseñados para entregar requisitos
conocidos con eficiencia creciente. Todo el aparato de la Guía Scrum — product
backlog, sprint planning, Definición de Hecho — presupone que el problema ha
sido formulado por un product owner capaz de escribirlo en una historia de
usuario. Pero el trabajo del conocimiento, como el diseño, es rebelde. La
primera propiedad de un problema retorcido según Rittel y Webber es que "No
existe una formulación definitiva." La segunda propiedad establece que no hay
regla de parada. Y la quinta propiedad ilumina que cada solución es una
"operación de un solo disparo" porque no hay oportunidad de aprender por
ensayo y error sin consecuencias.
Dada la naturaleza de los problemas retorcidos, parecería que Agile está mal
equipado para enfrentarlos. Y por la misma razón, mal equipado para la
naturaleza dinámica del conocimiento, donde las infraestructuras de
conocimiento nunca son operaciones de un solo disparo, requieren reglas de
parada y desafían sin lugar a dudas las formulaciones definitivas.
Por lo tanto, la organización del conocimiento en un dominio empresarial es
retorcida exactamente en este sentido. El problema de cómo representar
"cliente" en el modelo semántico de una empresa de telecomunicaciones no tiene
una respuesta correcta preexistente. Su formulación depende de quién hace qué
con esa representación, y eso cambia a medida que se construye. El alcance
del vocabulario se desplaza conforme se entrevista a las partes interesadas.
La taxonomía se reorganiza al encontrar casos límite. Los axiomas de la
ontología se tensan o aflojan cuando el razonador descubre contradicciones. No
hay que equivocarse — esto no se pliega al principio ágil de bienvenida a los
requisitos cambiantes, porque los requisitos se constituyen a través del
trabajo. El problema está en construcción, evolucionando frecuentemente,
durante su resolución.
Las organizaciones que gestionan todo el trabajo a través de Scrum están, en
efecto, optimizando para la previsibilidad en dominios que exigen exploración.
Los resultados inhiben la innovación y adoptan la apariencia de
productividad. Los gráficos de velocidad suben mientras la arquitectura
intelectual subyacente se aplana en entregas fragmentadas, desconectadas del
continuo que es el conocimiento.
El pensamiento de diseño pertenece al pantano
Donald Schön trazó la distinción de que "En la variada topografía de la
práctica profesional, hay una meseta alta y firme donde los profesionales
pueden hacer uso efectivo de la teoría y la técnica basadas en la
investigación, y hay una tierras bajas pantanosas donde las situaciones son
'enredos' confusos incapaces de solución técnica." Agile vive en la meseta
mientras que el trabajo del conocimiento vive en el pantano. El término de
Schön para describir cómo los profesionales competentes navegan el pantano fue
"reflexión-en-acción" — la recalibración iterativa y tácita que ocurre
mientras el trabajo transcurre.
El pensamiento de diseño, en el linaje de Herbert Simon, Richard Buchanan y
Schön, es la metodología del pantano. Asume que el problema está en
construcción. Sus cinco fases — empatizar, definir, idear, prototipar, probar
— no son un flujo lineal sino un proceso recursivo e iterativo que espera que
la definición del problema cambie a medida que la comprensión se profundiza.
La investigación en el Handbook of Human-Centered Artificial Intelligence
confirma esta alineación — integrar el pensamiento de diseño en el desarrollo
de IA hace que los sistemas sean más centrados en el usuario, iterativos e
impactantes, enfatizando la gobernanza de datos, la transparencia, la
explicabilidad y la mitigación del sesgo. La Tepper School of Business de
Carnegie Mellon University imparte ahora un programa dedicado a pensamiento de
diseño con IA, argumentando que la combinación aborda "problemas complejos y
sistémicos" que los marcos lineales no pueden gestionar con éxito. La práctica
de diseño empresarial de IBM, desarrollada a lo largo de dos décadas, se ha
aplicado en compromisos con clientes precisamente porque mantiene la
experiencia humana en el centro mientras la tecnología evoluciona por debajo —
el pensamiento de diseño es "atemporal", mientras que la tecnología está
"fechada".
Un estudio de 2026 en Scientific Reports examinó la relación entre el
pensamiento de orden superior, el uso de chatbots de IA generativa y la
creatividad en ingeniería, encontrando que el pensamiento de orden superior
tenía una importancia significativamente mayor para los resultados creativos
que el uso de herramientas de IA por sí solo — reforzando que la profundidad
cognitiva, no la velocidad de las herramientas, impulsa la innovación. La
investigación en las Proceedings of the Design Society argumenta además que la
IA generativa está desplazando el rol del diseñador de ejecutor a
configurador de paradigmas, haciendo que el pensamiento de diseño sea más
esencial a medida que la IA gestiona las tareas de producción mientras los
humanos deben guiar el encuadre, la ética y el significado.
Para ser claros, estos marcos no son distintos sabores de la misma
metodología. Scrum y el pensamiento de diseño representan epistemologías
completamente diferentes. Scrum asume que el problema te fue entregado,
mientras que el pensamiento de diseño asume que el problema debe encontrarse y
lidiarse con él.
¿Es la IA una herramienta de conocimiento?
Esto nos lleva a la pregunta que la industria evita y que yo, por mi parte,
llevo tiempo deseando formular. ¿Es la IA una herramienta de conocimiento?
¿Cómo se clasifica?
Un modelo de lenguaje de gran escala genera secuencias plausibles de texto.
Volviendo al artículo de Bender, Gebru, McMillan-Major y Mitchell mencionado
anteriormente, los autores definieron un modelo de lenguaje como un sistema
para ensamblar formas lingüísticas según patrones probabilísticos, "sin
ninguna referencia al significado." Sin referencia al significado, dado que
los LLMs sin configuración adicional carecen de anclaje externo. Optimizan
para la plausibilidad, no para la verdad, y por lo tanto, como sistemas
autónomos, pueden clasificarse mejor como herramientas de procesamiento de
información.
Se convierte en una herramienta de conocimiento únicamente cuando se ancla en
una capa semántica — vocabularios controlados, taxonomías, ontologías, grafos
de conocimiento — que proporciona referencia y procedencia. Las propias
medidas correctivas de la industria y el mercado asociado confirman esto como
un hecho. La generación aumentada por recuperación, los grafos de conocimiento
para LLMs y lo que el discurso reciente denomina "ingeniería de contexto"
son, bajo el empaquetado, la infraestructura semántica que debería haberse
construido primero. Pero fue añadida a posteriori para prevenir alucinaciones
que estaban prácticamente garantizadas desde el momento en que el modelo fue
desplegado sin ella.
El artículo sobre RAG de 2020 señaló este problema, identificando que la
memoria paramétrica es opaca y no puede actualizarse, y que la recuperación
desde fuentes estructuradas es el modo en que la procedencia se captura y
codifica. Pero parece que la industria tiene un problema de memoria, o el
mensaje llegó demasiado pronto para que la mayoría pudiera escucharlo.
Irónicamente, la procedencia es un elemento central de la gestión del
conocimiento y el trabajo ontológico, pues constituye un principio fundacional
de la disciplina y su espacio de problemas. Sin procedencia, el conocimiento
no puede existir. Las organizaciones están descubriendo ahora que la
necesitan, porque sus sistemas generativos no pueden explicarse a sí mismos.
Llámalo linaje, llámalo trazabilidad. Pero si construyes para el conocimiento,
llámalo procedencia, porque la procedencia abarca trazas, linaje, citaciones,
lo temporal y lo espacial.
De modo que si la IA generativa es una herramienta de procesamiento de
información, sostengo que para que la IA se convierta en una herramienta de
conocimiento debe haber procedencia y lógica descriptiva. Y para ser francos,
nadie llega a la procedencia haciendo sprints — esta se arquitecta y se
sostiene.
El conocimiento es una espiral, no un sprint
La ironía no pasa desapercibida en este momento. El mismo Nonaka que
coescribió el artículo de 1986 que inspiró Scrum, desarrolló el modelo SECI de
creación de conocimiento en 1995, describiendo el conocimiento como una
espiral entre formas tácitas y explícitas, que avanza a través de la
socialización, la externalización, la combinación y la internalización.
La espiral no tiene fin, como es propio de las espirales, emblema de la vida.
Cada rotación crea nuevo conocimiento que se convierte en el sustrato de la
siguiente. No existe una Definición de Hecho porque la definición de lo que se
está haciendo está siendo construida en sí misma. Scrum tomó la estructura de
equipo del primer artículo de Nonaka y descartó su siguiente década de
trabajo porque no encajaba limpiamente en Scrum. Los mecanismos del trabajo
del conocimiento, como Nonaka argumentó durante una década, son la metáfora,
la habilidad tácita, el aprendizaje por proximidad, el espacio compartido y la
paciente conversión de intuiciones en modelos y de modelos de vuelta en
práctica.
El Ontology Pipeline™ refleja esta espiral. Se comienza con un vocabulario
controlado — un proceso disciplinado que implica acuerdos sobre lo que
significan los términos. Se madura hacia una taxonomía introduciendo jerarquía
y estructurando los términos en relaciones padre-hijo. Posteriormente, la
taxonomía se madura hacia un tesauro añadiendo relaciones asociativas y
sinonimia, codificadas en SKOS. El tesauro es el trabajo base a partir del
cual se desarrolla una ontología, incorporando semántica formal mediante OWL y
RDF. Se arquitecta un grafo de conocimiento, ensamblando componentes que
incluyen los activos de conocimiento desarrollados a través del marco
iterativo del Ontology Pipeline™.
Cada etapa prepara el terreno para la siguiente y las etapas no pueden
saltarse ni eludirse. No se puede entregar el grafo de conocimiento sin una
ontología, y la ontología debe ser lógicamente consistente y coherente para
dotar de sentido a los datos. Un grafo de conocimiento sin control de
vocabulario arriesga convertirse en un mentiroso convincente o en fallar al
trabajar con lenguaje natural. Porque los activos de conocimiento son
dimensionalmente interdependientes, el conocimiento es efectivamente una
espiral, incapaz de ser contenida por la lógica de la manufactura.
El patrón y el remedio
Las organizaciones presupuestan para plataformas — Confluence, SharePoint,
suscripciones a LLMs, bases de datos vectoriales, un proveedor de RAG. Pero no
presupuestan para la lógica descriptiva y el conocimiento, quizás porque el
problema no se resuelve con una sola herramienta y resulta demasiado amorfo en
un mundo centrado en los datos y en Agile. El vocabulario controlado se
recorta a menos que genere valor inmediato para la base de datos. Con
frecuencia, la taxonomía se recorta porque no hace una buena demostración y su
valor reside en la fontanería, entregando su impacto con el tiempo. El
resultado es una pila siempre creciente de documentos con un chatbot pegado en
la entrada, respondiendo preguntas con confianza en la voz de un sistema que
no tiene referencia para ninguno de los términos dispersos a lo largo de los
documentos.
Entonces el sistema alucina. Se contradice a sí mismo entre sesiones. Presenta
como vigente una política desactualizada y muestra mensajes privados de
Outlook de empleados cuyas cuentas fueron desactivadas. He visto esto en la
vida real más veces de las que puedo contar. Y la percepción es peor cuando
una herramienta de proveedor es responsable de revelar chats y transacciones
de correo privadas. Habitualmente, el patrocinador ejecutivo pregunta por qué
la inversión no ha funcionado, ignorando la evidencia de los errores, como la
exposición de comunicaciones privadas. Trabajo de un día cualquiera, cuando
los burndown charts y la velocidad marcan el precedente de las formas de
trabajar y los sistemas de recompensa.
Los ejecutivos ignoran el hecho de que el sistema nunca fue anclado en el
conocimiento. Nunca lo fue porque anclar requiere el trabajo lento, costoso y
disciplinado de la organización del conocimiento — trabajo que no cabe en un
sprint, que no hace una buena demostración y que no genera un gráfico de
velocidad. La organización elige eludir la inversión en el aparato de
conocimiento descriptivo porque las líneas son demasiado difusas para
definirse claramente según el vocabulario de Agile. Porque el conocimiento es
infraestructura, no un producto ni una plataforma. El conocimiento es la
gasolina del depósito, el combustible del significado, el material que
justifica el razonamiento lógico.
El pensamiento de diseño le da al trabajo del conocimiento un hogar. Es cómodo
con la ambigüedad. Trata el descubrimiento del problema como un igual a la
resolución del problema. Desde luego no exige un incremento entregable cada
catorce días. En cambio, el pensamiento de diseño exige que entiendas lo que
estás construyendo antes de construirlo — y que sigas revisando esa
comprensión a medida que el trabajo avanza. La biblioteconomía y la ciencia de
la información, con más de un siglo de estándares para la organización del
conocimiento, proporcionan los métodos mientras el pensamiento de diseño
proporciona la epistemología. Juntos ofrecen lo que Agile nunca pudo — un
marco adecuado a la naturaleza del conocimiento mismo.
¿Un sprint de dos semanas para entregar qué? Si estás construyendo sistemas de
conocimiento, dos semanas no producirán nada coherente. El conocimiento no se
construye rápido y se moldea a la fuerza. Se construye con cuidado, con
procedencia, gobernanza y paciencia. El conocimiento no es un producto que
pueda manufacturarse y no — no se puede comprar conocimiento estándar que
encaje perfectamente con un dominio u organización. El conocimiento es
matizado, específico a cada organización, dominio y persona.
En el espíritu del argot del capital de riesgo: el conocimiento es el foso, y
ese foso no es Ágil.
---
Por Jessica Talisman — Intentional Arrangement (Substack)
✻ Baked for 2m 38s
Con el título A Two-Week Sprint for Knowledge
From Agile to Design Thinking
Jessica Talisman, MLS
Apr 21, 2026
y su traducción al espanõl
genera documentos en pdf y md en inglés y en español
⏺ Primero verifico si pandoc está disponible para generar los PDFs.
⏺ Bash(which pandoc && pandoc --version | head -1)
 /usr/local/bin/pandoc
pandoc 3.7.0.2
⏺ Bien. Creo los cuatro archivos en /tmp/talisman-article/.
⏺ Bash(mkdir -p /tmp/talisman-article)
 Done
⏺ Write(/tmp/talisman-article/a-two-week-sprint-for-knowledge.md)
 Wrote 101 lines to
../../../../tmp/talisman-article/a-two-week-sprint-for-knowledge.md
2 ## From Agile to Design Thinkinge
4 **Jessica Talisman, MLS**
6 *Apr 21, 2026*
7 ---
9 In 1986, Hirotaka Takeuchi and Ikujiro Nonaka published "The New
escribing how Honda, Canon, and Fuji-Xerox organized teams to bui
ld copiers and automobiles. Jeff Sutherland read that article, co
programming, and in 1993 adapted it for software development at E
A conference in 1995. By 2001, seventeen software developers gath
ered at a ski lodge in Snowbird, Utah, and signed the A
… +364 lines (ctrl+o to expand)
usa typst ?
⏺ Write(/tmp/talisman-article/un-sprint-de-dos-semanas-para-el-conocimiento.md)
 Wrote 101 lines to ../../../../tmp/talisman-article/un-sprint-de-dos-s
emanas-para-el-conocimiento.md
2 ## De Agile al Pensamiento de Diseñoonocimiento
4 **Jessica Talisman, MLS**
6 *21 de abril de 2026*
7 ---
9 En 1986, Hirotaka Takeuchi e Ikujiro Nonaka publicaron "The New N
biendo cómo Honda, Canon y Fuji-Xerox organizaban sus equipos par
a fabricar copiadoras y automóviles. Jeff Sutherland leyó ese art
gramación orientada a objetos, y en 1993 lo adaptó al desarrollo
o en la conferencia OOPSLA de 1995. En 2001, diecisiete desarroll
adores de software se reuniero
… +407 lines (ctrl+o to expand)
⏺ El usuario preguntó sobre Typst. Verifico si está disponible.
⏺ Bash(which typst && typst --version)
 /opt/homebrew/bin/typst
typst 0.14.2 (unknown hash)
⏺ Typst disponible. Genera PDFs mucho más elegantes que pandoc. Creo los fuentes
.typ y compilo.
⏺ Write(/tmp/talisman-article/en.typ)
 Wrote 160 lines to ../../../../tmp/talisman-article/en.typ
1 #set document(
2 title: "A Two-Week Sprint for Knowledge",
3 author: "Jessica Talisman, MLS",
4 )
5
6 #set page(
7 paper: "a4",
8 margin: (top: 2.8cm, bottom: 3cm, left: 3cm, right: 3cm),
9 footer: context [
10 #set text(size: 9pt, fill: luma(140))
… +433 lines (ctrl+o to expand)
⏺ Write(/tmp/talisman-article/es.typ)
 Wrote 160 lines to ../../../../tmp/talisman-article/es.typ
1 #set document(
2 title: "Un Sprint de Dos Semanas para el Conocimiento",
3 author: "Jessica Talisman, MLS",
4 )
5
6 #set page(
7 paper: "a4",
8 margin: (top: 2.8cm, bottom: 3cm, left: 3cm, right: 3cm),
9 footer: context [
10 #set text(size: 9pt, fill: luma(140))
… +475 lines (ctrl+o to expand)
⏺ Ahora compilo los dos PDFs en paralelo.
⏺ Bash(typst compile /tmp/talisman-article/en.typ
/tmp/talisman-article/a-two-week-sprint-for-knowledge.pdf 2>&1)
 warning: unknown font family: linux libertine
┌─ ../../../../private/tmp/talisman-article/en.typ:17:8
… +2 lines (ctrl+o to expand)
⏺ Bash(typst compile /tmp/talisman-article/es.typ
/tmp/talisman-article/un-sprint-de-dos-semanas-para-el-conocimiento.pdf
2>&1)
 warning: unknown font family: linux libertine
┌─ ../../../../private/tmp/talisman-article/es.typ:17:8
… +2 lines (ctrl+o to expand)
⏺ Solo warnings de fallback de fuente, no errores. Verifico los archivos
generados.
Listed 1 directory (ctrl+o to expand)
⏺ Los 4 archivos están en /tmp/talisman-article/:
┌───────────────────────────────────────────────────┬────────┐
│ Archivo │ Tamaño │
├───────────────────────────────────────────────────┼────────┤
│ a-two-week-sprint-for-knowledge.md │ 20 KB │
├───────────────────────────────────────────────────┼────────┤
│ a-two-week-sprint-for-knowledge.pdf │ 101 KB │
├───────────────────────────────────────────────────┼────────┤
│ un-sprint-de-dos-semanas-para-el-conocimiento.md │ 23 KB │
├───────────────────────────────────────────────────┼────────┤
│ un-sprint-de-dos-semanas-para-el-conocimiento.pdf │ 108 KB │
└───────────────────────────────────────────────────┴────────┘
Sí, usé Typst para los PDFs — produce tipografía profesional con
justificación, guiones automáticos por idioma (lang: "en" / lang: "es"),
paginación, y un bloque de autor limpio. Georgia como fuente serif principal.
Los fuentes .typ también están ahí si quieres ajustar el estilo.
✻ Cogitated for 11m 45s

View file

@ -0,0 +1,69 @@
# Presentation Docs — Index
Material de referencia para posts, slides y presentaciones de ontoref.
---
## Análisis y posicionamiento ontoref
| Archivo | Contenido |
|---|---|
| [ontoref-yin-yang-diferenciacion.md](ontoref-yin-yang-diferenciacion.md) | Por qué Talisman no resuelve el lado operacional. Ontología sin Reflection = buenas intenciones. |
| [ontoref-dags-diferenciacion.md](ontoref-dags-diferenciacion.md) | Por qué los DAGs de ontoref son distintos a CI/CD, compiladores, runbooks. DAGs como conocimiento vs. ejecución. |
| [ontoref-scope-proyecto-infra-personal.md](ontoref-scope-proyecto-infra-personal.md) | Scope completo: proyecto + infraestructura + personal. Stack soberano: jj, Radicle, NATS, CLI/UI/MCP/GraphQL. |
---
## Posts — Serie ontoref (EN + ES)
Publicados en jesusperez.pro/blog. Estado: borradores (`published: false`).
| Post | EN | ES |
|---|---|---|
| AI is a knowledge tool — who keeps it alive? | [en](posts/en/ai-knowledge-tool-who-keeps-it-alive.md) | [es](posts/es/la-ia-herramienta-conocimiento-quien-mantiene-vivo.md) |
| DAGs are everywhere — none know what they are | [en](posts/en/dags-everywhere-none-know-what-they-are.md) | [es](posts/es/dags-en-todos-lados-ninguno-sabe-lo-que-es.md) |
| One protocol, three subjects | [en](posts/en/one-protocol-three-subjects.md) | [es](posts/es/un-protocolo-tres-sujetos.md) |
| Your ontology should live with your code | [en](posts/en/your-ontology-should-live-with-your-code.md) | [es](posts/es/tu-ontologia-deberia-vivir-con-tu-codigo.md) |
---
## Fuentes externas — Talisman (KGC 2026)
| Archivo | Contenido |
|---|---|
| [Steal_This_Deck.pdf](Steal_This_Deck.pdf) | Deck completo. "Stop Betting, Start Building", KGC 2026, Jessica Talisman. |
| [talisman-steal-this-deck-extractions.md](talisman-steal-this-deck-extractions.md) | Extracciones del deck: citas, datos citable, mapeos a ontoref, nota de atribución. |
| [talisman-article/](talisman-article/) | Artículo de Talisman "A Two-Week Sprint for Knowledge" en EN/ES, PDF y Typst. |
---
## Fuentes externas — José Celano
| Archivo | Contenido |
|---|---|
| [JoseCelano_2026-6-08_Ontologías_y_DAGs.pdf](JoseCelano_2026-6-08_Ontologías_y_DAGs.pdf) | Ontologías y DAGs — presentación José Celano, junio 2026. |
---
## Material previo — ontología como filosofía del software
| Archivo | Contenido |
|---|---|
| [ontologia-filosofia-software.md](ontologia-filosofia-software.md) | Documento principal: ontología como fundamento filosófico del software. |
| [ontologia-filosofia-software.pdf](ontologia-filosofia-software.pdf) | Versión PDF. |
| [ontologia-filosofia-software.typ](ontologia-filosofia-software.typ) | Fuente Typst. |
| [ontologia_motor.md](ontologia_motor.md) | Ontología como motor — concepto central. |
| [ontologia_motor_resumen.md](ontologia_motor_resumen.md) | Resumen ejecutivo del motor ontológico. |
---
## Material previo — sprints y contexto
| Archivo | Contenido |
|---|---|
| [2-week-sprint-knowledge_done.md](2-week-sprint-knowledge_done.md) | Sprint de dos semanas para conocimiento — completado. |
| [about_plantir.md](about_plantir.md) | Contexto sobre Plantir como referencia de posicionamiento. |
| [2026-02-17-notas_voz.md](2026-02-17-notas_voz.md) | Notas de voz, 17 Feb 2026. |
| [2026-02-27-notas_voz_rust.md](2026-02-27-notas_voz_rust.md) | Notas de voz sobre Rust, 27 Feb 2026. |
| [2026-04-24-234910-me-planteo-estas-dos-propuestas-para-un-open-spac.txt](2026-04-24-234910-me-planteo-estas-dos-propuestas-para-un-open-spac.txt) | Propuestas para open space, 24 Abr 2026. |
| [2026-04-25-000346-puedes-sacar-el-texto-de-este-contenido.txt](2026-04-25-000346-puedes-sacar-el-texto-de-este-contenido.txt) | Extracción de contenido, 25 Abr 2026. |

Binary file not shown.

View file

@ -0,0 +1,28 @@
resume esto https://axiussdc.substack.com/p/palantir-just-said-the-quiet-part
# Resumen: "Palantir acaba de decir la parte que se callaba"
**Tesis central:** El artículo, publicado por Axius-SDC en abril de 2026, sostiene que Palantir admitió públicamente algo que la comunidad de arquitectura de datos lleva años discutiendo: **la IA no es el producto, la ontología sí lo es**. La IA es solo el motor de ejecución.
**Lo que Palantir acierta**
Citando un artículo de Sainath Palla sobre el AI-FDE (Foundry Development Environment) de Palantir, el autor reconoce que la ontología no es documentación, sino una capa operacional por la que fluyen datos, workflows y decisiones. El AI-FDE permite que la IA participe en construir el sistema (no solo asistir dentro de él), desplazando el cuello de botella de la implementación al juicio humano: qué construir y cómo debe comportarse.
**Donde Palantir falla**
El problema es estructural: la ontología vive *dentro* de la plataforma de Palantir y está gobernada centralmente. A medida que se añaden casos de uso, las intersecciones se multiplican y la carga de gobernanza crece más rápido que el ahorro en implementación. Las únicas personas capaces de manejar esa complejidad son los Forward Deployed Engineers (FDEs) de Palantir, lo que crea una dependencia del proveedor que ninguna IA puede eliminar. Estás "alquilando gobernanza", no comprando infraestructura.
**La alternativa propuesta: gobernanza distribuida**
En lugar de una ontología centralizada, el autor propone vincular las restricciones a los datos mismos a nivel de componente. La metáfora cambia: ya no construyes una red ferroviaria con intersecciones complejas, sino un *estándar de ancho de vía* donde cada segmento lleva su propia especificación. Modelos de workflow, atestación, cadenas de procedencia y roles viajan con el payload, no se gestionan desde una plataforma central. Una IA operando sobre este sustrato valida contra el modelo en tiempo de ejecución, sin necesidad de una ontología centralizada.
**Tres preguntas para hacerle a tu account manager de Palantir el lunes:**
1. ¿Cuántos FDEs requiere tu despliegue, y qué pasa si reduces ese número a la mitad?
2. Cuando el AI-FDE escala un nuevo caso de uso, ¿quién valida que no desestabilice los workflows existentes?
3. ¿Puedes exportar la ontología y desplegarla en tu propia infraestructura sin dependencia de Palantir?
**Conclusión**
Palantir no es el enemigo: es el "rompehielos" que está educando al Fortune 500 sobre la IA basada en ontologías. La pregunta no es *si* la arquitectura debe estar guiada por una ontología, sino si esa ontología debe ser propietaria, centralizada y gestionada por ingenieros del proveedor, o abierta, determinista y mantenida por practicantes propios. El autor promociona el programa SDC Practitioner (Apache 2.0, lanzamiento mayo 2026) como esta alternativa abierta.
En una frase: **Palantir demostró la tesis; según el autor, no deberías tener que pagarla en alquiler.**

View file

@ -0,0 +1,544 @@
# Ontología filosófica y modelo operacional en software
**Genealogía conceptual, autores y uso contemporáneo**
*Jesús Pérez Lorenzo · OntoRef · 2026*
---
## Contenidos
1. [¿Son los invariantes el equivalente a los universales filosóficos?](#1-son-los-invariantes-el-equivalente-a-los-universales-filosóficos)
2. [Genealogía filosófica del modelo operacional](#2-genealogía-filosófica-del-modelo-operacional)
3. [La ontología operacional en software: origen, autores y uso actual](#3-la-ontología-operacional-en-software-origen-autores-y-uso-actual)
4. [Taoísmo, Yin-Yang y ontología operacional](#4-taoísmo-yin-yang-y-ontología-operacional)
5. [DAGs — genealogía y función en ontología operacional](#5-dags--genealogía-y-función-en-ontología-operacional)
6. [Física cuántica, computación cuántica y ontología operacional](#6-física-cuántica-computación-cuántica-y-ontología-operacional)
7. [Por qué es relevante en software e infraestructuras digitales](#7-por-qué-es-relevante-en-software-e-infraestructuras-digitales)
8. [Referencias y lecturas recomendadas](#8-referencias-y-lecturas-recomendadas)
---
## 1. ¿Son los invariantes el equivalente a los universales filosóficos?
La pregunta es legítima pero la correspondencia es más fina de lo que parece a primera vista.
### Los universales en ontología filosófica
Los *universales* responden a una pregunta específica: ¿qué comparten múltiples particulares que los hace del mismo tipo? Son propiedades, relaciones o cualidades que pueden instanciarse en varios individuos distintos — "rojez", "humanidad", "ser-mayor-que". El debate clásico gira en torno a su estatus ontológico:
- **Realismo platónico**: los universales existen independientemente de sus instancias — las Formas habitan un reino separado del mundo sensible.
- **Realismo moderado** (Aristóteles): los universales existen *en* las cosas, no aparte de ellas. No hay humanidad sin seres humanos concretos.
- **Nominalismo** (Ockham, siglo XIV): solo existen los particulares. Los universales son nombres (*nomina*), convenciones lingüísticas sin referente ontológico propio.
- **Conceptualismo** (posición media): los universales existen en la mente como conceptos, no en el mundo ni como meros nombres.
### Los invariantes como propiedades esenciales de un singular
Los invariantes en un modelo operacional de software no son universales en ninguno de los sentidos anteriores. No preguntan qué comparten múltiples proyectos — preguntan *qué no puede cambiar en este proyecto concreto sin que deje de ser este proyecto*.
La analogía filosófica precisa es la distinción aristotélica entre **propiedades esenciales y accidentales**: la esencia es aquello sin lo cual una cosa no sería lo que es; el accidente es lo que puede cambiar sin alterar su naturaleza. Un invariante operacional declara: "esta propiedad es esencial para la identidad de este proyecto."
Kripke formalizó esto de manera más rigurosa en *Naming and Necessity* (1980): ciertas propiedades son **rígidas** — verdaderas en todos los mundos posibles accesibles al individuo. El agua es necesariamente H₂O aunque alguien la llame "phlogiston". Un invariante operacional tiene este carácter modal: no puede ser falso en ningún estado posible del proyecto sin que el proyecto haya dejado de ser ese proyecto. Los ADRs que lo modifican no *violan* el invariante — constituyen un nuevo individuo.
### Dónde sí hay resonancia con los universales
La familia semántica es compartida: ambos capturan *lo que no varía*. El universal no varía a través de la instanciación. El invariante no varía a través de la evolución temporal del proyecto. El eje difiere — instanciación vs. tiempo — pero el gesto filosófico es análogo.
### El estatus ontológico: hechos institucionales
Los universales platónicos existen independientemente de que alguien los declare. Los invariantes operacionales son **hechos institucionales** en el sentido de John Searle (*La construcción de la realidad social*, 1995): son verdad porque un colectivo los declaró y formalizó. Como el dinero, los contratos o las reglas del ajedrez — su existencia depende de actos declarativos sostenidos por una práctica social.
Un invariante que el equipo no reconoce como tal no es un invariante real; es teatro.
---
## 2. Genealogía filosófica del modelo operacional
| Concepto filosófico | Autor / Origen | Equivalente operacional | Implementación |
|---|---|---|---|
| **Universales** | Platón (428348 aC) — *República*, *Parménides* | Tipos / Schemas | Contratos Nickel: `String`, `[| 'Accepted, 'Proposed |]` |
| **Esencia / Accidente** | Aristóteles (384322 aC) — *Metafísica* | Invariante / Estado mutable | `invariante = true` en `core.ncl` vs. dimensiones en `state.ncl` |
| **Potencia / Acto** | Aristóteles — *Metafísica* IX | Estado actual → deseado + Gates | `current_state` / `desired_state` / `gate.ncl` |
| **Categorías** | Aristóteles — *Categorías* | Tipos de nodo ontológico | `invariants`, `tensions`, `practices`, `dimensions`, `gates` |
| **Sustancia primera / segunda** | Aristóteles — *Categorías* | Instancia / Tipo | Proyecto concreto vs. schema de proyecto |
| **Identidad a través del tiempo** | Locke (1689), Parfit (1984) | Versioning + ADR lifecycle | `status = Superseded`, `superseded_by` |
| **Dialéctica** | Hegel (1807) — *Fenomenología del Espíritu* | Tensiones activas | `tensions` en `core.ncl` — sin síntesis, gestionadas |
| **Logos como tensión de opuestos** | Heráclito (535475 aC) | Tensión constitutiva | Dualidad ontología/reflection como tensión de diseño |
| **Mundos posibles** | Leibniz (1710), Kripke (1980) | FSM — transiciones permitidas | ADRs como restricciones de accesibilidad entre estados |
| **Propiedades rígidas** | Kripke — *Naming and Necessity* (1980) | Invariantes como necesidad | Un invariante es verdad en todos los estados posibles |
| **Límites del lenguaje = límites del mundo** | Wittgenstein — *Tractatus* (1921) | Schema como frontera de lo expresable | Lo que no tipea en Nickel no puede existir en el sistema |
| **Juegos de lenguaje** | Wittgenstein — *Investigaciones* (1953) | Modos operacionales | Un modo define las reglas del juego para un contexto |
| **Verdad como correspondencia** | Tarski (1933) | Typecheck / validación formal | `nickel export` como verificación de correspondencia |
| **Hechos institucionales** | Searle (1995) | ADRs como decisiones declaradas | Una constraint existe porque el equipo la declaró |
| **Ontología formal vs. regional** | Husserl (1913) — *Ideas I* | Schema formal vs. ontología del proyecto | `adrs/schema.ncl` vs. `.ontology/core.ncl` |
| **Filosofía del proceso** | Whitehead (1929) — *Process and Reality* | Ontología (ser) vs. Reflection (devenir) | La dualidad estructural del protocolo |
| **Realismo estructural** | Ladyman & Ross (2007) | Grafo como lo real | El DAG captura relaciones; los nodos son secundarios |
| **Lógica de predicados** | Frege (1879) — *Begriffsschrift* | Queries sobre el grafo | `where severity == "Hard" \| select id claim` |
### Matices que la tabla no captura
**Aristóteles es el arquitecto base.** La esencia/accidente es exactamente invariante/estado-mutable. La potencia/acto es exactamente `current_state`/`desired_state` con la gate como umbral de actualización.
**Whitehead contra Aristóteles es la tensión interna del modelo.** Aristóteles ve la realidad como sustancias con propiedades. Whitehead la ve como procesos. Un modelo operacional de software vive en esa grieta: la ontología es aristotélica (qué *es* el proyecto), la capa de reflection es whiteheadiana (cómo *deviene*).
**Kripke resuelve lo que Aristóteles dejó ambiguo.** Aristóteles distingue esencial de accidental pero no formaliza la modalidad. Kripke lo hace: una propiedad esencial es rígida — verdadera en todos los mundos posibles accesibles desde el individuo.
**La ausencia notable: Heidegger.** El *ser-en-el-mundo* como condición de posibilidad de cualquier ontología explícita no aparece en ningún modelo operacional de software. Lo que Heidegger llamaría el trasfondo pre-temático es exactamente lo que los READMEs intentan capturar y fracasan. Un modelo operacional no lo resuelve; reduce la superficie de ese trasfondo haciendo explícito lo que puede serlo.
---
## 3. La ontología operacional en software: origen, autores y uso actual
### El problema que había que resolver
A finales de los años 1970, los sistemas de inteligencia artificial enfrentaban el *cuello de botella del conocimiento*: los sistemas expertos eran eficaces en dominios acotados pero no podían compartir conocimiento entre sí. Cada sistema construía su representación del mundo de forma ad hoc, incompatible con el resto.
### Los fundadores
**John McCarthy — el precursor (19581980s)**
Fundador del término "inteligencia artificial" y creador de LISP, fue el primero en proponer que los sistemas de IA necesitaban representaciones explícitas del mundo para razonar sobre él. Su *situational calculus* (1969, con Patrick Hayes) — un lenguaje formal para describir el mundo y sus cambios — es el antecedente directo de la ontología operacional. McCarthy adoptó el término "ontología" de la filosofía para referirse a los compromisos sobre qué tipos de cosas existen en el dominio modelado.
**Marvin Minsky — frames y estructuras de conocimiento (1974)**
En *A Framework for Representing Knowledge* (MIT AI Lab, 1974), Minsky propuso los *frames* como estructuras para organizar el conocimiento: cada frame representa un concepto con slots (atributos) y valores por defecto. Es la primera implementación computacional de algo cercano a la sustancia aristotélica con propiedades.
**Tom Gruber — la definición canónica (1993)**
Formuló la definición que se convirtió en estándar: *"An ontology is a specification of a conceptualization"* (*Knowledge Acquisition*, 1993). La definición es deliberadamente minimalista: una ontología no captura la realidad — captura un *punto de vista compartido* sobre un dominio. Esto la sitúa en el territorio de los hechos institucionales de Searle.
**Nicola Guarino — ontología formal en sistemas de información (1990s)**
Desarrolló la distinción entre *ontología de alto nivel* (conceptos universales como tiempo, espacio, proceso) y *ontología de dominio* (conceptos específicos de un campo). Fundó la conferencia *Formal Ontology in Information Systems* (FOIS, 1998).
**Doug Lenat — Cyc: el proyecto más ambicioso (19842017)**
Inició el proyecto *Cyc* en MCC con el objetivo de codificar todo el conocimiento de sentido común humano. Alcanzó más de 600.000 conceptos y 5 millones de aserciones. Demostró tanto el potencial como los límites del enfoque: la escala del conocimiento implícito humano es prácticamente inabarcable mediante codificación manual.
**Tim Berners-Lee, James Hendler y Ora Lassila — la Web Semántica (2001)**
Propusieron extender la Web con significado semántico mediante RDF, OWL y SPARQL — el ecosistema que popularizó el uso de ontologías en ingeniería de software más allá de la IA.
### Línea temporal
| Año | Hito |
|---|---|
| 1958 | McCarthy propone representación explícita del conocimiento para IA |
| 1969 | McCarthy & Hayes — situational calculus |
| 1974 | Minsky — frames: primera estructura análoga a la sustancia aristotélica |
| 1984 | Lenat inicia Cyc |
| 1993 | Gruber — definición canónica |
| 1998 | Primera conferencia FOIS |
| 2001 | Berners-Lee et al. — Web Semántica: RDF, OWL, SPARQL |
| 2004 | OWL se convierte en recomendación W3C |
| 2012 | Google Knowledge Graph — ontología operacional en producción |
| 2020s | LLMs entrenados sobre grafos ontológicos; ontologías como contexto para agentes |
### Uso actual
**Biomedicina:** Gene Ontology (GO), SNOMED CT, ICD-11, Human Phenotype Ontology.
**Grafos de conocimiento empresariales:** Google Knowledge Graph (500.000M de hechos), Wikidata (100M de elementos), schema.org.
**Industria y manufactura:** ISO 15926 (petróleo y gas), IFC/BIM (construcción), ontologías Industry 4.0.
**Inteligencia artificial:** ConceptNet, entrenamiento de LLMs (Wikidata, DBpedia), contexto estructurado para agentes autónomos.
**Software de proyecto — la frontera actual:** El uso de ontologías operacionales *dentro* de proyectos de software para describir el propio proyecto es el desarrollo más reciente. Los precursores son los ADRs (Nygard, 2011) y los modelos C4 (Brown). La propuesta de *ontoref* pertenece a esta frontera: el proyecto *es* el dominio, y la ontología *es* el modelo operacional de ese dominio.
---
## 4. Taoísmo, Yin-Yang y ontología operacional
### Lo que la tradición occidental no vio
La ontología occidental desde Parménides privilegia el *ser* sobre el *no-ser*, la *presencia* sobre la *ausencia*, la *identidad* sobre la *diferencia*. El Taoísmo invierte la pregunta. El Tao (道) es previo a la distinción ser/no-ser:
> 道可道,非常道。名可名,非常名。
> *El Tao que puede nombrarse no es el Tao eterno. El nombre que puede pronunciarse no es el nombre eterno.*
> — Laozi, *Tao Te Ching*, cap. 1 (~siglo VI-IV aC)
Lo que no puede formalizarse no es una limitación del modelo — es la condición de posibilidad de todo lo que sí puede formalizarse.
### Yin-Yang — polaridad constitutiva, no oposición
El yin y el yang no son *contrarios* sino *complementarios que se definen mutuamente*:
- Cada polo **contiene una semilla del opuesto**
- El exceso de un polo **genera su contrario** (極則反)
- La tensión entre ellos **nunca se resuelve** — solo se gestiona
- Uno **no existe sin el otro**
Esto es estructuralmente distinto de la dialéctica hegeliana, que busca síntesis. El yin-yang no resuelve — la tensión es constitutiva, permanente. Heráclito es el occidental más cercano: "el camino hacia arriba y hacia abajo son el mismo."
### Tabla de correspondencias
| Concepto taoísta | Texto / Época | Equivalente occidental | Correspondencia operacional |
|---|---|---|---|
| **Tao** (道) | Laozi, *Tao Te Ching* (~6-4 aC) | Ser de Heidegger | Trasfondo pre-temático que ningún schema captura |
| **Yin / Yang** | Tradición Zhou (~1100 aC), *I Ching* | Heráclito — logos como tensión | Ontología (ser) / Reflection (devenir) |
| **Wu wei** (無為) | Laozi, *Tao Te Ching* | Sin equivalente exacto | "No enforcement" — coherencia sin imposición |
| **Ji** (機) — momento de transformación | Zhuangzi (~4 aC) | Kripke — mundos posibles | Gates — umbral cualitativo entre estados |
| **Wu ji → Tai ji** | Zhou Dunyi (1017-1073), *Taijitu shuo* | Husserl — formal vs. regional | schema.ncl (vacío) → core.ncl (polaridad concreta) |
| **Relatividad perspectival** | Zhuangzi — la rana del pozo | Wittgenstein tardío | Toda ontología es una conceptualización (Gruber) |
### Las cuatro correspondencias profundas
**Yin/Yang ↔ Ontología/Reflection.** La ontología captura lo que ES — capa yang: formal, dura, definida. La reflection captura lo que DEVIENE — capa yin: fluida, procesual. Como en el símbolo: cada una contiene una semilla de la otra.
**Wu wei ↔ "No enforcement".** Coherencia voluntaria que emerge de la adopción justificada, sin mecanismo de enforcement. No impones — creas las condiciones para que el alineamiento emerja naturalmente.
**Tensiones ↔ Yin-Yang permanente.** Las tensiones operacionales son explícitamente irresolubles. Si eliminas la tensión entre formalización y adopción maximizando un polo, el sistema muere.
**Gates ↔ Ji (機).** El punto de inflexión donde el potencial se actualiza. No un estado sino el umbral entre estados. Dinámico, cualitativo, contextual.
### La crítica taoísta a la ontología clasificatoria
Los sistemas OWL intentan eliminar la ambigüedad mediante clasificación precisa. El *Tao Te Ching* advierte: cuanto más preciso el nombre, más excluyes. Esto explica por qué las ontologías biomédicas sufren continuamente con casos límite y dependencia del contexto. La precisión y la cobertura son yin y yang — no se resuelven, se equilibran.
Zhuangzi lo lleva más lejos: la ontología correcta para un proyecto no es la más formalmente precisa sino la que el equipo puede sostener y evolucionar. Gruber llegó al mismo lugar en 1993: una ontología es "una especificación de una conceptualización" — un punto de vista compartido, no una verdad metafísica.
---
## 5. DAGs — genealogía y función en ontología operacional
### El problema que fuerza el grafo
El grafo emerge como la estructura mínima que captura relaciones arbitrarias entre conceptos. Pero el grafo general puede tener ciclos — y los ciclos en ontología son epistemológicamente problemáticos.
### Por qué la aciclicidad
Dos tradiciones independientes llegan al mismo resultado:
**Aristóteles** (*Analíticos Posteriores*): una definición válida procede de género más diferencia específica. La jerarquía es estrictamente descendente. Si A se define en términos de B y B en términos de A, ninguno está definido.
**Russell** (1901): la paradoja del conjunto de todos los conjuntos que no se contienen a sí mismos. La solución en *Principia Mathematica* (1910) es la *teoría de tipos*: los tipos forman una jerarquía estricta — un tipo de nivel N solo puede referirse a tipos de nivel N-1 o inferior. Esto es estructuralmente un DAG. La aciclicidad es la solución formal al colapso por autorreferencia.
### Línea genealógica
| Año | Autor / Hito | Contribución |
|---|---|---|
| 384-322 aC | Aristóteles — *Analíticos Posteriores* | Definición bien fundada: jerarquía sin circularidad |
| 1879 | Frege — *Begriffsschrift* | Lógica de predicados: relaciones formales entre conceptos |
| 1901-1910 | Russell & Whitehead — *Principia Mathematica* | Teoría de tipos como DAG: aciclicidad contra autorreferencia |
| 1933 | Tarski — "El concepto de verdad" | Verdad como correspondencia entre proposiciones y estructuras |
| 1958 | US Navy — PERT charts (programa Polaris) | DAG para planificación: camino crítico como recorrido del grafo |
| 1969 | Collins & Quillian — redes semánticas | Primera red semántica computacional: is-a como DAG |
| 1974 | Minsky — frames | Herencia múltiple: DAG sobre árbol |
| 1976 | Feldman — Make | Build system como DAG: orden topológico, ciclos = error |
| 1985 | Brachman & Schmolze — KL-ONE | Description Logic: subsunción formal como DAG |
| 1988 | Pearl — Bayesian networks | Causalidad como DAG: asimetría temporal inherentemente acíclica |
| 2001 | Berners-Lee et al. — Web Semántica | RDF/OWL: grafos de conocimiento como estándar industrial |
| 2005 | Torvalds — Git | Historia de commits como DAG: irreversibilidad del tiempo en VCS |
### Las cuatro funciones del DAG en ontología operacional
**① Análisis de impacto decidible.** Antes de cambiar un nodo, recorre el DAG y sabes exactamente qué se ve afectado. En un grafo cíclico esto es indecidible. Concreto: `terraform plan` recorre el DAG de recursos para determinar orden de creación/modificación/destrucción.
**② Orden topológico para ejecución fiable.** Cualquier operación con dependencias tiene un orden correcto que el DAG da sin coordinador central. Principio detrás de Make, Bazel, Gradle, y cualquier build system no trivial.
**③ Justificación sin circularidad epistémica.** La ontología operacional es fundacionalista: los invariantes son nodos raíz justificados por ADR explícito. Un ciclo significaría dogma circular, no arquitectura.
**④ Rollback determinista.** El inverso topológico del DAG es siempre computable. Sin DAG, el rollback depende de que alguien recuerde el orden correcto.
### La tensión con el Taoísmo — DAG vs. ciclo
El DAG es *yang*: direccional, jerárquico, asimétrico, irreversible. El yin-yang taoísta es *cíclico*: yin genera yang, yang genera yin.
La resolución está en separar dos temporalidades:
- **Sincrónica** (snapshot): el DAG es válido dentro de un momento dado.
- **Diacrónica** (evolución): la secuencia de snapshots puede tener patrones cíclicos.
El DAG captura la *estructura del ser en un momento*. El ciclo taoísta captura el *proceso de transformación a través del tiempo*. El *I Ching* — el libro de los cambios — es exactamente esto: 64 hexagramas como estados (nodos), transformaciones entre ellos como aristas. Un grafo con ciclos, porque la realidad vuelve.
---
## 6. Física cuántica, computación cuántica y ontología operacional
### El golpe más profundo — Aristóteles estaba equivocado en el nivel fundamental
La mecánica cuántica demuestra que, en el nivel fundamental, las propiedades no son definidas hasta que se miden. Un electrón no *tiene* posición — la posición emerge del acto de medición.
El **teorema de Kochen-Specker** (1967) lo prueba formalmente: es imposible asignar valores definidos a todos los observables cuánticos simultáneamente, incluso antes de la medición. La indeterminación es *ontológica*, no epistémica. El suelo sobre el que se construyeron 2.300 años de ontología occidental se mueve.
### Las tres conexiones genuinas
**Complementariedad de Bohr ↔ Yin-Yang y la dualidad Ontología/Reflection**
Niels Bohr (1885-1962) formuló el *principio de complementariedad*: onda y partícula son dos descripciones mutuamente excluyentes pero igualmente necesarias. No puedes observar ambas simultáneamente — el aparato de medición determina qué aspecto revelas.
Bohr lo conectó explícitamente con el Taoísmo: cuando fue ennoblecido en 1947 adoptó el símbolo yin-yang como parte de su escudo de armas con la inscripción *Contraria sunt complementa*.
La dualidad ontología/reflection tiene la misma estructura: la capa que uses (core.ncl o los modos de reflection) determina qué aspecto del proyecto ves. Ninguna es más verdadera — son complementarias.
**Contextualidad cuántica ↔ Ontología perspectival**
El teorema de Kochen-Specker prueba que el resultado de medir un observable depende de *qué otros observables se miden simultáneamente*. Las propiedades son *contextuales*. Esto convierte en hecho físico lo que tres tradiciones habían sostenido filosóficamente: Zhuangzi (perspectivismo), Wittgenstein tardío (juegos de lenguaje), Gruber (conceptualización).
Para la ontología operacional: el "estado" de un proyecto es siempre relativo al actor y al contexto de consulta.
**El problema del observador ↔ El trasfondo que no puede formalizarse**
En mecánica cuántica, la medición colapsa la función de onda — el observador es parte del sistema. No hay *view from nowhere* (Nagel, 1986). Esto es exactamente el trasfondo pre-temático de Heidegger — la condición de posibilidad de cualquier formalización que ella misma no puede ser formalizada.
### La interpretación más relevante — Mecánica Cuántica Relacional (Rovelli, 1996)
Carlo Rovelli propuso que no existen estados absolutos — solo estados *relativos a otros sistemas*. El estado de una partícula es una propiedad relacional, no intrínseca. Esto conecta directamente con Ladyman y Ross (realismo estructural) y con la ontología perspectival.
Para el modelo operacional: el `current_state` en `state.ncl` no es una propiedad intrínseca del proyecto — es relacional respecto al actor que consulta, los objetivos perseguidos, el momento del ciclo de vida.
### La computación cuántica — honestidad sobre sus límites
| Aspecto | Situación real | Relevancia para ontología operacional |
|---|---|---|
| Tamaño de grafos | La ventaja cuántica aparece a escala masiva | Los grafos de proyecto son pequeños — sin ventaja |
| Algoritmos relevantes | Grover O(√N), Shor factorización | No mapean a consultas típicas de ontología |
| Decoherencia | Cualquier interacción colapsa la superposición | Incompatible con operación continua |
| Redes semánticas cuánticas | Investigación activa — relaciones con amplitudes | Captura gradualidad que OWL binario pierde |
| LLMs y espacios de Hilbert | Embeddings como productos internos | **Directamente relevante** — misma matemática |
### La conexión no obvia — LLMs como geometría cuántica aplicada
Los Large Language Models operan en espacios vectoriales de alta dimensión donde el significado es relacional y contextual — la misma matemática de espacios de Hilbert que usa la mecánica cuántica, aplicada a representación semántica.
La ontología operacional (core.ncl, invariantes, tensiones) es, en este contexto, una *base* en ese espacio — un conjunto de vectores que fijan el marco de referencia en el que el agente opera. Inyectar el contexto ontológico al inicio de una sesión es fijar ese marco de referencia antes de que el agente empiece a computar.
La física cuántica importa aquí no porque vayamos a correr ontologías en computadores cuánticos — sino porque demuestra que las asunciones ontológicas clásicas son físicamente incorrectas en el nivel fundamental. La ontología operacional es contextual, relacional y perspectival *por diseño correcto*, no por limitación.
---
## 7. Por qué es relevante en software e infraestructuras digitales
### El problema real no es técnico — es epistémico
```
Lo que el sistema hace ≠ Lo que el equipo cree que hace
Lo que la infra tiene desplegado ≠ Lo que Terraform declara
Lo que el ADR decidió ≠ Lo que el código implementa
Lo que el agente asume ≠ Lo que el contexto real requiere
```
Cada una de esas brechas es silenciosa, acumulativa, y solo se descubre cuando falla en producción. No son fallos de las personas — son consecuencias inevitables de operar un sistema complejo sin un modelo del propio sistema.
### Lo que la ontología operacional hace que nada más hace
| Documentación tradicional | Ontología operacional |
|---|---|
| Prosa — no consultable | Grafo tipado — consultable con queries |
| Estática — se desactualiza sin señal | Verificada en cada commit mediante typecheck |
| Describe lo que hubo | Formaliza lo que ES y lo que debe ser |
| Solo legible por humanos | Legible por humanos, agentes de IA y CI |
| Captura el qué | Captura el qué, el por qué, y lo que no puede cambiar |
| Nadie la actualiza bajo presión | El proceso la actualiza — es el path, no documentación paralela |
**Formaliza lo que no puede romperse.** Los invariantes no son comentarios — son constraints tipadas que el sistema verifica antes del runtime.
**Captura las tensiones activas.** "Coste vs. disponibilidad", "velocidad vs. estabilidad" viven en la ontología, no en la cabeza del miembro más veterano. Cuando ese alguien se va, los trade-offs permanecen.
**Da al agente de IA el contexto que no puede inferir del código.** Un agente puede leer todo el código de un sistema y no saber que hay un acuerdo que impide escalar horizontalmente cierto servicio. La ontología operacional hace eso explícito y machine-readable.
### Por qué los DAGs — especificidad técnica
**Análisis de impacto decidible.** Antes de cambiar un invariante o un nodo de infra — recorre el DAG y sabes exactamente qué se ve afectado. Terraform hace exactamente esto: `terraform plan` recorre el DAG de recursos para determinar el orden de aplicación.
**Orden topológico para ejecución fiable.** Cualquier operación con dependencias tiene un orden correcto sin necesidad de un coordinador central. El principio detrás de Make (1976), Bazel, Gradle.
**Justificación sin circularidad.** "Este invariante existe porque esta práctica lo requiere, y esta práctica existe porque este invariante lo define" es dogma circular, no arquitectura. El DAG hace esa circularidad visible e inaceptable.
**Rollback determinista.** El inverso topológico del DAG es siempre computable. Sin DAG, el rollback depende de memoria humana.
### Por qué la reflection — la ontología sin operación es un museo
**Modos como DAGs operacionales.** Un procedimiento sin DAG puede ejecutarse en cualquier orden, saltarse, o ignorarse. Un modo como DAG tiene dependencias explícitas — el sistema verifica que cada paso se ejecutó antes de avanzar.
**ADRs con constraints tipadas.** Una decisión sin constraints es prosa. Con constraints tipadas es un contrato que el CI puede verificar en cada commit.
**Detección de drift.** La diferencia entre el estado sellado y el estado actual es medible si y solo si formalizaste el estado sellado. Sin eso, "¿ha derivado esto?" no tiene respuesta objetiva.
### Las tres presiones convergentes
**Presión 1: Los sistemas superan la comprensión individual.** Un monolito de 2005 podía vivir en la cabeza de dos personas. Un sistema cloud-native con decenas de microservicios, múltiples regiones y SLOs por servicio no cabe en ninguna cabeza.
**Presión 2: Los agentes de IA tienen acceso de escritura.** Hoy los agentes pueden abrir PRs, modificar configuración, ejecutar Terraform, y desplegar. Un agente sin contexto ontológico no es un asistente — es un actor autónomo con comprensión incompleta del sistema que modifica.
**Presión 3: Los equipos son multi-actor y distribuidos.** Humanos en zonas horarias distintas, agentes de IA con distintos contextos de sesión, pipelines de CI en paralelo — todos escribiendo sobre el mismo sistema sin coordinador central.
### La síntesis
> Un sistema que sabe lo que es, puede verificar que sigue siendo lo que dice ser, y puede comunicarlo a cualquier actor — humano, agente o CI — de forma estructurada y sin ambigüedad.
Eso no es documentación. Es un modelo operacional vivo.
La ontología operacional es la respuesta a: *¿qué estructura mínima necesita un sistema para conocerse a sí mismo de forma que ese conocimiento pueda ser usado, verificado y compartido?*
---
## 8. Referencias y lecturas recomendadas
### Filosofía clásica y ontología
**Aristóteles.** *Metafísica*. Traducción de Tomás Calvo Martínez. Madrid: Gredos, 1994. [~330 aC]
> El libro fundamental para la distinción esencia/accidente, potencia/acto y las categorías. El libro IX sobre potencia es la base del modelo estado-actual/estado-deseado.
**Aristóteles.** *Analíticos Posteriores*. Traducción de Miguel Candel Sanmartín. Madrid: Gredos, 1988. [~350 aC]
> Formaliza el requisito de definiciones bien fundadas — antecedente directo de la aciclicidad en DAGs.
**Laozi.** *Tao Te Ching*. Traducción de Stephen Mitchell. Nueva York: Harper Perennial, 1992. [~siglo VI-IV aC]
> Texto fundacional del Taoísmo. Los primeros y últimos capítulos son los más relevantes para la relación con ontología operacional.
**Zhuangzi.** *Zhuangzi: The Complete Writings*. Traducción de Brook Ziporyn. Indianapolis: Hackett, 2020. [~siglo IV aC]
> Especialmente los capítulos internos (1-7). La relatividad perspectival como crítica de toda ontología clasificatoria.
**Frege, G.** *Begriffsschrift*. Halle: Nebert, 1879.
> Primera formulación de la lógica de predicados. El antecedente formal de toda representación de conocimiento estructurado.
**Russell, B. & Whitehead, A.N.** *Principia Mathematica*. Cambridge: Cambridge University Press, 1910-1913.
> La teoría de tipos como DAG para evitar la autorreferencia circular. El principio del círculo vicioso como fundamento de la aciclicidad.
**Wittgenstein, L.** *Tractatus Logico-Philosophicus*. Londres: Kegan Paul, 1922.
> "Los límites de mi lenguaje son los límites de mi mundo." El schema Nickel define la frontera de lo representable.
**Wittgenstein, L.** *Investigaciones filosóficas*. Oxford: Blackwell, 1953.
> Juegos de lenguaje como práctica. Los modos operacionales como juegos de lenguaje de un proyecto.
**Hegel, G.W.F.** *Fenomenología del Espíritu*. Traducción de Manuel Jiménez Redondo. Valencia: Pre-Textos, 2006. [1807]
> Dialéctica tesis-antítesis-síntesis. Contrástese con el yin-yang: síntesis vs. tensión gestionada permanentemente.
**Husserl, E.** *Ideas relativas a una fenomenología pura*. México: FCE, 1993. [1913]
> La distinción ontología formal/regional — estructura vacía universal vs. conceptos de dominio específico.
**Whitehead, A.N.** *Process and Reality*. Nueva York: Free Press, 1978. [1929]
> La realidad como proceso, no sustancia. El fundamento filosófico de la capa reflection como devenir distinto del ser ontológico.
**Kripke, S.** *Naming and Necessity*. Cambridge: Harvard University Press, 1980.
> Propiedades esenciales rígidas y semántica de mundos posibles. El fundamento modal de los invariantes operacionales.
**Searle, J.** *The Construction of Social Reality*. Nueva York: Free Press, 1995.
> Hechos institucionales como fundamento de las decisiones declaradas. El ADR como acto de habla colectivo que crea realidad operacional.
**Parfit, D.** *Reasons and Persons*. Oxford: Oxford University Press, 1984.
> Identidad a través del tiempo. La pregunta "¿es el mismo proyecto?" cuando sus invariantes cambian vía ADR.
**Ladyman, J. & Ross, D.** *Every Thing Must Go: Metaphysics Naturalized*. Oxford: Oxford University Press, 2007.
> Realismo estructural: las relaciones son más reales que las entidades. El DAG como estructura fundamental, los nodos como secundarios.
**Quine, W.V.O.** "On What There Is." *Review of Metaphysics*, 2(5), 1948.
> El concepto de compromiso ontológico: toda teoría asume la existencia de ciertas entidades. Todo schema asume una ontología aunque no lo declare.
**Tarski, A.** "The Concept of Truth in Formalized Languages." En *Logic, Semantics, Metamathematics*. Oxford: Clarendon Press, 1956. [1933]
> Verdad como correspondencia. El fundamento de la validación formal — nickel typecheck como verificación tarsquiana.
**Zhou Dunyi.** *Taijitu shuo* (Explicación del Diagrama del Supremo Último). [1017-1073]
> La cosmología taoísta sistematizada: wu ji → tai ji → yin-yang. La transición de lo indiferenciado a la polaridad como modelo de emergencia estructural.
**Heidegger, M.** *Ser y Tiempo*. Traducción de Jorge Eduardo Rivera. Santiago: Editorial Universitaria, 1997. [1927]
> El trasfondo pre-temático que ninguna ontología captura completamente. La ausencia notable en todos los modelos operacionales de software.
**Heráclito.** *Fragmentos*. Edición de Rodolfo Mondolfo. Buenos Aires: Losada, 2007. [~500 aC]
> El fragmento DK22B60 — "el camino hacia arriba y hacia abajo son el mismo" — es el equivalente presocrático más cercano al yin-yang.
**Bateson, G.** *Steps to an Ecology of Mind*. Nueva York: Ballantine, 1972.
> La diferencia que hace una diferencia. Epistemología sistémica que conecta Taoísmo, cibernética y representación del conocimiento.
**Winograd, T. & Flores, F.** *Understanding Computers and Cognition*. Norwood: Ablex, 1986.
> La crítica más influyente a la IA simbólica desde Heidegger. El trasfondo de comprensión no puede formalizarse — límite estructural de toda ontología operacional.
### Inteligencia artificial y representación del conocimiento
**Minsky, M.** "A Framework for Representing Knowledge." MIT AI Lab Memo 306, 1974.
> Los frames como primera estructura computacional análoga a la sustancia aristotélica con propiedades heredables.
**Collins, A.M. & Quillian, M.R.** "Retrieval time from semantic memory." *Journal of Verbal Learning and Verbal Behavior*, 8(2), 240-247, 1969.
> Primera red semántica computacional. La topología del grafo predice el comportamiento cognitivo.
**Brachman, R.J. & Schmolze, J.G.** "An overview of the KL-ONE knowledge representation system." *Cognitive Science*, 9(2), 171-216, 1985.
> Description Logic: la subsunción como relación formal que genera el DAG de conceptos. Antecedente teórico de OWL.
**Gruber, T.** "A translation approach to portable ontology specifications." *Knowledge Acquisition*, 5(2), 199-220, 1993.
> La definición canónica: ontología como especificación de una conceptualización. Referencia obligatoria para cualquier trabajo en ontología de software.
**Lenat, D.B. & Guha, R.V.** *Building Large Knowledge-Based Systems: Representation and Inference in the Cyc Project*. Reading: Addison-Wesley, 1990.
> El proyecto más ambicioso de ontología operacional. Sus fracasos son tan informativos como sus logros.
**Pearl, J.** *Probabilistic Reasoning in Intelligent Systems*. San Mateo: Morgan Kaufmann, 1988.
> Bayesian networks como DAGs causales. La formalización de por qué la causalidad es inherentemente acíclica.
**Pearl, J.** *Causality: Models, Reasoning, and Inference*. Cambridge: Cambridge University Press, 2000.
> El do-calculus. Distinción entre correlación y causa como operaciones sobre el DAG causal.
**Guarino, N. (ed.)** *Formal Ontology in Information Systems*. Amsterdam: IOS Press, 1998.
> Actas de FOIS'98. La conferencia fundacional del campo. El artículo introductorio de Guarino sobre ontología formal vs. de dominio es referencia esencial.
**Berners-Lee, T., Hendler, J. & Lassila, O.** "The Semantic Web." *Scientific American*, 284(5), 34-43, 2001.
> El artículo que popularizó las ontologías en ingeniería de software. La visión de la web como grafo de conocimiento estructurado.
### Física cuántica y fundamentos
**Bohr, N.** "Can Quantum-Mechanical Description of Physical Reality Be Considered Complete?" *Physical Review*, 48, 696-702, 1935.
> La respuesta de Bohr al artículo EPR. Formula el principio de complementariedad como posición filosófica irrenunciable.
**Bohr, N.** "Discussion with Einstein on Epistemological Problems in Atomic Physics." En Schilpp (ed.), *Albert Einstein: Philosopher-Scientist*. Evanston: Library of Living Philosophers, 1949.
> El debate más importante de la física del siglo XX sobre interpretación cuántica.
**Heisenberg, W.** *Physics and Philosophy: The Revolution in Modern Science*. Nueva York: Harper & Row, 1958.
> El capítulo sobre el papel del lenguaje en física cuántica conecta directamente con Wittgenstein.
**Schrödinger, E.** "Die gegenwärtige Situation in der Quantenmechanik." *Naturwissenschaften*, 23(48-49-50), 1935.
> El artículo del gato. Introduce el entrelazamiento (*Verschränkung*) y demuestra que la superposición no puede interpretarse clásicamente.
**Kochen, S. & Specker, E.P.** "The Problem of Hidden Variables in Quantum Mechanics." *Journal of Mathematics and Mechanics*, 17(1), 59-87, 1967.
> Prueba formal de que es imposible asignar valores definidos a todos los observables cuánticos simultáneamente. La contextualidad como hecho matemático.
**Bell, J.S.** *Speakable and Unspeakable in Quantum Mechanics*. Cambridge: Cambridge University Press, 1987.
> El capítulo "Against measurement" es una crítica filosófica fundamental al vocabulario de la física cuántica.
**Aspect, A., Grangier, P. & Roger, G.** "Experimental Realization of Einstein-Podolsky-Rosen-Bohm Gedankenexperiment." *Physical Review Letters*, 49(2), 91-94, 1982.
> La confirmación experimental de que las desigualdades de Bell se violan. La naturaleza es no-local.
**Everett, H.** "Relative State Formulation of Quantum Mechanics." *Reviews of Modern Physics*, 29(3), 454-462, 1957.
> La interpretación de muchos mundos. Ontología donde todos los estados posibles son reales — el antípoda de la ontología operacional que colapsa posibilidades en decisiones.
**Rovelli, C.** "Relational Quantum Mechanics." *International Journal of Theoretical Physics*, 35(8), 1637-1678, 1996.
> No existen estados absolutos — solo estados relativos a otros sistemas. La ontología más radical de la lista.
**Rovelli, C.** *Helgoland*. Milano: Adelphi, 2020.
> La versión divulgativa de la mecánica cuántica relacional. El capítulo sobre relaciones y el budismo es relevante para la conexión con el Taoísmo.
**Nagel, T.** *The View from Nowhere*. Oxford: Oxford University Press, 1986.
> El argumento de que toda objetividad requiere un punto de vista. En tensión directa con el problema del observador cuántico.
**Nielsen, M.A. & Chuang, I.L.** *Quantum Computation and Quantum Information*. Cambridge: Cambridge University Press, 2000.
> El texto estándar de computación cuántica. Los capítulos 2-3 sobre espacios de Hilbert son la base para entender la conexión con LLMs.
**Deutsch, D.** *The Fabric of Reality*. Londres: Allen Lane, 1997.
> La defensa más ambiciosa de la interpretación de muchos mundos. El capítulo sobre epistemología cuántica.
### Ingeniería de software e infraestructura
**Nygard, M.** "Documenting Architecture Decisions." Cognitect Blog, 2011.
> La propuesta original de ADRs. El precursor directo de los ADRs con constraints tipadas.
**Evans, E.** *Domain-Driven Design: Tackling Complexity in the Heart of Software*. Boston: Addison-Wesley, 2003.
> El lenguaje ubicuo como ontología operacional informal. El bounded context como ontología regional de Husserl aplicada a dominios de negocio.
**Kleppmann, M.** *Designing Data-Intensive Applications*. Sebastopol: O'Reilly, 2017.
> El capítulo sobre modelos de datos: la elección de modelo (relacional, documental, grafo) es una decisión ontológica.
**Morris, K.** *Infrastructure as Code*. Sebastopol: O'Reilly, 2016.
> Formaliza la declaratividad en infraestructura. La distinción entre estado deseado y estado actual — el núcleo del modelo `state.ncl`.
**Humble, J. & Farley, D.** *Continuous Delivery*. Boston: Addison-Wesley, 2010.
> El pipeline de despliegue como DAG de verificaciones. Cada etapa es un gate que el sistema debe cruzar formalmente.
**Kim, G., Humble, J., Debois, P. & Willis, J.** *The DevOps Handbook*. Portland: IT Revolution Press, 2016.
> El flujo de trabajo DevOps como protocolo operacional. Los feedback loops como ciclo ontología/reflection.
**Dehghani, Z.** "How to Move Beyond a Monolithic Data Lake to a Distributed Data Mesh." Martin Fowler Blog, 2019.
> Data mesh como ontología operacional distribuida: cada dominio es responsable de su propia ontología de datos.
**Fowler, M.** "Who Needs an Architect?" *IEEE Software*, 20(5), 11-13, 2003.
> Define arquitectura como "las decisiones que son difíciles de cambiar" — equivalente exacto de los invariantes en ontología operacional.
**Richardson, C.** *Microservices Patterns*. Shelter Island: Manning, 2018.
> El patrón Saga como DAG de transacciones distribuidas. Cada paso tiene `depends_on` implícito — el DAG operacional aplicado a consistencia eventual.
**Brown, S.** *Software Architecture for Developers*. Leanpub, 2014.
> El modelo C4 como alternativa a UML. Múltiples niveles de abstracción para describir un sistema.
**Hohpe, G. & Woolf, B.** *Enterprise Integration Patterns*. Boston: Addison-Wesley, 2003.
> Patrones como vocabulario compartido — nombrar los patrones operacionales es un acto ontológico.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,963 @@
#set document(
title: "Ontología filosófica y modelo operacional en software",
author: "Jesús Pérez Lorenzo",
)
#set page(
paper: "a4",
margin: (top: 2.5cm, bottom: 2.5cm, left: 2.8cm, right: 2.8cm),
numbering: "1",
number-align: right,
)
#set text(
font: "New Computer Modern",
size: 11pt,
lang: "es",
)
#set heading(numbering: "1.1.")
#set par(
justify: true,
leading: 0.75em,
first-line-indent: 1.2em,
)
#show heading: it => {
set text(weight: "bold")
set par(first-line-indent: 0em)
v(0.8em)
it
v(0.4em)
}
#show heading.where(level: 1): it => {
set text(size: 14pt, fill: rgb("#c04000"))
it
}
#show heading.where(level: 2): it => {
set text(size: 12pt, fill: rgb("#444444"))
it
}
#show table: it => {
set text(size: 9pt)
it
}
// ─── PORTADA ───────────────────────────────────────────────────────────────
#align(center)[
#v(2cm)
#text(size: 20pt, weight: "bold", fill: rgb("#c04000"))[
Ontología filosófica \
y modelo operacional en software
]
#v(0.6cm)
#text(size: 12pt, fill: rgb("#666666"), style: "italic")[
Genealogía conceptual, autores y uso contemporáneo
]
#v(0.4cm)
#line(length: 60%, stroke: rgb("#c04000") + 0.5pt)
#v(0.4cm)
#text(size: 11pt)[Jesús Pérez Lorenzo]
#v(0.2cm)
#text(size: 10pt, fill: rgb("#888888"))[OntoRef · 2026]
#v(2cm)
]
#outline(
title: "Contenidos",
indent: 1.5em,
depth: 2,
)
#pagebreak()
// ─── 1. UNIVERSALES E INVARIANTES ──────────────────────────────────────────
= ¿Son los invariantes el equivalente a los universales filosóficos?
La pregunta es legítima pero la correspondencia es más fina de lo que parece a primera vista.
== Los universales en ontología filosófica
Los *universales* responden a una pregunta específica: _¿qué comparten múltiples particulares que los hace del mismo tipo?_ Son propiedades, relaciones o cualidades que pueden instanciarse en varios individuos distintos "rojez", "humanidad", "ser-mayor-que". El debate clásico gira en torno a su estatus ontológico:
- *Realismo platónico*: los universales existen independientemente de sus instancias las Formas habitan un reino separado del mundo sensible.
- *Realismo moderado* (Aristóteles): los universales existen _en_ las cosas, no aparte de ellas. No hay humanidad sin seres humanos concretos.
- *Nominalismo* (Ockham, siglo XIV): solo existen los particulares. Los universales son nombres (_nomina_), convenciones lingüísticas sin referente ontológico propio.
- *Conceptualismo* (posición media): los universales existen en la mente como conceptos, no en el mundo ni como meros nombres.
== Los invariantes como propiedades esenciales de un singular
Los invariantes en un modelo operacional de software no son universales en ninguno de los sentidos anteriores. No preguntan qué comparten múltiples proyectos preguntan _qué no puede cambiar en este proyecto concreto sin que deje de ser este proyecto_.
La analogía filosófica precisa es la distinción aristotélica entre *propiedades esenciales y accidentales*: la esencia es aquello sin lo cual una cosa no sería lo que es; el accidente es lo que puede cambiar sin alterar su naturaleza. Un invariante operacional declara: "esta propiedad es esencial para la identidad de este proyecto."
Kripke formalizó esto de manera más rigurosa en _Naming and Necessity_ (1980): ciertas propiedades son *rígidas* verdaderas en todos los mundos posibles accesibles al individuo. El agua es necesariamente H₂O aunque alguien la llame "phlogiston". Un invariante operacional tiene este carácter modal: no puede ser falso en ningún estado posible del proyecto sin que el proyecto haya dejado de ser ese proyecto. Los ADRs que lo modifican no _violan_ el invariante constituyen un nuevo individuo.
== Dónde sí hay resonancia con los universales
La familia semántica es compartida: ambos capturan _lo que no varía_. El universal no varía a través de la instanciación (la rojez es la misma en esta manzana y en esa). El invariante no varía a través de la evolución temporal del proyecto. El eje difiere instanciación vs. tiempo pero el gesto filosófico es análogo.
== El estatus ontológico: hechos institucionales
Hay una diferencia de fondo que ninguna de las categorías anteriores captura. Los universales platónicos existen independientemente de que alguien los declare. Los invariantes operacionales son *hechos institucionales* en el sentido de John Searle (_La construcción de la realidad social_, 1995): son verdad porque un colectivo los declaró y formalizó. Como el dinero, los contratos o las reglas del ajedrez su existencia depende de actos declarativos sostenidos por una práctica social. Un invariante que el equipo no reconoce como tal no es un invariante real; es teatro.
Esto los hace más frágiles que los universales metafísicos, pero también más honestos sobre su naturaleza.
#pagebreak()
// ─── 2. TABLA COMPARATIVA ──────────────────────────────────────────────────
= Genealogía filosófica del modelo operacional
La siguiente tabla mapea los elementos básicos de la ontología filosófica a sus equivalentes en modelos operacionales de software, con autores y origen temporal.
#v(0.5em)
#table(
columns: (1.8fr, 1.5fr, 1.8fr, 1.8fr),
inset: 7pt,
align: (left, left, left, left),
fill: (col, row) => if row == 0 { rgb("#c04000") } else if calc.odd(row) { rgb("#fdf6f0") } else { white },
table.header(
text(fill: white, weight: "bold")[Concepto filosófico],
text(fill: white, weight: "bold")[Autor / Origen],
text(fill: white, weight: "bold")[Equivalente operacional],
text(fill: white, weight: "bold")[Implementación],
),
[*Universales*],
[Platón (428348 aC) \ _República_, _Parménides_],
[Tipos / Schemas],
[Contratos Nickel: `String`, `[| 'Accepted, 'Proposed |]`],
[*Esencia / Accidente*],
[Aristóteles (384322 aC) \ _Metafísica_],
[Invariante / Estado mutable],
[`invariante = true` en `core.ncl` vs. dimensiones en `state.ncl`],
[*Potencia / Acto*],
[Aristóteles \ _Metafísica_ IX],
[Estado actual deseado + Gates],
[`current_state` / `desired_state` / `gate.ncl`],
[*Categorías* (10 predicamentos)],
[Aristóteles \ _Categorías_],
[Tipos de nodo ontológico],
[`invariants`, `tensions`, `practices`, `dimensions`, `gates`],
[*Sustancia primera / segunda*],
[Aristóteles \ _Categorías_],
[Instancia / Tipo],
[Proyecto concreto vs. schema de proyecto],
[*Identidad a través del tiempo*],
[Locke (1689) _Ensayo_ \ Parfit (1984) _Reasons_],
[Versioning + ADR lifecycle],
[`status = Superseded`, `superseded_by` identidad que evoluciona formalmente],
[*Dialéctica*],
[Hegel (1807) \ _Fenomenología del Espíritu_],
[Tensiones activas],
[`tensions` en `core.ncl` sin síntesis, gestionadas permanentemente],
[*Logos como tensión de opuestos*],
[Heráclito (535475 aC)],
[Tensión constitutiva irreducible],
[Dualidad ontología / reflection como tensión de diseño],
[*Mundos posibles*],
[Leibniz (1710) _Teodicea_ \ Kripke (1980) _Naming and Necessity_],
[FSM transiciones permitidas],
[ADRs como restricciones de accesibilidad entre estados],
[*Propiedades rígidas / esenciales*],
[Kripke (1980) \ _Naming and Necessity_],
[Invariantes como necesidad, no contingencia],
[Un invariante es verdad en todos los estados posibles del proyecto],
[*Límites del lenguaje = límites del mundo*],
[Wittgenstein (1921) \ _Tractatus Logico-Philosophicus_],
[Schema como frontera de lo expresable],
[Lo que no tipea en Nickel no puede existir en el sistema],
[*Juegos de lenguaje*],
[Wittgenstein (1953) \ _Investigaciones filosóficas_],
[Modos operacionales],
[Un modo define las reglas del juego para un contexto operacional],
[*Verdad como correspondencia*],
[Tarski (1933) \ _El concepto de verdad_],
[Typecheck / validación formal],
[`nickel export` como verificación de correspondencia con el schema],
[*Hechos institucionales*],
[Searle (1995) \ _La construcción de la realidad social_],
[ADRs como decisiones declaradas],
[Una constraint existe porque el equipo la declaró y comprometió],
[*Ontología formal vs. regional*],
[Husserl (1913) \ _Ideas I_],
[Schema formal vs. ontología del proyecto],
[`adrs/schema.ncl` (formal) vs. `.ontology/core.ncl` (regional)],
[*Filosofía del proceso*],
[Whitehead (1929) \ _Process and Reality_],
[Ontología (ser) vs. Reflection (devenir)],
[La dualidad estructural del protocolo capas con ritmos distintos],
[*Realismo estructural*],
[Ladyman & Ross (2007) \ _Every Thing Must Go_],
[Grafo como lo real, no las entidades],
[El DAG captura relaciones; los nodos son secundarios],
[*Lógica de predicados*],
[Frege (1879) \ _Begriffsschrift_],
[Queries sobre el grafo],
[`where severity == "Hard" | select id claim`],
)
#v(0.8em)
== Matices que la tabla no captura
*Aristóteles es el arquitecto base.* La esencia/accidente es exactamente invariante/estado-mutable. La potencia/acto es exactamente `current_state`/`desired_state` con la gate como umbral de actualización. Que esto haya tardado 2.300 años en aparecer explícitamente en software es indicativo de cuánto tiempo estuvo implícito en las decisiones de diseño.
*Whitehead contra Aristóteles es la tensión interna del modelo.* Aristóteles ve la realidad como sustancias con propiedades. Whitehead la ve como procesos no hay cosas, hay eventos. Un modelo operacional de software vive en esa grieta: la ontología es aristotélica (qué _es_ el proyecto), la capa de reflection es whiteheadiana (cómo _deviene_). Mantenerlas separadas es un reconocimiento implícito de que ninguna basta sola.
*Kripke resuelve lo que Aristóteles dejó ambiguo.* Aristóteles distingue esencial de accidental pero no formaliza la modalidad. Kripke lo hace: una propiedad esencial es rígida verdadera en todos los mundos posibles accesibles desde el individuo. Los invariantes operacionales tienen exactamente este carácter modal. Los ADRs que los modifican no violan los invariantes constituyen un nuevo individuo.
*La ausencia notable: Heidegger.* El _ser-en-el-mundo_ como condición de posibilidad de cualquier ontología explícita no aparece en ningún modelo operacional de software. Lo que Heidegger llamaría el trasfondo pre-temático el contexto que no se puede formalizar porque es la condición de posibilidad de la formalización es exactamente lo que los READMEs intentan capturar y fracasan. Un modelo operacional no lo resuelve; lo que hace es reducir la superficie de ese trasfondo haciendo explícito lo que puede serlo.
#pagebreak()
// ─── 3. QUIÉN CREÓ LA ONTOLOGÍA OPERACIONAL ────────────────────────────────
= La ontología operacional en software: origen, autores y uso actual
== El problema que había que resolver
A finales de los años 1970, los sistemas de inteligencia artificial enfrentaban un problema conocido como *el cuello de botella del conocimiento*: los sistemas expertos eran eficaces en dominios muy acotados pero no podían compartir conocimiento entre ni razonar fuera de su dominio específico. Cada sistema construía su representación del mundo de forma ad hoc, incompatible con el resto.
La pregunta que emergió fue: ¿es posible especificar formalmente el conocimiento de un dominio de manera que múltiples sistemas puedan compartirlo y razonar sobre él?
== Los fundadores
=== John McCarthy — el precursor (19581980s)
John McCarthy, fundador del término "inteligencia artificial" y creador de LISP, fue el primero en proponer que los sistemas de IA necesitaban _representaciones explícitas del mundo_ para razonar sobre él. Su noción de *situational calculus* (1969, con Patrick Hayes) un lenguaje formal para describir el mundo y sus cambios es el antecedente directo de la ontología operacional.
McCarthy adoptó el término "ontología" de la filosofía para referirse a los compromisos sobre qué tipos de cosas existen en el dominio modelado. No era una metáfora era un préstamo deliberado de la tradición aristotélica.
=== Marvin Minsky — frames y estructuras de conocimiento (1974)
En su influyente artículo _A Framework for Representing Knowledge_ (MIT AI Lab, 1974), Minsky propuso los *frames* como estructuras para organizar el conocimiento: cada frame representa un concepto con slots (atributos) y valores por defecto. Es la primera implementación computacional de algo cercano a la sustancia aristotélica con propiedades.
Los frames son el antecedente directo de los esquemas orientados a objetos y, más tarde, de las ontologías formales.
=== Tom Gruber — la definición canónica (1993)
Tom Gruber, en el Knowledge Systems Laboratory de Stanford, formuló la definición que se convirtió en estándar: _"An ontology is a specification of a conceptualization"_ (_A translation approach to portable ontology specifications_, Knowledge Acquisition, 1993).
La definición es deliberadamente minimalista: una ontología no captura la realidad captura un _punto de vista compartido_ sobre un dominio. Esto la aleja del realismo metafísico y la sitúa en el territorio de los hechos institucionales de Searle.
Gruber también desarrolló ONTOLINGUA, el primer sistema formal para escribir y compartir ontologías entre sistemas de IA heterogéneos.
=== Nicola Guarino — ontología formal en sistemas de información (1990s)
Nicola Guarino, del Instituto de Ciencias y Tecnologías Cognitivas (ISTC-CNR) de Italia, desarrolló la distinción entre *ontología de alto nivel* (conceptos universales como tiempo, espacio, proceso, objeto) y *ontología de dominio* (conceptos específicos de un campo).
Fundó la conferencia _Formal Ontology in Information Systems_ (FOIS, 1998), aún activa, que establece el puente entre la filosofía analítica y la ingeniería del conocimiento. Su trabajo sobre la distinción entre ontologías *descriptivas* (cómo los agentes conceptualizan el mundo) y *prescriptivas* (cómo deben conceptualizarlo para interoperar) es directamente aplicable a los modelos operacionales de software.
=== Doug Lenat — Cyc: el proyecto más ambicioso (19842017)
Doug Lenat inició el proyecto *Cyc* en 1984 en MCC (Microelectronics and Computer Technology Corporation), con el objetivo de codificar todo el conocimiento de sentido común humano como una ontología operacional. Fue el proyecto de representación de conocimiento más grande y longevo de la historia.
Cyc alcanzó más de 600.000 conceptos y 5 millones de aserciones. Demostró tanto el potencial como los límites del enfoque: la escala del conocimiento implícito humano es prácticamente inabarcable mediante codificación manual.
=== Tim Berners-Lee, James Hendler y Ora Lassila — la Web Semántica (2001)
En su artículo seminal en _Scientific American_ (2001), propusieron extender la Web con significado semántico: páginas que no solo humanos sino también máquinas pudieran "entender". La ontología era el mecanismo de interoperabilidad semántica.
Esto produjo las tecnologías RDF (Resource Description Framework), OWL (Web Ontology Language) y SPARQL el ecosistema de la Web Semántica que popularizaron el uso de ontologías en ingeniería de software más allá de la IA.
== Línea temporal condensada
#table(
columns: (1fr, 3fr),
inset: 7pt,
fill: (col, row) => if row == 0 { rgb("#c04000") } else if calc.odd(row) { rgb("#fdf6f0") } else { white },
table.header(
text(fill: white, weight: "bold")[Año],
text(fill: white, weight: "bold")[Hito],
),
[1958], [McCarthy propone representación explícita del conocimiento para IA],
[1969], [McCarthy & Hayes situational calculus: lógica de cambios en el mundo],
[1974], [Minsky frames: primera estructura computacional análoga a la sustancia aristotélica],
[1984], [Lenat inicia Cyc codificación masiva de conocimiento de sentido común],
[1991], [Primeras ontologías de alto nivel: DOLCE, SUMO conceptos universales compartidos],
[1993], [Gruber definición canónica: "ontología = especificación de una conceptualización"],
[1998], [Primera conferencia FOIS (Formal Ontology in Information Systems)],
[2001], [Berners-Lee et al. Web Semántica: RDF, OWL, SPARQL],
[2004], [OWL se convierte en recomendación W3C estándar industrial],
[20062010], [Wikidata, DBpedia ontologías abiertas a escala masiva],
[2012], [Google Knowledge Graph ontología operacional en producción para miles de millones de usuarios],
[2020s], [LLMs entrenados sobre grafos de conocimiento ontológicos; ontologías como contexto estructurado para agentes de IA],
)
== Uso actual
=== Biomedicina — el dominio más maduro
La ontología operacional tiene su mayor penetración en ciencias biomédicas:
- *Gene Ontology (GO)*: iniciada en 1998, describe funciones génicas de forma estandarizada entre organismos. Usada en todos los laboratorios de biología molecular del mundo.
- *SNOMED CT*: más de 350.000 conceptos médicos con relaciones formales. Estándar en sistemas de historia clínica electrónica en Europa, EEUU, Australia.
- *ICD-11* (OMS): clasificación internacional de enfermedades en formato ontológico desde 2018.
- *Human Phenotype Ontology (HPO)*: diagnóstico de enfermedades raras mediante matching ontológico entre síntomas del paciente y fenotipos conocidos.
=== Grafos de conocimiento empresariales
- *Google Knowledge Graph* (2012): más de 500.000 millones de hechos estructurados que alimentan los resultados de búsqueda y el panel de conocimiento.
- *Wikidata*: ontología abierta con más de 100 millones de elementos, mantenida colaborativamente, usada como fuente de conocimiento por Wikimedia, investigación académica y sistemas de IA.
- *schema.org*: vocabulario ontológico estándar para marcado semántico de páginas web, creado por Google, Microsoft, Yahoo y Yandex. Presente en la mayoría de sitios web modernos.
=== Industria y manufactura
- *ISO 15926* (petróleo y gas): ontología para integración de datos a lo largo del ciclo de vida de plantas industriales. Usada por Shell, BP, Statoil.
- *IFC* (Industry Foundation Classes): ontología para información de construcción. Base del estándar BIM (Building Information Modeling) en arquitectura e ingeniería civil.
- *Industry 4.0 ontologies*: RAMI 4.0, Asset Administration Shell representación formal de activos industriales en fábricas conectadas.
=== Inteligencia artificial y agentes
- *ConceptNet*: grafo de conocimiento de sentido común con 21 millones de aserciones, usado en entrenamiento de modelos de lenguaje.
- *Entrenamiento de LLMs*: Wikidata y DBpedia son fuentes estructuradas en los corpus de entrenamiento de GPT, LLaMA y modelos similares.
- *Contexto para agentes*: el uso emergente más relevante ontologías como contexto estructurado que se inyecta en agentes de IA para que operen con comprensión del dominio en lugar de inferirlo del lenguaje natural.
=== Software de proyecto — la frontera actual
El uso de ontologías operacionales _dentro_ de proyectos de software no para describir dominios externos sino para describir el propio proyecto es el desarrollo más reciente y menos formalizado.
Los precursores son los ADRs (Architecture Decision Records, propuestos por Michael Nygard en 2011) y los modelos C4 (Simon Brown). Pero estos son documentación, no grafos consultables.
La propuesta de *ontoref* pertenece a esta frontera: aplicar las estructuras de la ontología formal invariantes tipados, tensiones explícitas, estados formalizados, gates verificables al proyecto de software como entidad que se describe a misma. El proyecto _es_ el dominio, y la ontología _es_ el modelo operacional de ese dominio.
Lo que distingue este enfoque de la documentación tradicional es que el modelo es:
- *Consultable*: mediante pipelines estructuradas, no búsqueda de texto
- *Verificable*: el typecheck valida la coherencia en cada commit
- *Ejecutable*: los modos operacionales son grafos acíclicos que el sistema puede ejecutar
- *Machine-readable*: los agentes de IA pueden consumirlo directamente como contexto estructurado
== Conclusión
La ontología operacional en software no es una importación reciente de la filosofía es una convergencia que lleva setenta años ocurriendo, desde los frames de Minsky hasta los grafos de conocimiento de Google. Lo que ha cambiado con la proliferación de agentes de IA es la urgencia: cuando un agente puede modificar código, infraestructura o configuración de forma autónoma, la ausencia de un modelo explícito del proyecto no es una deuda técnica. Es una pérdida de control.
La ontología no es la solución a ese problema. Es el nombre del problema que hay que resolver.
#pagebreak()
// ─── 4. TAOÍSMO Y ONTOLOGÍA OPERACIONAL ────────────────────────────────────
= Taoísmo, Yin-Yang y ontología operacional
== Lo que la tradición occidental no vio
La ontología occidental desde Parménides privilegia el *ser* sobre el *no-ser*, la *presencia* sobre la *ausencia*, la *identidad* sobre la *diferencia*. Toda la tradición aristotélica-escolástica-analítica trabaja desde ese punto de partida.
El Taoísmo invierte la pregunta. El Tao (道) es previo a la distinción ser/no-ser. El primer capítulo del _Tao Te Ching_ (Laozi, ~siglo VI-IV aC):
#align(center)[
#text(style: "italic")[
道可道,非常道。名可名,非常名。\
El Tao que puede nombrarse no es el Tao eterno.\
El nombre que puede pronunciarse no es el nombre eterno.
]
]
Lo que no puede formalizarse no es una limitación del modelo es la condición de posibilidad de todo lo que puede formalizarse. La ontología operacional que puede expresarse completamente en un schema no captura la realidad operacional completa. Heidegger llegó al mismo lugar por camino diferente: el *trasfondo pre-temático* que hace posible cualquier acto de comprensión explícita es precisamente lo que escapa a la formalización.
== Yin-Yang — polaridad constitutiva, no oposición
La confusión más frecuente: yin y yang no son *contrarios* sino *complementarios que se definen mutuamente*. Las propiedades estructurales del par son:
- Cada polo *contiene una semilla del opuesto* representada por el punto de color contrario en el símbolo
- El exceso de un polo *genera su contrario* (極則反 la extremidad se invierte)
- La tensión entre ellos *nunca se resuelve* en síntesis solo se gestiona dinámicamente
- Uno *no existe sin el otro* yin sin yang es vacío inerte, yang sin yin es movimiento sin forma
Esto es estructuralmente distinto de la dialéctica hegeliana, que busca síntesis resolver la contradicción en una unidad superior. El yin-yang no resuelve. La tensión es constitutiva, permanente. Hegel usa la contradicción como *palanca*; el Taoísmo la habita como *condición*.
Heráclito (535-475 aC) es el occidental más cercano: su logos como tensión dinámica de opuestos "el camino hacia arriba y hacia abajo son el mismo" es estructuralmente idéntico al yin-yang. La diferencia: Heráclito vive en la tensión, Hegel la usa para superarla.
== Correspondencias con el modelo operacional
#table(
columns: (1.4fr, 1.3fr, 1.8fr, 1.8fr),
inset: 7pt,
fill: (col, row) => if row == 0 { rgb("#c04000") } else if calc.odd(row) { rgb("#fdf6f0") } else { white },
table.header(
text(fill: white, weight: "bold")[Concepto taoísta],
text(fill: white, weight: "bold")[Texto / Época],
text(fill: white, weight: "bold")[Equivalente occidental más cercano],
text(fill: white, weight: "bold")[Correspondencia operacional],
),
[*Tao* (道) principio previo a toda distinción],
[Laozi, _Tao Te Ching_ \ (~6-4 aC)],
[Ser de Heidegger previo a los entes],
[Trasfondo pre-temático que ningún schema captura completamente],
[*Yin / Yang* polaridad que no se resuelve],
[Tradición Zhou (~1100 aC) \ sistematizada en _I Ching_],
[Heráclito logos como tensión de opuestos],
[Ontología (ser) / Reflection (devenir) capas que se necesitan mutuamente],
[*Wu wei* (無為) acción sin forzar],
[Laozi, _Tao Te Ching_],
[Sin equivalente exacto en Occidente],
["No enforcement" coherencia que emerge de adopción justificada, no impuesta],
[*Ji* (機) el momento exacto de transformación],
[Zhuangzi, _Zhuangzi_ \ (~4 aC)],
[Kripke mundos posibles accesibles],
[Gates umbral cualitativo entre estados, no condición binaria],
[*Wu ji → Tai ji* de lo indiferenciado a la polaridad],
[Zhou Dunyi (1017-1073) \ _Taijitu shuo_],
[Husserl ontología formal vs. regional],
[schema.ncl (vacío universal) core.ncl (polaridad concreta del proyecto)],
[*Relatividad perspectival*],
[Zhuangzi la rana del pozo],
[Wittgenstein tardío juegos de lenguaje],
[Toda ontología es una conceptualización, no una captura de la realidad (Gruber)],
)
== Las cuatro correspondencias profundas
*Yin/Yang ↔ Ontología/Reflection.* La dualidad ya está en el diseño del protocolo. La ontología (core.ncl) captura lo que ES invariantes, estructura, ser; capa yang: formal, dura, definida. La reflection captura lo que DEVIENE operaciones, modos, movimiento; capa yin: fluida, procesual. Como en el símbolo: cada una contiene una semilla de la otra. La ontología describe estados *futuros* (desired_state) contiene movimiento. La reflection puede *actualizar* la ontología vía ADRs contiene forma. Separarlas es necesario porque tienen ritmos de cambio distintos; fundirlas colapsaría el yin en el yang.
*Wu wei ↔ "No enforcement".* Uno de los invariantes del protocolo: coherencia voluntaria que emerge de la adopción justificada, sin mecanismo de enforcement. Esto es wu wei aplicado a diseño de sistemas. No impones creas las condiciones para que el alineamiento emerja naturalmente. Un sistema de tipos fuertes fuerza; una ontología operacional orienta. La diferencia es exactamente la diferencia entre imponer el Tao y fluir con él.
*Tensiones ↔ Yin-Yang permanente.* Las tensiones en un modelo operacional ("formalización vs. fricción de adopción", "ontología vs. reflection") son explícitamente irresolubles. No se busca síntesis se gestionan. Si eliminas la tensión entre formalización y adopción maximizando uno de los polos, el sistema muere: o es un schema perfecto que nadie adopta, o es caos operacional que todos adoptan pero nadie puede razonar sobre él.
*Gates ↔ Ji (機).* El concepto taoísta de ji es el punto de inflexión donde el potencial se actualiza no un estado sino el umbral entre estados. Dinámico, cualitativo, contextual. Las gates en el modelo operacional son exactamente esto: no checkboxes son condiciones que marcan el momento en que un estado puede transformarse en otro.
== La crítica taoísta a la ontología clasificatoria
Los sistemas como OWL intentan eliminar la ambigüedad mediante clasificación precisa. El _Tao Te Ching_ advierte: cuanto más preciso el nombre, más excluyes. La realidad vive en el espacio *entre* las categorías, no en las categorías mismas.
Esto explica por qué las ontologías biomédicas las más maduras del campo sufren continuamente con casos límite, dependencia del contexto y la necesidad de múltiples ejes de clasificación simultáneos. La vista taoísta diría: estáis intentando resolver una tensión constitutiva. La precisión y la cobertura son yin y yang no se resuelven, se equilibran.
Zhuangzi va más lejos que Laozi. Toda distinción ontológica es perspectival. Esto no lleva al nihilismo sino a la *apropiada contextualidad*: la ontología correcta para un proyecto no es la más formalmente precisa sino la que el equipo puede sostener y evolucionar. Gruber llegó al mismo lugar en 1993 sin saberlo: una ontología es "una especificación de una conceptualización" un punto de vista compartido, no una verdad metafísica.
#pagebreak()
// ─── 5. DAGs ────────────────────────────────────────────────────────────────
= DAGs — genealogía y función en ontología operacional
== El problema que fuerza el grafo
Antes del grafo hay un problema: ¿cómo representar conocimiento estructurado de forma que se pueda razonar sobre él? La prosa no es consultable ni verificable. Las listas planas pierden las relaciones. Las jerarquías de árbol pierden los nodos con múltiples padres. El grafo emerge como la estructura mínima que captura relaciones arbitrarias entre conceptos.
Pero el grafo general tiene un problema: puede tener ciclos. Y los ciclos en ontología son epistemológicamente problemáticos.
== Por qué la aciclicidad — el problema de la justificación circular
Dos tradiciones independientes llegan al mismo resultado:
*Aristóteles* (_Analíticos Posteriores_): una definición válida procede de género más diferencia específica. El género es más general que lo definido la jerarquía es estrictamente descendente. Si A se define en términos de B y B en términos de A, ninguno está definido. La justificación circular es un defecto lógico.
*Russell* (1901): la paradoja del conjunto de todos los conjuntos que no se contienen a mismos. La solución en _Principia Mathematica_ (1910, con Whitehead) es la *teoría de tipos*: los tipos forman una jerarquía estricta un tipo de nivel N solo puede referirse a tipos de nivel N-1 o inferior. Esto es estructuralmente un DAG. La aciclicidad no es una restricción arbitraria es la solución formal al colapso por autorreferencia.
La convergencia: en lógica, ontología formal y teoría de tipos, la aciclicidad es el requisito de que las definiciones sean *bien fundadas*. No puede haber descenso infinito ni circularidad.
== Línea genealógica
#table(
columns: (0.8fr, 2fr, 2.5fr),
inset: 7pt,
fill: (col, row) => if row == 0 { rgb("#c04000") } else if calc.odd(row) { rgb("#fdf6f0") } else { white },
table.header(
text(fill: white, weight: "bold")[Año],
text(fill: white, weight: "bold")[Autor / Hito],
text(fill: white, weight: "bold")[Contribución al DAG en ontología],
),
[384-322 aC],
[Aristóteles _Analíticos Posteriores_],
[Definición bien fundada: jerarquía sin circularidad. Género diferencia especie.],
[1879],
[Frege _Begriffsschrift_],
[Lógica de predicados: relaciones formales entre conceptos. Base para representar grafos de conocimiento.],
[1901-1910],
[Russell & Whitehead _Principia Mathematica_],
[Teoría de tipos como DAG: aciclicidad como solución al colapso por autorreferencia.],
[1933],
[Tarski "El concepto de verdad"],
[Verdad como correspondencia entre proposiciones y estructuras (grafos). Base de la validación formal.],
[1958],
[US Navy PERT charts (programa Polaris)],
[DAG para planificación de proyectos: camino crítico como recorrido del grafo de dependencias temporales.],
[1969],
[Collins & Quillian "Retrieval time from semantic memory"],
[Primera red semántica computacional: is-a como DAG. La topología del grafo predice tiempos de reacción humanos.],
[1974],
[Minsky "A Framework for Representing Knowledge"],
[Frames con herencia múltiple: DAG sobre árbol. Primer sistema con nodos de múltiples padres.],
[1976],
[Feldman Make],
[Build system como DAG de dependencias. Orden topológico para compilación incremental. Ciclos = error explícito.],
[1985],
[Brachman & Schmolze KL-ONE],
[Description Logic: subsunción formal como DAG. Antecedente directo de OWL.],
[1988],
[Pearl _Probabilistic Reasoning in Intelligent Systems_],
[Causalidad como DAG: la asimetría temporal hace la causalidad inherentemente acíclica.],
[2000],
[Pearl _Causality_],
[Do-calculus: intervención como operación sobre el DAG causal. Separación entre correlación y causa.],
[2001],
[Berners-Lee et al. Web Semántica],
[RDF/OWL: grafos de conocimiento como estándar industrial. DAG de subsunción en jerarquías de clases.],
[2005],
[Torvalds Git],
[Historia de commits como DAG: irreversibilidad del tiempo en control de versiones. Merges = nodos con múltiples padres.],
)
== Las cuatro funciones del DAG en ontología operacional
*① Traversal determinista para análisis de impacto.* Si cambias un nodo, el DAG define exactamente qué nodos son alcanzables desde él (descendientes) y qué nodos lo alcanzan (ancestros). Esto es análisis de impacto: "si modifico este invariante, ¿qué tensiones, prácticas y gates se ven afectadas?" En un grafo cíclico esto es indecidible el cierre transitivo de un nodo puede ser todo el grafo.
*② Orden topológico para ejecución.* Los modos operacionales son DAGs de pasos con dependencias. El ordenamiento topológico da el orden de ejecución válido sin necesidad de un scheduler externo. Si el paso B depende del paso A, A siempre precede a B en cualquier topological sort.
*③ Justificación bien fundada — sin circularidad epistémica.* El debate clásico en epistemología enfrenta fundacionalismo (el conocimiento descansa en fundamentos no circulares DAG con nodos raíz) contra coherentismo (red de creencias mutuamente sustentadas puede ser cíclica). La ontología operacional es fundacionalista por diseño: los invariantes son nodos raíz justificados por ADR explícito, no por otros nodos del grafo. Un ciclo significaría: "este invariante existe porque esta práctica existe, y esta práctica existe porque este invariante existe" dogma circular, no arquitectura.
*④ Rollback con orden definido.* Deshacer cambios requiere un orden inverso bien definido. En un grafo cíclico no existe ese orden. El DAG garantiza que el rollback siempre tiene una secuencia válida.
== La tensión con el Taoísmo — DAG vs. ciclo
El DAG es *yang* en su estructura: direccional, jerárquico, asimétrico, irreversible. Codifica la flecha del tiempo las causas preceden a los efectos, las definiciones preceden a lo definido.
El yin-yang taoísta es *cíclico*: yin genera yang, yang genera yin. Las estaciones, el día y la noche, vida y muerte todo retorna.
La resolución está en separar dos temporalidades:
- *Sincrónica* (snapshot): el DAG es válido dentro de un momento dado. La ontología en $t_0$ es un grafo acíclico.
- *Diacrónica* (evolución): la secuencia de snapshots puede tener patrones cíclicos. Lo que era tensión puede convertirse en invariante; lo que era práctica puede ser abandonado y redescubierto.
El DAG captura la *estructura del ser en un momento*. El ciclo taoísta captura el *proceso de transformación a través del tiempo*. Son complementarios, no contradictorios exactamente como ontología y reflection son complementarios en el protocolo. El *I Ching* el libro de los cambios es un sistema para razonar sobre las transformaciones de un estado a otro: 64 hexagramas como estados (nodos), transformaciones entre ellos como aristas. El sistema completo es un grafo. No un DAG un grafo con ciclos, porque la realidad vuelve.
#pagebreak()
// ─── 6. FÍSICA CUÁNTICA ─────────────────────────────────────────────────────
= Física cuántica, computación cuántica y ontología operacional
== El golpe más profundo — Aristóteles estaba equivocado en el nivel fundamental
La ontología aristotélica asume que las sustancias tienen propiedades definidas. La mecánica cuántica demuestra que, en el nivel fundamental, las propiedades no son definidas hasta que se miden. Un electrón no *tiene* posición la posición emerge del acto de medición.
Esto no es ignorancia del observador sobre una propiedad preexistente. El *teorema de Kochen-Specker* (1967) lo prueba formalmente: es imposible asignar valores definidos a todos los observables cuánticos simultáneamente, incluso antes de la medición. La indeterminación es *ontológica*, no epistémica. El suelo sobre el que se construyeron 2.300 años de ontología occidental se mueve.
== Las tres conexiones genuinas
=== Complementariedad de Bohr ↔ Yin-Yang y la dualidad Ontología/Reflection
Niels Bohr (1885-1962) formuló el *principio de complementariedad*: onda y partícula son dos descripciones mutuamente excluyentes pero igualmente necesarias del mismo fenómeno. No puedes observar ambas simultáneamente el aparato de medición determina qué aspecto revelas.
Bohr lo conectó explícitamente con el Taoísmo: cuando fue ennoblecido en 1947 adoptó el símbolo yin-yang como parte de su escudo de armas con la inscripción _Contraria sunt complementa_ los contrarios son complementarios.
La dualidad ontología/reflection del modelo operacional tiene la misma estructura. El "aparato de medición" qué preguntas haces al sistema determina qué aspecto revelas. La capa que uses (core.ncl o los modos de reflection) determina qué ves. Ninguna es más verdadera son complementarias.
=== Contextualidad cuántica ↔ Ontología perspectival
El teorema de Kochen-Specker prueba algo más radical que la incertidumbre de Heisenberg: el resultado de medir un observable cuántico depende de *qué otros observables se miden simultáneamente*. Las propiedades son *contextuales* no existen independientemente del contexto de medición.
Esto convierte en hecho físico lo que tres tradiciones de esta discusión habían sostenido filosóficamente:
- *Zhuangzi*: toda distinción ontológica es perspectival
- *Wittgenstein tardío*: el significado depende del juego de lenguaje
- *Gruber*: una ontología es una especificación de una conceptualización, no una captura de la realidad
Para la ontología operacional: el "estado" de un proyecto es siempre relativo al actor y al contexto de consulta. Un agente de IA y un desarrollador humano consultando el mismo core.ncl están haciendo mediciones con aparatos distintos. Lo que ven es complementario, no idéntico.
=== El problema del observador ↔ El trasfondo que no puede formalizarse
En mecánica cuántica, la medición colapsa la función de onda el observador es parte del sistema. No hay _view from nowhere_ (Nagel, 1986). Todo conocimiento es conocimiento *desde* una posición.
Esto es exactamente lo que Heidegger llamó el trasfondo pre-temático la condición de posibilidad de cualquier formalización que ella misma no puede ser formalizada. Y lo que el _Tao Te Ching_ afirma: el Tao que puede nombrarse no es el Tao eterno.
La ontología operacional que crees que describes objetivamente está siendo constituida por el acto de escribirla. Esto no es un defecto del modelo es un límite estructural de toda representación.
== La interpretación más relevante — Mecánica Cuántica Relacional
Carlo Rovelli propuso en 1996 (_Relational Quantum Mechanics_, International Journal of Theoretical Physics) que no existen estados absolutos solo estados *relativos a otros sistemas*. El estado de una partícula no es una propiedad intrínseca es una propiedad relacional respecto al sistema con el que interactúa.
Esto es la ontología más radical posible y conecta directamente con Ladyman y Ross (realismo estructural): no hay propiedades, solo relaciones. Lo real son las estructuras relacionales, no las entidades.
Para el modelo operacional: el "estado" de un proyecto (current_state en state.ncl) no es una propiedad intrínseca del proyecto es relacional respecto al actor que consulta, los objetivos perseguidos, el momento del ciclo de vida. Dos actores con marcos de referencia distintos pueden tener mediciones del estado coherentes pero distintas. No hay contradicción hay contextualidad relacional.
== La computación cuántica — honestidad sobre sus límites
#table(
columns: (1.5fr, 2fr, 2fr),
inset: 7pt,
fill: (col, row) => if row == 0 { rgb("#c04000") } else if calc.odd(row) { rgb("#fdf6f0") } else { white },
table.header(
text(fill: white, weight: "bold")[Aspecto],
text(fill: white, weight: "bold")[Situación real (2026)],
text(fill: white, weight: "bold")[Relevancia para ontología operacional],
),
[Tamaño de grafos],
[La ventaja cuántica aparece a escala masiva],
[Los grafos de ontología de proyecto son pequeños sin ventaja cuántica],
[Algoritmos relevantes],
[Grover: O(√N) búsqueda. Shor: factorización polinómica],
[No mapean a consultas típicas sobre ontologías de proyecto],
[Decoherencia],
[Cualquier interacción colapsa la superposición],
[El uso práctico requiere aislamiento que contradice operación continua],
[Redes semánticas cuánticas],
[Investigación activa relaciones con amplitudes de probabilidad],
[Captura gradualidad que OWL binario pierde relevante a mediano plazo],
[Bayesian networks cuánticas],
[Extensión de Pearl con amplitudes complejas],
[Modelar dependencias con comportamiento emergente no reducible],
[LLMs y espacios de Hilbert],
[Los embeddings son productos internos en espacio vectorial de alta dimensión],
[Directamente relevante: la misma matemática cuántica, aplicada a semántica],
)
== La conexión no obvia — LLMs como geometría cuántica aplicada
Los Large Language Models operan en espacios vectoriales de alta dimensión donde el significado es relacional y contextual. No es metáfora cuántica es la misma matemática de espacios de Hilbert que usa la mecánica cuántica, aplicada a representación semántica.
La ontología operacional (core.ncl, invariantes, tensiones) es, en este contexto, una *base* en ese espacio un conjunto de vectores que fijan el marco de referencia en el que el agente opera. Inyectar el contexto ontológico al inicio de una sesión es, en la geometría de estos modelos, fijar el marco de referencia antes de que el agente empiece a computar.
La física cuántica importa aquí no porque vayamos a correr ontologías en computadores cuánticos sino porque demuestra que las asunciones ontológicas clásicas (propiedades definidas, estados absolutos, observador separado del sistema) son físicamente incorrectas en el nivel fundamental. La ontología operacional es contextual, relacional y perspectival *por diseño correcto*, no por limitación.
#pagebreak()
// ─── 7. RELEVANCIA EN SOFTWARE E INFRA ──────────────────────────────────────
= Por qué es relevante en software e infraestructuras digitales
== El problema real no es técnico — es epistémico
El software moderno tiene un déficit estructural que no se resuelve con mejores herramientas:
#align(center)[
#block(
fill: rgb("#1a1a1a"),
radius: 4pt,
inset: 12pt,
width: 90%,
)[
#set text(font: "Courier New", size: 9pt, fill: rgb("#cccccc"))
Lo que el sistema hace Lo que el equipo cree que hace \
Lo que la infra tiene desplegado Lo que Terraform declara \
Lo que el ADR decidió Lo que el código implementa \
Lo que el agente asume Lo que el contexto real requiere
]
]
Cada una de esas brechas es silenciosa, acumulativa, y solo se descubre cuando falla en producción. No son fallos de las personas son consecuencias inevitables de operar un sistema complejo sin un modelo del propio sistema.
== Lo que la ontología operacional hace que nada más hace
#table(
columns: (1.5fr, 1.5fr),
inset: 7pt,
fill: (col, row) => if row == 0 { rgb("#c04000") } else if calc.odd(row) { rgb("#fdf6f0") } else { white },
table.header(
text(fill: white, weight: "bold")[Documentación tradicional],
text(fill: white, weight: "bold")[Ontología operacional],
),
[Prosa no consultable],
[Grafo tipado consultable con queries estructuradas],
[Estática se desactualiza sin señal],
[Verificada en cada commit mediante typecheck],
[Describe lo que hubo],
[Formaliza lo que ES y lo que debe ser],
[Solo legible por humanos],
[Legible por humanos, agentes de IA y pipelines de CI],
[Captura el qué],
[Captura el qué, el por qué, y lo que no puede cambiar],
[Nadie la actualiza bajo presión],
[El proceso la actualiza es el path, no documentación paralela],
)
*Formaliza lo que no puede romperse.* Los invariantes no son comentarios son constraints tipadas que el sistema verifica antes del runtime, no después.
*Captura las tensiones activas.* "Coste vs. disponibilidad", "velocidad de despliegue vs. estabilidad" viven en la ontología, no en la cabeza del miembro más veterano del equipo. Cuando ese alguien se va, los trade-offs permanecen.
*Da al agente de IA el contexto que no puede inferir del código.* Un agente puede leer todo el código de un sistema y no saber que hay un acuerdo que impide escalar horizontalmente cierto servicio, o que la decisión de usar gRPC interno fue consecuencia de una tensión específica de latencia que no está en ningún archivo. La ontología operacional hace eso explícito y machine-readable.
== Por qué los DAGs — especificidad técnica
Los DAGs no son una elección estética. Son la única estructura que garantiza cuatro propiedades simultáneamente en sistemas operacionales:
*Análisis de impacto decidible.* Antes de cambiar un invariante o un nodo de infra recorre el DAG y sabes exactamente qué se ve afectado. Concreto: en Terraform, el grafo de recursos es un DAG. `terraform plan` recorre ese grafo para determinar qué debe crearse, modificarse o destruirse en qué orden. Si hubiera ciclos, el plan sería indecidible.
*Orden topológico para ejecución fiable.* Cualquier operación con dependencias despliegue, migración, rollback, onboarding tiene un orden correcto. El DAG lo da sin necesidad de un coordinador central. Este es el principio detrás de Make (1976), Bazel, Gradle, y cualquier build system no trivial.
*Justificación sin circularidad.* Si el invariante A existe porque la práctica B lo requiere, y la práctica B existe porque el invariante A lo define tienes dogma, no arquitectura. El DAG hace esa circularidad visible e inaceptable.
*Rollback determinista.* El inverso topológico del DAG es siempre computable. Sin DAG, el rollback es un proceso manual que depende de que alguien recuerde el orden correcto.
== Por qué la reflection — la ontología sin operación es un museo
La ontología captura el *ser*. Pero los sistemas de software no solo son *operan*. La reflection es la capa que hace la ontología ejecutable:
*Modos como DAGs operacionales.* Un procedimiento sin DAG es una lista de pasos que alguien puede ejecutar en cualquier orden, saltar, o ignorar. Un modo como DAG tiene dependencias explícitas el sistema puede verificar que se ejecutó correctamente antes de avanzar.
*ADRs con constraints tipadas.* Una decisión sin constraints es prosa. Con constraints tipadas es un contrato que el CI puede verificar en cada commit. La pregunta "¿estamos violando alguna decisión arquitectural?" pasa de ser reflexión manual a ser comprobación automática.
*Detección de drift.* La diferencia entre el estado sellado (lo que declaraste que era verdad) y el estado actual (lo que realmente hay desplegado) es medible si y solo si formalizaste el estado sellado. Sin eso, la pregunta "¿ha derivado esto?" no tiene respuesta objetiva.
== Las tres presiones que hacen esto urgente ahora
Estos conceptos no son nuevos. Lo nuevo es que tres presiones convergen simultáneamente haciendo que la ausencia del modelo operacional sea costosa de una forma que antes era tolerable:
*Presión 1: Los sistemas superan la capacidad de comprensión individual.* Un monolito de 2005 podía vivir en la cabeza de dos personas. Un sistema cloud-native de 2026 con decenas de microservicios, múltiples regiones, pipelines automatizados y SLOs por servicio no cabe en ninguna cabeza. El modelo mental individual ha dejado de ser suficiente como mecanismo de coordinación.
*Presión 2: Los agentes de IA tienen acceso de escritura.* Hasta hace poco, una herramienta de IA que generaba código incorrecto producía texto que un humano revisaba antes de ejecutar. Hoy los agentes pueden abrir PRs, modificar configuración, ejecutar Terraform, y desplegar. El ciclo humano-en-el-loop se comprime. Un agente sin contexto ontológico en ese entorno no es un asistente es un actor autónomo con comprensión incompleta del sistema que modifica.
*Presión 3: Los equipos son multi-actor y distribuidos.* Humanos en zonas horarias distintas, agentes de IA con distintos contextos de sesión, pipelines de CI ejecutando en paralelo todos escribiendo sobre el mismo sistema sin coordinador central. Sin un protocolo compartido y machine-readable para "qué puede cambiarse, bajo qué condiciones, con qué justificación", la coordinación depende de que todo el mundo lea el mismo Confluence actualizado. Eso nunca funcionó.
== La síntesis
Lo que la ontología operacional, reflection y DAGs aportan en conjunto es algo que ninguna de sus partes aporta sola:
#align(center)[
#block(
stroke: rgb("#c04000") + 1pt,
radius: 4pt,
inset: 14pt,
width: 88%,
)[
#set text(size: 11pt)
Un sistema que sabe lo que es, puede verificar que sigue siendo \
lo que dice ser, y puede comunicarlo a cualquier actor humano, \
agente o CI de forma estructurada y sin ambigüedad.
]
]
Eso no es documentación. Es un modelo operacional vivo.
La ausencia de ese modelo no es un problema de disciplina del equipo. Es una propiedad emergente de sistemas complejos operados sin representación explícita. El drift, la amnesia de decisiones, la ilusión de control en infraestructura declarativa no son fallos de las personas. Son consecuencias inevitables.
La ontología operacional es la respuesta a la pregunta: *¿qué estructura mínima necesita un sistema para conocerse a sí mismo de forma que ese conocimiento pueda ser usado, verificado y compartido?*
#pagebreak()
// ─── 8. REFERENCIAS ─────────────────────────────────────────────────────────
= Referencias y lecturas recomendadas
== Filosofía clásica y ontología
#set par(first-line-indent: 0em)
*Aristóteles.* _Metafísica_. Traducción de Tomás Calvo Martínez. Madrid: Gredos, 1994. [~330 aC] \
#text(fill: rgb("#666666"), size: 9.5pt)[El libro fundamental para la distinción esencia/accidente, potencia/acto y las categorías. El libro IX sobre potencia es la base del modelo estado-actual/estado-deseado.]
*Aristóteles.* _Analíticos Posteriores_. Traducción de Miguel Candel Sanmartín. Madrid: Gredos, 1988. [~350 aC] \
#text(fill: rgb("#666666"), size: 9.5pt)[Formaliza el requisito de definiciones bien fundadas antecedente directo de la aciclicidad en DAGs.]
*Laozi.* _Tao Te Ching_. Traducción y comentarios de Stephen Mitchell. Nueva York: Harper Perennial, 1992. [~siglo VI-IV aC] \
#text(fill: rgb("#666666"), size: 9.5pt)[Texto fundacional del Taoísmo. Los primeros y últimos capítulos son los más relevantes para la relación con ontología operacional.]
*Zhuangzi.* _Zhuangzi: The Complete Writings_. Traducción de Brook Ziporyn. Indianapolis: Hackett, 2020. [~siglo IV aC] \
#text(fill: rgb("#666666"), size: 9.5pt)[Especialmente los capítulos internos (1-7). La relatividad perspectival de Zhuangzi como crítica de toda ontología clasificatoria.]
*Frege, G.* _Begriffsschrift_. Halle: Nebert, 1879. \
#text(fill: rgb("#666666"), size: 9.5pt)[Primera formulación de la lógica de predicados. El antecedente formal de toda representación de conocimiento estructurado.]
*Russell, B. & Whitehead, A.N.* _Principia Mathematica_. Cambridge: Cambridge University Press, 1910-1913. \
#text(fill: rgb("#666666"), size: 9.5pt)[La teoría de tipos como DAG para evitar la autorreferencia circular. El capítulo introductorio explica el principio del círculo vicioso.]
*Wittgenstein, L.* _Tractatus Logico-Philosophicus_. Londres: Kegan Paul, 1922. \
#text(fill: rgb("#666666"), size: 9.5pt)["Los límites de mi lenguaje son los límites de mi mundo." El fundamento de por qué el schema Nickel define la frontera de lo representable en el sistema.]
*Wittgenstein, L.* _Investigaciones filosóficas_. Oxford: Blackwell, 1953. \
#text(fill: rgb("#666666"), size: 9.5pt)[Juegos de lenguaje como práctica. El significado como uso los modos operacionales como juegos de lenguaje de un proyecto.]
*Hegel, G.W.F.* _Fenomenología del Espíritu_. Traducción de Manuel Jiménez Redondo. Valencia: Pre-Textos, 2006. [1807] \
#text(fill: rgb("#666666"), size: 9.5pt)[Dialéctica tesis-antítesis-síntesis. Contrástese con el yin-yang: la diferencia entre síntesis y tensión gestionada permanentemente.]
*Husserl, E.* _Ideas relativas a una fenomenología pura y una filosofía fenomenológica_. México: FCE, 1993. [1913] \
#text(fill: rgb("#666666"), size: 9.5pt)[La distinción ontología formal/regional estructura vacía universal vs. conceptos de dominio específico.]
*Whitehead, A.N.* _Process and Reality_. Nueva York: Free Press, 1978. [1929] \
#text(fill: rgb("#666666"), size: 9.5pt)[La realidad como proceso, no sustancia. El fundamento filosófico de la capa reflection como devenir distinto del ser ontológico.]
*Kripke, S.* _Naming and Necessity_. Cambridge: Harvard University Press, 1980. \
#text(fill: rgb("#666666"), size: 9.5pt)[Propiedades esenciales rígidas y semántica de mundos posibles. El fundamento modal de los invariantes operacionales.]
*Searle, J.* _The Construction of Social Reality_. Nueva York: Free Press, 1995. \
#text(fill: rgb("#666666"), size: 9.5pt)[Hechos institucionales como fundamento de las decisiones declaradas. El ADR como acto de habla colectivo que crea realidad operacional.]
*Parfit, D.* _Reasons and Persons_. Oxford: Oxford University Press, 1984. \
#text(fill: rgb("#666666"), size: 9.5pt)[Identidad a través del tiempo. La pregunta "¿es el mismo proyecto?" cuando sus invariantes cambian vía ADR.]
*Ladyman, J. & Ross, D.* _Every Thing Must Go: Metaphysics Naturalized_. Oxford: Oxford University Press, 2007. \
#text(fill: rgb("#666666"), size: 9.5pt)[Realismo estructural: las relaciones son más reales que las entidades. El DAG como estructura fundamental, los nodos como secundarios.]
*Quine, W.V.O.* "On What There Is." _Review of Metaphysics_, 2(5), 1948. \
#text(fill: rgb("#666666"), size: 9.5pt)[El concepto de compromiso ontológico: toda teoría asume la existencia de ciertas entidades. Todo schema asume una ontología aunque no lo declare.]
*Tarski, A.* "The Concept of Truth in Formalized Languages." En _Logic, Semantics, Metamathematics_. Oxford: Clarendon Press, 1956. [1933] \
#text(fill: rgb("#666666"), size: 9.5pt)[Verdad como correspondencia. El fundamento de la validación formal nickel typecheck como verificación tarsquiana.]
== Inteligencia artificial y representación del conocimiento
*Minsky, M.* "A Framework for Representing Knowledge." MIT AI Lab Memo 306, 1974. \
#text(fill: rgb("#666666"), size: 9.5pt)[Los frames como primera estructura computacional análoga a la sustancia aristotélica con propiedades heredables.]
*Collins, A.M. & Quillian, M.R.* "Retrieval time from semantic memory." _Journal of Verbal Learning and Verbal Behavior_, 8(2), 240-247, 1969. \
#text(fill: rgb("#666666"), size: 9.5pt)[Primera red semántica computacional. Demuestra que la topología del grafo de conocimiento predice el comportamiento cognitivo.]
*Brachman, R.J. & Schmolze, J.G.* "An overview of the KL-ONE knowledge representation system." _Cognitive Science_, 9(2), 171-216, 1985. \
#text(fill: rgb("#666666"), size: 9.5pt)[Description Logic: la subsunción como relación formal que genera el DAG de conceptos. Antecedente teórico de OWL.]
*Gruber, T.* "A translation approach to portable ontology specifications." _Knowledge Acquisition_, 5(2), 199-220, 1993. \
#text(fill: rgb("#666666"), size: 9.5pt)[La definición canónica: ontología como especificación de una conceptualización. Referencia obligatoria para cualquier trabajo en ontología de software.]
*Lenat, D.B. & Guha, R.V.* _Building Large Knowledge-Based Systems: Representation and Inference in the Cyc Project_. Reading: Addison-Wesley, 1990. \
#text(fill: rgb("#666666"), size: 9.5pt)[El proyecto más ambicioso de ontología operacional. Sus fracasos son tan informativos como sus logros.]
*Pearl, J.* _Probabilistic Reasoning in Intelligent Systems_. San Mateo: Morgan Kaufmann, 1988. \
#text(fill: rgb("#666666"), size: 9.5pt)[Bayesian networks como DAGs causales. La formalización de por qué la causalidad es inherentemente acíclica.]
*Pearl, J.* _Causality: Models, Reasoning, and Inference_. Cambridge: Cambridge University Press, 2000. \
#text(fill: rgb("#666666"), size: 9.5pt)[El do-calculus. Distinción entre correlación y causa como operaciones sobre el DAG. Segunda edición (2009) incluye el modelo causal completo.]
*Guarino, N. (ed.)* _Formal Ontology in Information Systems_. Amsterdam: IOS Press, 1998. \
#text(fill: rgb("#666666"), size: 9.5pt)[Actas de FOIS'98. La conferencia fundacional del campo. Especialmente el artículo introductorio de Guarino sobre ontología formal vs. de dominio.]
*Berners-Lee, T., Hendler, J. & Lassila, O.* "The Semantic Web." _Scientific American_, 284(5), 34-43, 2001. \
#text(fill: rgb("#666666"), size: 9.5pt)[El artículo que popularizó las ontologías en ingeniería de software. La visión de la web como grafo de conocimiento estructurado.]
== Ingeniería de software y praxis
*Nygard, M.* "Documenting Architecture Decisions." Cognitect Blog, 2011. Disponible en: cognitect.com/blog \
#text(fill: rgb("#666666"), size: 9.5pt)[La propuesta original de ADRs como documentación ligera de decisiones. El precursor directo de los ADRs con constraints tipadas.]
*Brown, S.* _Software Architecture for Developers_. Leanpub, 2014. \
#text(fill: rgb("#666666"), size: 9.5pt)[El modelo C4 como alternativa a los diagramas UML. Introduce la idea de múltiples niveles de abstracción para describir un sistema.]
*Hohpe, G. & Woolf, B.* _Enterprise Integration Patterns_. Boston: Addison-Wesley, 2003. \
#text(fill: rgb("#666666"), size: 9.5pt)[Patrones como vocabulario compartido la idea de que nombrar los patrones operacionales es un acto ontológico.]
== Perspectivas adicionales
*Zhou Dunyi.* _Taijitu shuo_ (Explicación del Diagrama del Supremo Último). [1017-1073] \
#text(fill: rgb("#666666"), size: 9.5pt)[La cosmología taoísta sistematizada: wu ji tai ji yin-yang. La transición de lo indiferenciado a la polaridad como modelo de emergencia estructural.]
*Heidegger, M.* _Ser y Tiempo_. Traducción de Jorge Eduardo Rivera. Santiago: Editorial Universitaria, 1997. [1927] \
#text(fill: rgb("#666666"), size: 9.5pt)[El trasfondo pre-temático que ninguna ontología captura completamente. La ausencia notable en todos los modelos operacionales de software.]
*Heráclito.* _Fragmentos_. Edición de Rodolfo Mondolfo. Buenos Aires: Losada, 2007. [~500 aC] \
#text(fill: rgb("#666666"), size: 9.5pt)[El único pensador presocrático estructuralmente equivalente al yin-yang. El fragmento DK22B60 "el camino hacia arriba y hacia abajo son el mismo" es el punto de partida.]
*Bateson, G.* _Steps to an Ecology of Mind_. Nueva York: Ballantine, 1972. \
#text(fill: rgb("#666666"), size: 9.5pt)[La diferencia que hace una diferencia. Epistemología sistémica que conecta Taoísmo, cibernética y representación del conocimiento. Perspectiva poco citada en CS pero fundamentalmente relevante.]
*Winograd, T. & Flores, F.* _Understanding Computers and Cognition_. Norwood: Ablex, 1986. \
#text(fill: rgb("#666666"), size: 9.5pt)[La crítica más influyente a la IA simbólica desde Heidegger. Argumenta que el trasfondo de comprensión no puede formalizarse límite estructural de toda ontología operacional.]
== Física cuántica y fundamentos
*Bohr, N.* "Can Quantum-Mechanical Description of Physical Reality Be Considered Complete?" _Physical Review_, 48, 696-702, 1935. \
#text(fill: rgb("#666666"), size: 9.5pt)[La respuesta de Bohr al artículo EPR de Einstein-Podolsky-Rosen. Formula el principio de complementariedad como posición filosófica irrenunciable.]
*Bohr, N.* "Discussion with Einstein on Epistemological Problems in Atomic Physics." En Schilpp (ed.), _Albert Einstein: Philosopher-Scientist_. Evanston: Library of Living Philosophers, 1949. \
#text(fill: rgb("#666666"), size: 9.5pt)[El debate más importante de la física del siglo XX sobre interpretación cuántica. Bohr y Einstein representan dos ontologías irreconciliables.]
*Heisenberg, W.* _Physics and Philosophy: The Revolution in Modern Science_. Nueva York: Harper & Row, 1958. \
#text(fill: rgb("#666666"), size: 9.5pt)[La reflexión filosófica más accesible de uno de los fundadores. El capítulo sobre el papel del lenguaje en física cuántica conecta directamente con Wittgenstein.]
*Schrödinger, E.* "Die gegenwärtige Situation in der Quantenmechanik." _Naturwissenschaften_, 23(48-49-50), 1935. \
#text(fill: rgb("#666666"), size: 9.5pt)[El artículo del gato. Introduce el concepto de entrelazamiento (_Verschränkung_) y demuestra que la superposición cuántica no puede interpretarse clásicamente.]
*Kochen, S. & Specker, E.P.* "The Problem of Hidden Variables in Quantum Mechanics." _Journal of Mathematics and Mechanics_, 17(1), 59-87, 1967. \
#text(fill: rgb("#666666"), size: 9.5pt)[Prueba formal de que es imposible asignar valores definidos a todos los observables cuánticos simultáneamente. La contextualidad como hecho matemático, no filosófico.]
*Bell, J.S.* _Speakable and Unspeakable in Quantum Mechanics_. Cambridge: Cambridge University Press, 1987. \
#text(fill: rgb("#666666"), size: 9.5pt)[Colección de ensayos del autor del teorema de Bell (1964). El capítulo "Against measurement" es una crítica filosófica fundamental al vocabulario de la física cuántica.]
*Aspect, A., Grangier, P. & Roger, G.* "Experimental Realization of Einstein-Podolsky-Rosen-Bohm Gedankenexperiment." _Physical Review Letters_, 49(2), 91-94, 1982. \
#text(fill: rgb("#666666"), size: 9.5pt)[La confirmación experimental de que las desigualdades de Bell se violan. Prueba que la naturaleza es no-local no hay variables ocultas locales.]
*Everett, H.* "Relative State Formulation of Quantum Mechanics." _Reviews of Modern Physics_, 29(3), 454-462, 1957. \
#text(fill: rgb("#666666"), size: 9.5pt)[La interpretación de muchos mundos. Relevante porque ofrece una ontología donde todos los estados posibles son reales el antípoda de la ontología operacional que colapsa posibilidades en decisiones.]
*Rovelli, C.* "Relational Quantum Mechanics." _International Journal of Theoretical Physics_, 35(8), 1637-1678, 1996. \
#text(fill: rgb("#666666"), size: 9.5pt)[No existen estados absolutos solo estados relativos a otros sistemas. La ontología más radical de la lista y la más relevante para entender por qué el estado de un proyecto es siempre relacional.]
*Rovelli, C.* _Helgoland_. Milano: Adelphi, 2020. \
#text(fill: rgb("#666666"), size: 9.5pt)[La versión divulgativa de la mecánica cuántica relacional. Accesible y filosóficamente riguroso. El capítulo sobre relaciones y el budismo es relevante para la conexión con el Taoísmo.]
*Nagel, T.* _The View from Nowhere_. Oxford: Oxford University Press, 1986. \
#text(fill: rgb("#666666"), size: 9.5pt)[El argumento de que toda objetividad requiere un punto de vista. En tensión directa con el problema del observador cuántico y la ontología perspectival de Zhuangzi.]
*Nielsen, M.A. & Chuang, I.L.* _Quantum Computation and Quantum Information_. Cambridge: Cambridge University Press, 2000. \
#text(fill: rgb("#666666"), size: 9.5pt)[El texto estándar de computación cuántica. Los capítulos 2-3 sobre el formalismo matemático (espacios de Hilbert, operadores, entrelazamiento) son la base para entender la conexión con LLMs.]
*Deutsch, D.* _The Fabric of Reality_. Londres: Allen Lane, 1997. \
#text(fill: rgb("#666666"), size: 9.5pt)[La defensa más ambiciosa de la interpretación de muchos mundos. El capítulo sobre epistemología cuántica conecta con la cuestión de qué puede conocerse sobre un sistema complejo.]
== Ingeniería de software e infraestructura
*Evans, E.* _Domain-Driven Design: Tackling Complexity in the Heart of Software_. Boston: Addison-Wesley, 2003. \
#text(fill: rgb("#666666"), size: 9.5pt)[El lenguaje ubicuo como ontología operacional informal. El bounded context es estructuralmente equivalente a una ontología regional de Husserl aplicada a dominios de negocio.]
*Kleppmann, M.* _Designing Data-Intensive Applications_. Sebastopol: O'Reilly, 2017. \
#text(fill: rgb("#666666"), size: 9.5pt)[El capítulo sobre modelos de datos es el más relevante: la elección de modelo (relacional, documental, grafo) es una decisión ontológica define qué relaciones pueden existir.]
*Morris, K.* _Infrastructure as Code_. Sebastopol: O'Reilly, 2016. \
#text(fill: rgb("#666666"), size: 9.5pt)[Formaliza la declaratividad en infraestructura. El capítulo sobre principios hace explícita la distinción entre estado deseado y estado actual el núcleo del modelo state.ncl.]
*Humble, J. & Farley, D.* _Continuous Delivery_. Boston: Addison-Wesley, 2010. \
#text(fill: rgb("#666666"), size: 9.5pt)[El pipeline de despliegue como DAG de verificaciones. La idea de que cada etapa del pipeline es un gate que el sistema debe cruzar formalmente.]
*Kim, G., Humble, J., Debois, P. & Willis, J.* _The DevOps Handbook_. Portland: IT Revolution Press, 2016. \
#text(fill: rgb("#666666"), size: 9.5pt)[El flujo de trabajo DevOps como protocolo operacional. La sección sobre feedback loops es estructuralmente equivalente al ciclo ontología/reflection.]
*Dehghani, Z.* "How to Move Beyond a Monolithic Data Lake to a Distributed Data Mesh." Martin Fowler Blog, 2019. \
#text(fill: rgb("#666666"), size: 9.5pt)[Data mesh como ontología operacional distribuida para datos. Cada dominio es responsable de su propia ontología de datos la aplicación más cercana al modelo ontoref en infraestructura de datos.]
*Fowler, M.* "Who Needs an Architect?" _IEEE Software_, 20(5), 11-13, 2003. \
#text(fill: rgb("#666666"), size: 9.5pt)[Define arquitectura como "las decisiones que son difíciles de cambiar" equivalente exacto de los invariantes en ontología operacional.]
*Richardson, C.* _Microservices Patterns_. Shelter Island: Manning, 2018. \
#text(fill: rgb("#666666"), size: 9.5pt)[El patrón Saga como DAG de transacciones distribuidas. Cada paso del saga tiene depends_on implícito el DAG operacional aplicado a consistencia eventual.]
#text(fill: rgb("#666666"), size: 9.5pt)[La crítica más influyente a la IA simbólica desde Heidegger. Argumenta que el trasfondo de comprensión no puede formalizarse límite de toda ontología operacional.]

View file

@ -0,0 +1,53 @@
No pude acceder directamente al artículo de Sainath Palla en *AI in Plain English* (la página bloquea el acceso automatizado), pero pude reconstruir su contenido a partir de las extensas citas textuales que el artículo anterior de Axius-SDC reprodujo, además de información complementaria del autor sobre AI-FDE y la ontología de Palantir.
# Resumen: "Palantir AI FDE: cuando la ontología se convierte en motor de estrategia"
**Autor:** Sainath Palla (consultor senior de Palantir Foundry, fundador de Analyticorex). Publicado el 25 de marzo de 2026.
**Tesis central**
Palla argumenta que el AI-FDE de Palantir (la versión asistida por IA del clásico *Forward Deployed Engineer*) representa un cambio cualitativo, no incremental, en cómo se construyen los sistemas empresariales. Lo decisivo no es que la IA escriba código más rápido, sino que opera *sobre la ontología*: el modelo de datos vivo, los workflows existentes y el contexto operacional de la empresa. El AI-FDE traduce peticiones en lenguaje natural a operaciones de Foundry, encargándose de tareas como crear pipelines de transformación de datos, gestión de repositorios y construcción/mantenimiento de la ontología.
**El argumento clave**
> "AI-FDE no opera sobre código aislado. Trabaja sobre la ontología, el modelo de datos existente y los workflows que ya representan al negocio. Ese contexto compartido es lo que le permite generar algo significativo. Sin él, solo obtienes andamiaje más rápido."
Esa frase final —*sin la ontología solo obtienes andamiaje más rápido*— es la idea que vertebra el ensayo. Para Palla, los enfoques de IA centrados en el modelo (más parámetros, más velocidad) están atacando el problema equivocado: el cuello de botella empresarial no es generar código, sino dotar a la IA de un sustrato semántico sobre el cual razonar.
**El cambio en el rol del practicante**
Palla describe un desplazamiento del trabajo humano:
> "Menos esfuerzo va a escribir transformaciones, conectar datasets y unir workflows básicos, y más a decidir qué debe construirse, revisar cómo está estructurado y dar forma a cómo se comporta el sistema una vez en marcha."
El cuello de botella se mueve de la *implementación* al *juicio*. El practicante deja de ser un ejecutor para convertirse en arquitecto y revisor.
**El gobierno mediante branching**
Palla destaca un aspecto que evita el caos: cuando el AI-FDE realiza cambios, siempre crea una "Branch Proposal" y la presenta para revisión humana. Los humanos quedan liberados de la tarea tediosa de "conectar datos" y pasan al rol más avanzado de revisar y aprobar las ontologías construidas y las acciones propuestas por la IA para garantizar que se "alinean con la intención del negocio". Es, en esencia, *Git para operaciones empresariales*: cada cambio propuesto por la IA pasa por un pull request antes de tocar la realidad.
**La metáfora del ferrocarril**
Aquí Palla introduce su advertencia, que el artículo de Axius-SDC retomó críticamente:
> "Un cambio en un lugar empieza a influir en otros. Nada está realmente aislado. Los cambios no se quedan locales. Se propagan por todo el sistema."
A medida que las vías se tienden más rápido, el reto deja de ser construir una ruta aislada y pasa a ser cómo se cruzan, cómo se comparte la capacidad y cómo los cambios en una parte de la red afectan a todo lo demás. La velocidad de construcción aumenta la complejidad de las intersecciones.
**Conclusión del autor**
> "Los practicantes que piensan en términos de sistemas, no solo de implementaciones, se vuelven más importantes. A medida que la capacidad aumenta, la complejidad no desaparece. Se desplaza. Y alguien tiene que darle forma."
Para Palla, el AI-FDE no elimina al ingeniero: lo eleva. La IA escala la *construcción*, pero el *diseño sistémico*, las decisiones de gobernanza y la coherencia entre dominios siguen siendo trabajo humano —y se vuelven más críticos, no menos.
---
**Diferencia esencial entre los dos artículos**
| | Palla (este artículo) | Axius-SDC (artículo anterior) |
|---|---|---|
| Postura | Celebratoria: la ontología como "motor de estrategia" valida el enfoque de Palantir | Crítica: la ontología centralizada genera dependencia del proveedor |
| Foco | El cambio del rol del practicante hacia el juicio sistémico | Quién es el dueño de ese juicio (los FDEs de Palantir vs. los tuyos) |
| Metáfora | Red ferroviaria que crece y exige nueva disciplina | Estándar de ancho de vía distribuido en cada segmento |
Axius-SDC tomó las observaciones de Palla como punto de partida —admitiendo que tiene razón en el diagnóstico— para luego argumentar que la solución debería ser una gobernanza distribuida y de código abierto, en lugar de una ontología propietaria gestionada por los ingenieros desplegados de Palantir.

View file

@ -0,0 +1,89 @@
# Resumen: "Palantir AI FDE: Cuando la ontología se convierte en motor de estrategia"
**Autor:** Sainath Palla — 25 de marzo de 2026
---
**Punto de partida: el caso de uso de la cadena de suministro**
Palla abre con un ejemplo concreto: un sistema de resolución de escasez de inventario. Datos de ERP, proveedores y logística fluyen hacia una ontología modelada en torno a materiales, proveedores y pedidos. Pipelines calculan la escasez, una aplicación Workshop muestra opciones al planificador, y AIP Logic sugiere si conviene acelerar, sustituir o transferir inventario.
El sistema funciona, pero la observación clave es esta: **la mayor parte del esfuerzo no está en la recomendación**, sino en todo lo que la rodea — diseñar la ontología, alinear los pipelines, cablear los workflows y mantener consistentes los writebacks y la auditoría. Esa es la dificultad real del primer caso de uso.
---
**Lo que ha cambiado: la IA cruza al otro lado**
Antes, la IA vivía *dentro* del caso de uso (asistiendo decisiones ya construidas). El AI-FDE rompe esa frontera: ahora la IA participa en *cómo se construye el propio sistema*. Escala pipelines, sugiere extensiones de la ontología, conecta workflows y ensambla la aplicación. El trabajo no desaparece, se desplaza: menos esfuerzo en transformaciones y plumbing, más en decidir qué construir y cómo debe comportarse.
---
**Lo que se desbloquea**
Aparentemente es solo velocidad. Pero la velocidad solo importa porque el AI-FDE opera sobre un sustrato con contexto previo:
> "Sin él, solo obtienes andamiaje más rápido. Con él, empiezas a obtener sistemas usables mucho más rápido."
El verdadero desbloqueo es la **capacidad de extender un sistema operacional existente**, donde cada pieza nueva se construye sobre contexto compartido.
---
**Donde empieza a romperse**
Lo mismo que acelera la expansión la vuelve más difícil de gestionar. Cuando se añaden casos —rendimiento de proveedores, optimización de inventario, planificación de producción— todos operan sobre la misma ontología, los mismos datos, los mismos workflows. **Nada queda realmente aislado**. Lo nuevo no es la propagación de cambios, sino la *velocidad y frecuencia* con que se forman esas conexiones.
Metáfora: una **red ferroviaria**. Tender vías rápido es fácil; lo difícil es cómo se cruzan, cómo se comparte la capacidad y cómo un cambio en un punto afecta al resto.
---
**Consecuencia: la velocidad amplifica el comportamiento**
Cuando el sistema está alineado, el valor se compone rápido. Cuando hay desalineación, los pequeños fallos también se propagan —y son más difíciles de rastrear. La pregunta deja de ser "¿cómo construyo este caso?" para convertirse en "¿qué le pasa al sistema cuando este caso se añade?".
---
**El cuello de botella se ha movido**
La capa inferior (integración, pipelines, plantillas de workflow) se comprime. Pero la capa superior **no se comprime**: juicio de dominio, disciplina ontológica, diseño de gobernanza, límites de decisión y dónde dejar al humano en el loop. El AI-FDE cambia *qué tan rápido puedes construir*; no elimina la necesidad de decidir *qué vale la pena construir*.
---
**El sistema empieza a entenderse a sí mismo**
Cuando la ontología se estabiliza, el sistema deja de ser solo ejecutable y se vuelve **legible** —tanto para humanos como para la propia IA. El AI-FDE accede a la estructura de la ontología, las relaciones entre objetos, los workflows, las evaluaciones y las decisiones históricas. Eso le permite detectar señales: relaciones faltantes, workflows que terminan demasiado pronto, bucles de feedback incompletos, patrones duplicados.
> "El siguiente conjunto de casos de uso ya no viene solo desde fuera. Empieza a emerger desde dentro del propio sistema."
En ese momento, **la ontología deja de ser solo un modelo operacional y se comporta como un motor de estrategia**, mostrando para qué está listo el sistema a continuación. Esta es la idea que da título al artículo.
---
**Vuelta a los fundamentos**
A medida que construir se vuelve más fácil, los fundamentos pesan más. Una ontología mal definida o relaciones inconsistentes ya no solo generan fricción: *moldean el comportamiento de múltiples workflows simultáneos*. La diferencia entre construir para entregar y construir para crecer solo se ve cuando el sistema se expande.
---
**Las guardarraíles tienen que llegar antes**
Cuando muchos workflows dependen del mismo contexto, corregir deja de ser un acto local. Las definiciones claras, las relaciones consistentes y los límites de decisión explícitos ya no son tareas de limpieza posterior: son **parte del diseño desde el inicio**. Una vez que el sistema se expande, no estás arreglando un caso de uso —estás remodelando una capa operacional compartida.
---
**Reflexión final para practicantes**
> "Los practicantes que piensan en términos de sistemas, no solo de implementaciones, se vuelven más importantes. A medida que la capacidad aumenta, la complejidad no desaparece. Se desplaza. Y alguien tiene que darle forma."
El valor ya no está en qué tan rápido cableas un workflow, sino en qué tan bien entiendes el sistema como un todo: cómo se estructuran las decisiones, cómo se ensambla el contexto, cómo se hacen visibles los trade-offs y cómo interactúan las partes a lo largo del tiempo.
---
## Idea-fuerza
El artículo describe una **transición en tres tiempos**:
1. **De ejecutor a constructor:** la IA pasa de asistir dentro del caso de uso a participar en construir el sistema.
2. **De constructor a intérprete:** una vez la ontología es rica, la IA puede leer el estado del sistema y proponer qué falta.
3. **De intérprete a estratega:** la ontología deja de ser un modelo de datos y empieza a generar las próximas preguntas del negocio.
Por eso la conclusión no es "la IA reemplaza al ingeniero", sino que el cuello de botella se mueve hacia arriba: hacia el juicio sistémico, la disciplina ontológica y el diseño de cómo el sistema se comporta cuando crece.

View file

@ -0,0 +1,107 @@
# Ontoref — Por qué los DAGs de ontoref son diferentes
Referencia para posts, slides, presentaciones.
Contexto: análisis de por qué DAGs son ubicuos pero nadie tiene la perspectiva de ontoref.
---
## DAGs son ubicuos — y todos hacen lo mismo
En CI/CD, compiladores, runbooks, Airflow, dbt, Bazel: el DAG es un **modelo de ejecución**.
Las aristas significan "esto antes que aquello". El grafo describe *cómo se computa*,
no *qué es lo que se computa*. Es ordenación topológica, no conocimiento.
```
CI/CD: test → build → deploy
Compiler: parse → typecheck → codegen → link
Runbook: check_health → drain → restart → verify
```
Todos inerts. El grafo no sabe qué es el proyecto. No tiene semántica sobre sus propias aristas.
---
## Ontoref usa DAGs en dos modos distintos
**DAG ontológico — aristas con tipo semántico:**
```
Practice "ncl-schemas" --implements--> Principle "type-safety"
Practice "ncl-schemas" --enforces--> Constraint "zero-runtime-deps"
Practice "ncl-schemas" --enables--> Capability "agent-queryable-state"
Practice "ncl-schemas" --tension--> Practice "adoption-friction"
```
No es "esto antes que aquello". Es conocimiento — compromisos formales sobre qué ES
el proyecto y cómo se relacionan sus conceptos. El equivalente sería un compilador
que sabe por qué existe y qué trade-offs tomó.
**DAG de reflexión — aristas con contrato ejecutable + restricción de actor:**
```
step "validate-state" (actor: any) --> step "run-mode" (actor: developer)
step "run-mode" (actor: developer) --> step "report" (actor: agent|developer)
```
No es solo ejecución. El DAG es validado como contrato antes de ejecutarse,
registra su propio progreso, y sabe quién puede hacer qué.
---
## El gap que nadie ha cerrado
```
Knowledge Graph world: semántica sobre el dominio del negocio
Software tooling world: DAGs sobre la ejecución del software
ontoref: semántica sobre el proyecto de software en sí mismo
```
El mundo de knowledge graphs (W3C, RDF/OWL, Talisman) aplica semántica a dominios
empresariales — productos, clientes, transacciones. Nunca al proyecto de software mismo.
El mundo de herramientas de software (GitHub, Jira, CI/CD) trata el conocimiento del
proyecto como documentos no tipados. Usa DAGs para ejecución, nunca para representación
de conocimiento.
---
## La pieza que lo hace no-trivial
Los dos DAGs se hablan:
- El DAG ontológico dice *qué es* el proyecto (prácticas, principios, constraints activos)
- El DAG de reflexión *opera sobre ese estado* — detecta deriva, ejecuta modos, registra transiciones
- Las migraciones propagan cambios del DAG ontológico a proyectos consumidores
Sin esa conexión: ontología bonita que nadie mantiene (buenas intenciones)
o runbooks sin semántica (ejecución ciega).
---
## Por qué no había emergido antes
Tres condicionantes que ahora se alinean:
1. **Agentes AI** — que necesitan consumir conocimiento estructurado del proyecto,
no texto libre. Antes no había consumidor que lo requiriera con urgencia.
2. **NCL/Nickel** — contratos y tipos que permiten expresar ontología sin triplestore externo.
RDF/OWL requería infraestructura pesada y expertise especializado.
3. **La escala correcta** — Knowledge Graphs empresariales son proyectos de años.
Ontoref opera a escala de repositorio, con adopción incremental y sin enforcement.
---
## Para slides
**Una línea:** Todos usan DAGs para HACER cosas. Ontoref usa DAGs para CONOCER cosas
sobre el proyecto, Y para HACER cosas, Y hace que las dos capas se hablen.
**Contraste:**
| DAGs tradicionales | DAGs en ontoref |
|---|---|
| Modelo de ejecución | Modelo de conocimiento + ejecución |
| Aristas: "depende de" | Aristas: `implements`, `enforces`, `tension`, `enables` |
| Inert respecto al proyecto | Semánticamente cargado con el estado del proyecto |
| Evalúa y descarta | Evalúa, registra, propaga |
| Grafo describe proceso externo | Grafo describe el proyecto mismo |

View file

@ -0,0 +1,157 @@
# Ontoref — Scope: Proyecto, Infraestructura, Personal
Referencia para posts, slides, presentaciones.
Contexto: ontoref como protocolo de auto-conocimiento para cualquier sistema con identidad e intención,
más allá de gestión de proyectos de software.
---
## La afirmación central
Ontoref no es una herramienta de gestión de proyectos.
Es un **protocolo de auto-conocimiento para cualquier sistema con identidad e intención**.
Los tres dominios comparten la misma estructura profunda:
```
Ontology (Yin) Reflection (Yang)
────────────────────────── ──────────────────────────────
Proyecto Qué ES el software Qué DEVIENE — ADRs, modos,
principios, prácticas migraciones, deriva de docs
Infra Qué ES el sistema Qué DEVIENE — provisioning,
servicios, deps, drift detection, remediation,
constraints, topología runbooks como DAGs ejecutables
Personal Quién ERES Quién DEVIENS — decisiones con
valores, roles, consecuencias duraderas, hábitos,
principios vitales revisiones, deriva de intención
```
---
## Infraestructura
Infra ya usa DAGs (Terraform, Pulumi, Ansible) pero todos inerts: describen
*cómo se provisiona*, no *por qué existe así*. Ontoref sobre infra captura
las tensiones reales:
```
Servicio "auth" --tension--> Principio "stateless"
porque necesita session state pero el principio dice no
Constraint "zero-downtime" --enforces--> Práctica "blue-green"
y eso está en el ADR-infra-003, no en el Terraform
```
jj + Radicle + NATS federation = GitOps soberano donde cada entorno
(dev, staging, prod) se describe a sí mismo y puede ser consultado
desde el proyecto que lo consume.
**El problema que resuelve:** infraestructura que deriva sin registro de por qué
se tomaron las decisiones que la definen. El ADR de infra captura el "por qué"
con el mismo peso que un ADR arquitectónico de software.
---
## Personal
El dominio más radical. La misma pregunta que ontoref hace a un proyecto —
*¿qué eres, qué tensiones activas tienes, en qué estado estás respecto a donde
quieres estar?* — es exactamente la pregunta que el conocimiento personal
estructurado necesita responder.
No un segundo cerebro de notas. Un grafo de compromisos.
```
Valor "profundidad" --tension--> Rol "padre"
porque el tiempo es finito y los dos lo demandan
Decisión "dejar empresa X" --constraint--> Futuro laboral
con el mismo peso que un ADR arquitectónico
Estado "aprendiendo Rust" --blocker--> "contribuir a ontoref-core"
FSM personal, no lista de tareas
```
**El problema que resuelve:** la distancia entre quién intentas ser y quién
estás siendo crece sin mecanismo de detección. Reflection personal es ese mecanismo.
---
## Lo que todos los dominios comparten
Los tres tienen:
- **Identidad** — qué son, qué principios los definen
- **Tensiones activas** — trade-offs que no se resuelven, se gestionan
- **Decisiones con consecuencias duraderas** — el equivalente de ADRs
- **Estado observable** — dónde están vs. dónde quieren estar
- **Deriva** — la distancia entre intención y realidad que crece con el tiempo
- **Operaciones** — los modos que reducen esa deriva
Y todos se benefician del mismo stack:
- Almacenamiento soberano (NCL versionado junto al sujeto)
- Federación (NATS, modos federados)
- Multi-superficie (CLI + UI + MCP + GraphQL)
- VCS soberano (jj + Radicle)
---
## El stack completo — lo que añade cada capa
**Almacenamiento soberano:**
El enterprise knowledge graph asume un triplestore central. Ontoref invierte eso:
el conocimiento vive *con el sujeto*, en NCL versionado. Nadie puede quitarte
tu ontología porque está en tu repo/entorno/diario.
**Modos federados:**
Proyectos que se describen a sí mismos Y pueden ser descubiertos y consultados
por otros proyectos — sin servidor central.
```
proyecto-A consulta: ¿qué proyectos implementan "auth-pattern-X"?
proyecto-B, proyecto-C responden desde sus propias ontologías
sin intermediario que lo sepa todo
```
**CLI + UI + MCP + GraphQL:**
| Superficie | Consumidor |
|---|---|
| CLI (Nushell) | Desarrollador en terminal, scripts, CI |
| UI (axum) | Managers, revisión visual, onboarding |
| MCP | Agentes AI |
| GraphQL | Herramientas externas, dashboards, queries ad-hoc |
GraphQL sobre el DAG ontológico reduce la barrera de consumo sin sacrificar estructura.
SPARQL es el estándar semántico pero requiere expertise. GraphQL es lo que todo el mundo escribe.
**jj + Radicle:**
- jj: cada working copy es siempre un commit — alinea con ontoref, no hay "estado sucio"
- Radicle: P2P, local-first, sin GitHub — si el código es soberano, el conocimiento sobre ese código también
---
## Comparativa de posicionamiento
| Enterprise KG (Talisman) | Ontoref |
|---|---|
| Triplestore central | NCL distribuido, por sujeto |
| Equipo dedicado de ontólogos | El sujeto se mantiene a sí mismo |
| Dominio: datos organizacionales | Dominio: proyecto + infra + personal |
| GitHub/GitLab | jj + Radicle (soberano, P2P) |
| SPARQL sobre HTTP | CLI + UI + MCP + GraphQL |
| Un grafo global | Federación de sujetos auto-descritos |
| Escala: organización | Escala: individuo → proyecto → ecosistema |
---
## Para slides — una línea por dominio
- **Proyecto:** El código sabe compilar. Ontoref hace que sepa qué es y por qué.
- **Infra:** Terraform sabe provisionar. Ontoref hace que sepa por qué existe así.
- **Personal:** Tu agenda sabe qué haces. Ontoref hace que sepas quién eres y hacia dónde derivas.
**Cierre:** El sujeto más pequeño que puede adoptar ontoref es una persona.
El más grande es un ecosistema de proyectos federados.

View file

@ -0,0 +1,67 @@
# Ontoref — Yin/Yang: Lo que Talisman no resuelve
Referencia para posts, slides, presentaciones.
Contexto: extracción del análisis comparativo con "Steal This Deck" (Talisman, KGC 2026).
---
## El gap del argumento de Talisman
Talisman describe infraestructura de conocimiento como artefacto de construcción —
vocabularios, ontologías, knowledge graphs. El problema que identifica (governance
"forever, not a project") lo *nombra* pero no lo *resuelve*. Su stack termina en
"Govern" como sexto paso, sin mecanismo interno de mantenimiento.
**La pregunta que no responde:**
- ¿Quién vigila que la ontología no derive respecto al sistema real?
- ¿Quién detecta cuando una decisión arquitectónica invalida un nodo del grafo?
- ¿Quién propaga los cambios a los consumidores?
Sin ese cierre operacional, la ontología más rigurosa acaba siendo buenas intenciones
formalizadas. Archaeology, no infrastructure.
---
## Ontoref es las dos mitades
```
Ontology (Yin — lo que ES) Reflection (Yang — lo que DEVIENE)
─────────────────────────────── ────────────────────────────────────
.ontology/core.ncl reflection/modes/*.ncl
Nodos, edges, constraints DAGs ejecutables, pasos reportables
Compromisos formales Operaciones que cambian estado
state.ncl (fotografía) state.ncl FSM (transición activa)
ADRs (decisión tomada) migrations/ (propagación al ecosistema)
describe project sync diff --docs (detección de deriva)
```
Sin Reflection, la ontología cristaliza — describe lo que el proyecto *quería ser*
en el momento en que se escribió.
Sin Ontology, Reflection es ejecución sin verdad — sabe *qué hacer* pero no *qué es*.
---
## La tensión nombrada en core.ncl
> "Ontology vs Reflection: Ontology captures what IS (invariants, structure, being).
> Reflection captures what BECOMES (operations, drift, memory).
> Both must coexist without one dominating. This tension is onref's core identity."
Talisman articula bien el Yin. Ontoref es la respuesta al Yang que su framework no provee.
---
## Para slides
**Una línea:** Talisman dice "construye ontologías, goviernalas". Ontoref es el mecanismo
de governance interno que hace que esa instrucción sea ejecutable.
**Contraste visual:**
| Talisman (KGC 2026) | Ontoref |
|---|---|
| Construye knowledge infrastructure | Construye + mantiene desde adentro |
| Governance como paso final | Reflection como loop continuo |
| Ontología como artefacto | Ontología + operaciones coexistentes |
| "Forever, not a project" (aspiración) | FSM + modes + migrations (mecanismo) |

View file

@ -0,0 +1,97 @@
---
# Post metadata
id: "ai-knowledge-tool-who-keeps-it-alive"
title: "AI is a Knowledge Tool. But Who Keeps the Knowledge Alive?"
slug: "ai-knowledge-tool-who-keeps-it-alive"
subtitle: "Talisman is right — and stops exactly where the hard problem begins"
excerpt: "Jessica Talisman's KGC 2026 talk is the clearest articulation of why AI strategies fail: organizations invest in data infrastructure and expect reasoning to emerge. She's right. But her solution — build knowledge infrastructure — stops exactly where the hard problem begins: who keeps the knowledge from drifting?"
# Publication info
author: "Jesús Pérez"
date: "2026-05-10"
published: false
featured: false
# Categorization
category: "ontoref"
tags: ["ontoref", "knowledge-graphs", "ontology", "ai-infrastructure", "reflection"]
# Display
read_time: "7 min read"
sort_order: 1
css_class: "category-ontoref"
category_description: "Ontoref — protocol and tooling for structured self-knowledge in software projects"
category_published: true
---
# AI is a Knowledge Tool. But Who Keeps the Knowledge Alive?
Jessica Talisman delivered a talk at KGC 2026 called "Stop Betting, Start Building." The data she opens with is brutal:
- **89%** of firms report zero productivity impact from AI after three years (NBER, Feb 2026, n=5,937 executives)
- Experienced developers are **19% slower** with AI tools, not faster (METR RCT, 2025)
- The average AI-using worker gains **14 minutes** per week in net productivity (Foxit/Sapio, March 2026)
The market is bullish. The evidence is not.
Her diagnosis is correct: *AI is a knowledge tool, not a data tool.* Organizations are pouring money into data lakes, vector stores, and ETL pipelines and expecting context and reasoning to emerge from raw tokens. It won't. Models were trained on linked data, RDF triples, and controlled vocabularies — the top fifteen C4 training sources are knowledge-graph-heavy. They're then deployed against environments stripped of all that structure, and we wonder why they hallucinate.
Her prescription is also correct: build knowledge infrastructure. Controlled vocabularies first. Taxonomies. Thesauri. Ontologies — formal commitments, classes, properties, constraints. Knowledge graphs on top. And govern them. Forever, not as a project.
She's right. And she stops exactly where the hard problem begins.
## The Governance Gap
The sixth step in her stack is "Govern — this is infrastructure, stewardship, versioning, growth, forever." She names the problem. But naming it is not a mechanism.
The real question is: *who ensures the ontology doesn't drift from the system it describes?*
Software evolves. An architectural decision made in March invalidates a node in the graph by October. A new team joins with different mental models. A library gets replaced and a constraint becomes fictional. Knowledge graphs accumulate debt the same way codebases do — silently, without an alarm.
In the enterprise knowledge graph world, the answer to governance is headcount: hire ontologists, librarians, knowledge engineers. That works at organizational scale with dedicated budgets. It is not a viable answer for a software project, an infrastructure environment, or an individual trying to maintain structured self-knowledge.
## What's Actually Missing: the Operational Loop
Every serious ontology without an operational closure layer becomes archaeology within eighteen months. Beautiful, formally correct, and describing a system that no longer exists.
The missing piece is not more knowledge. It's a feedback loop that:
1. **Observes** the current state of the system against its declared intent
2. **Detects** drift before it becomes permanent
3. **Executes** operations that reduce that drift
4. **Records** decisions with lasting architectural weight
5. **Propagates** changes to everything that depends on this knowledge
This is the Yang to the ontology's Yin. Talisman describes the Yin in precise detail. The Yang is what makes it not an artifact.
## Ontoref: Both Halves
Ontoref is a protocol for structured self-knowledge in software projects. It operates as two coexisting layers that cannot function without each other:
**Ontology (what IS):** typed nodes and edges representing practices, principles, tensions, capabilities. Not documentation — formal commitments. A `Practice` node that `implements` a `Principle`, `enforces` a `Constraint`, `enables` a `Capability`, and is in active `tension` with another `Practice`. The project as a knowledge graph.
**Reflection (what BECOMES):** executable DAGs — operational modes that run against the ontological state, detect drift, report transitions, and propagate changes. The `sync diff --docs` command that catches when a crate's documentation has drifted from its ontology node. The FSM in `state.ncl` that tracks where each project dimension is versus where it intends to go. The migrations system that ensures protocol changes reach every consumer project.
The tension between these two layers is not a design flaw — it's named explicitly in the project's own ontology as its core identity:
> *"Ontology captures what IS. Reflection captures what BECOMES. Both must coexist without one dominating."*
Without Reflection, the Ontology crystallizes. It becomes a snapshot of what the project wanted to be at the moment it was written. Without Ontology, Reflection is execution without truth — it knows how to operate but not what it's operating on.
## The Accuracy Numbers
Talisman's data on what structured knowledge actually does to AI accuracy is worth repeating:
- Question-answering on enterprise SQL: **16% → 72%** when an ontology checks and repairs LLM-generated queries (Allemang & Sequeda, data.world AI Lab, 2024)
- GraphRAG vs. vector RAG: **3.4× accuracy** across 43 enterprise queries (Diffbot KG-LM Benchmark, 2023)
- Vector RAG collapses to **0% accuracy past five entities per query**. KG-grounded retrieval sustains performance well beyond that
The accuracy gap is not closed by a bigger model. It is closed by a defined schema, an ontology, and a validated query.
But only if the ontology is kept alive. Only if someone — or something — is running the operational loop that keeps it from drifting into fiction.
That's the part Talisman's framework doesn't provide. That's what Reflection is for.
---
*Ontoref is open source. The protocol specification, Nushell automation, and Rust crates are at [github.com/jesusperezlorenzo/ontoref](https://github.com/jesusperezlorenzo/ontoref).*

View file

@ -0,0 +1,106 @@
---
# Post metadata
id: "dags-everywhere-none-know-what-they-are"
title: "DAGs Are Everywhere. None of Them Know What They Are."
slug: "dags-everywhere-none-know-what-they-are"
subtitle: "Every build system, CI pipeline, and runbook uses DAGs for execution. Ontoref uses them for knowledge."
excerpt: "CI/CD pipelines, compilers, runbooks, data orchestrators — they all use directed acyclic graphs. Every single one of them uses DAGs as execution models: this before that, topological ordering, dependency resolution. None of them use DAGs to represent what the system is, why it exists, or what trade-offs define it. That's the gap ontoref fills."
# Publication info
author: "Jesús Pérez"
date: "2026-05-10"
published: false
featured: false
# Categorization
category: "ontoref"
tags: ["ontoref", "dag", "ontology", "knowledge-graphs", "software-architecture"]
# Display
read_time: "6 min read"
sort_order: 2
css_class: "category-ontoref"
category_description: "Ontoref — protocol and tooling for structured self-knowledge in software projects"
category_published: true
---
# DAGs Are Everywhere. None of Them Know What They Are.
Every build system uses DAGs. Every CI/CD pipeline uses DAGs. Compilers, data orchestrators, runbooks, package managers, Kubernetes operators — all DAGs. Directed acyclic graphs are so ubiquitous in software infrastructure that the question "why DAGs?" barely registers as a question anymore.
But there's a second question nobody asks: *what do those DAGs represent?*
The answer is always the same: **execution order**. This before that. Topological sorting. Dependency resolution. The graph describes *how* something computes, not *what* it is.
```
CI/CD: test → build → deploy
Compiler: parse → typecheck → codegen → link
Runbook: check_health → drain → restart → verify
```
These graphs are inert with respect to the system they operate on. A CI pipeline doesn't know what the project is, why it was built this way, or what trade-offs define its architecture. It knows that tests must pass before deployment. That's all it knows.
## The Semantic Gap
This inertness is not a flaw — it's appropriate. Build systems should be fast and mechanical. Runbooks should be executable without requiring philosophical knowledge about the system. Execution graphs and knowledge graphs are different tools for different purposes.
The problem is that the software industry has reached for DAGs for *everything except knowledge representation*. The knowledge of what a system is — its principles, its architectural decisions, its active tensions, its capability surface — lives in documents. Wikis. READMEs. Tickets. Verbal tradition.
These are all unstructured, un-typed, un-queryable, and guaranteed to drift from the actual system within months. They describe what the project was, not what it is.
## Ontoref's Dual DAG
Ontoref uses DAGs in two fundamentally different modes, and the distinction matters:
**Ontological DAG — semantically typed edges:**
```
Practice "ncl-schemas" --implements--> Principle "type-safety"
Practice "ncl-schemas" --enforces--> Constraint "zero-runtime-deps"
Practice "ncl-schemas" --enables--> Capability "agent-queryable-state"
Practice "ncl-schemas" --tension--> Practice "adoption-friction"
```
These edges are not "depends on." They are typed relationships: `implements`, `enforces`, `enables`, `tension`. The graph is the knowledge — formal commitments about what the project is and how its concepts relate. The equivalent would be a compiler that knows why it exists and what architectural trade-offs it made.
**Reflection DAG — executable contracts with actor restrictions:**
```
step "validate-state" (actor: any) --> step "run-mode" (actor: developer)
step "run-mode" (actor: developer) --> step "report" (actor: agent|developer)
```
This is not just execution ordering. The DAG is validated as a contract before it runs, records its own progress through state transitions, and encodes who can execute each step. An agent trying to run a developer-restricted step receives a typed rejection, not a runtime error.
## The Missing Connection
What makes this non-trivial is that the two DAGs communicate:
- The ontological DAG declares what the project IS — active constraints, practices in tension, current state dimensions
- The reflection DAG OPERATES on that state — detecting drift, executing modes, recording transitions
- Migrations propagate changes in the ontological DAG to downstream consumer projects
In every other system that uses DAGs, the graph is evaluated and discarded. In ontoref, evaluation modifies the state that the next execution reads. The graph has memory.
## Why This Didn't Exist Before
Three things had to align:
**Agents as consumers.** Before agentic AI, there was no automated consumer that needed structured knowledge about a project. Humans could read the wiki. Agents cannot — they need typed, queryable, machine-readable project knowledge to work accurately rather than hallucinate context. Ontoref's knowledge DAG is what agents consume via MCP.
**Configuration languages with contracts.** Expressing an ontology without an external triplestore required a configuration language with types and contracts. Nickel (NCL) provides exactly that: a typed, lazy configuration language where schema violations are caught at evaluation time, not at runtime. RDF/OWL would have required dedicated infrastructure and specialist expertise.
**Repository scale, not enterprise scale.** Enterprise knowledge graphs are multi-year initiatives. Ontoref operates at the scale of a single repository — incremental adoption, no enforcement, no dedicated team. The smallest unit that can adopt it is a project of one.
## The Consequence
When a project has a knowledge DAG that its agents can consume, the accuracy arithmetic changes entirely. The numbers from KGC 2026 research:
- Ontology-grounded retrieval: **3.4× more accurate** than vector RAG on enterprise queries
- Ontology-validated queries: accuracy jumps from **16% to 72%** on SQL question-answering
The gap is closed not by a bigger model but by structured knowledge the model can reason against. A DAG where the edges mean something.
---
*Ontoref is open source. The protocol specification, Nushell automation, and Rust crates are at [github.com/jesusperezlorenzo/ontoref](https://github.com/jesusperezlorenzo/ontoref).*

View file

@ -0,0 +1,122 @@
---
# Post metadata
id: "one-protocol-three-subjects"
title: "One Protocol, Three Subjects"
slug: "one-protocol-three-subjects"
subtitle: "The same question applies to your project, your infrastructure, and yourself"
excerpt: "Ontoref started as a protocol for software project self-knowledge. The same architecture — ontological DAG for what IS, reflection DAG for what BECOMES — applies without modification to infrastructure environments and to individuals. The subject changes. The question is identical: what are you, what tensions define you, where are you versus where you intend to be?"
# Publication info
author: "Jesús Pérez"
date: "2026-05-10"
published: false
featured: false
# Categorization
category: "ontoref"
tags: ["ontoref", "ontology", "infrastructure", "personal-knowledge", "reflection"]
# Display
read_time: "7 min read"
sort_order: 3
css_class: "category-ontoref"
category_description: "Ontoref — protocol and tooling for structured self-knowledge in software projects"
category_published: true
---
# One Protocol, Three Subjects
The question ontoref asks of a software project is precise: *What are you? What principles define you? What tensions are you actively managing? What decisions have you made with lasting consequences? Where are you versus where you intend to be?*
This turns out to be the same question that infrastructure environments need answered. And individuals.
The subject changes. The question is identical.
## What All Three Have in Common
Any system with identity and intent shares the same structural problem:
- **Identity** — what it is, what principles define it
- **Active tensions** — trade-offs that don't resolve, they're managed
- **Durable decisions** — choices with lasting consequences that constrain the future
- **Observable state** — where it is versus where it intends to be
- **Drift** — the distance between intention and reality, growing silently with time
- **Operations** — the actions that reduce that drift
Software projects have all of these. So do infrastructure environments. So do people.
And they all benefit from the same architecture: an ontological layer that captures what IS, and a reflection layer that captures what BECOMES and operates to keep those two aligned.
## Infrastructure
Infrastructure already uses DAGs — Terraform, Pulumi, Ansible. All of them describe *how* the system is provisioned. None of them capture *why it exists as it does*.
The `why` is where knowledge lives:
```
Service "auth" --tension--> Principle "stateless"
reason: needs session state, but the principle forbids it
Constraint "zero-downtime" --enforces--> Practice "blue-green"
reason: ADR-infra-003, after the 2025 deploy incident
```
This information is not in the Terraform plan. It's in the memory of the engineer who was there, in a Slack thread from eight months ago, or nowhere at all. When that engineer leaves, the infrastructure loses its own history — it knows what it is, not why.
Infrastructure ontoref captures the architectural decisions of the infrastructure itself, with the same ADR formalism as software projects. It tracks state dimensions — which environments are stable, which are in active migration, what's blocked and why. The FSM doesn't track deployment status; it tracks *epistemic status* — how well the infrastructure understands itself.
Federation via NATS means each environment (dev, staging, prod) describes itself and can be queried from the projects that depend on it. Sovereign GitOps: the infrastructure knowledge lives with the infrastructure code, not in a vendor's database.
## Personal
This is the most radical application. But it follows directly from the same logic.
A person with values, roles, and long-term intentions faces exactly the same structural problem as a software project:
```
Value "depth" --tension--> Role "parent"
reason: time is finite and both demand it fully
Decision "left company X" --constraint--> Future career choices
with the same architectural weight as ADR-001
State "learning Rust" --blocker--> "contribute to ontoref-core"
a personal FSM dimension, not a task list item
```
The knowledge graph here is not a productivity system. It's not a second brain of notes. It's a graph of *commitments* — typed, provenanced, with active tensions named rather than suppressed.
The drift problem is identical: who you are becoming versus who you intended to be. Reflection modes for a person might be weekly reviews, annual state assessments, or triggered reviews when a major decision introduces new constraints. The operational loop is the same — observe, detect drift, execute, record.
## The Same Stack, Different Subject
All three domains run on the same infrastructure:
**Sovereign storage:** Knowledge lives alongside its subject — in the project repo, in the infrastructure code, in a personal vault. No vendor holds it. No platform dependency. NCL files versioned with the same discipline as code.
**Multi-surface access:**
| Surface | Consumer |
|---|---|
| CLI (Nushell) | Developer, scripts, CI |
| UI (axum) | Visual review, onboarding |
| MCP | AI agents |
| GraphQL | External tools, dashboards |
**VCS alignment:** jj's model — where the working copy is always a commit — aligns naturally with ontoref's approach to continuous state capture. There is no "dirty state"; every moment is capturable. Radicle as transport makes the whole stack sovereign and P2P — code collaboration without a central platform.
**Federation:** Projects querying each other's knowledge graphs without a central broker. Infrastructure environments exposing their self-knowledge to the projects that depend on them. Individuals whose personal knowledge graph informs their contributions to the projects they work on.
## Why Scale Matters
The smallest unit that can adopt ontoref is a person. One developer, one project, one infrastructure environment. No team required. No budget conversation.
The largest unit is an ecosystem of federated, self-describing projects that can query each other's knowledge without any of them being the authoritative central source.
That range — individual to ecosystem — is possible because the protocol is the same at every scale. The ontological DAG and the reflection DAG don't change shape based on the size of the subject. They change in density and depth, not in structure.
The organizations that Talisman argues should invest in knowledge infrastructure are trying to build this top-down, with dedicated teams and multi-year programs. Ontoref makes it available bottom-up — starting with a single project, a single environment, a single person, each describing themselves in the same language.
---
*Ontoref is open source. The protocol specification, Nushell automation, and Rust crates are at [github.com/jesusperezlorenzo/ontoref](https://github.com/jesusperezlorenzo/ontoref).*

View file

@ -0,0 +1,106 @@
---
# Post metadata
id: "your-ontology-should-live-with-your-code"
title: "Your Ontology Should Live With Your Code"
slug: "your-ontology-should-live-with-your-code"
subtitle: "Sovereignty, federation, and why knowledge infrastructure without a home isn't infrastructure at all"
excerpt: "Enterprise knowledge graphs live in a triplestore maintained by a dedicated team on a vendor's platform. When the team changes, the budget gets cut, or the vendor pivots, the knowledge disappears. Ontoref takes the opposite approach: sovereign, local-first knowledge that lives alongside its subject, versioned, queryable across four surfaces, and federated without a central broker."
# Publication info
author: "Jesús Pérez"
date: "2026-05-10"
published: false
featured: false
# Categorization
category: "ontoref"
tags: ["ontoref", "sovereignty", "federation", "radicle", "jujutsu", "knowledge-infrastructure"]
# Display
read_time: "6 min read"
sort_order: 4
css_class: "category-ontoref"
category_description: "Ontoref — protocol and tooling for structured self-knowledge in software projects"
category_published: true
---
# Your Ontology Should Live With Your Code
Jessica Talisman, in her KGC 2026 talk, says something worth highlighting: *"Your ontology is your moat — your IP."*
She's right. And she's describing the enterprise knowledge graph model, where the moat is held in a vendor's triplestore, maintained by a dedicated team, on a platform with a subscription. When the team changes, the budget gets cut, or the vendor pivots, the moat drains.
Ontoref takes the opposite approach.
## What Sovereign Knowledge Means
In ontoref, the knowledge of what a project is — its principles, practices, tensions, architectural decisions, operational state — lives as NCL files alongside the code. In the repository. Versioned with the same discipline as the source.
This is not a documentation approach. It's a storage decision with architectural consequences:
- **No vendor can take it from you.** The ontology is in your repo, not their database.
- **No platform migration.** GitHub, Gitea, Radicle — the knowledge moves with the code.
- **No dedicated infrastructure.** The daemon runs locally; the files are the source of truth.
- **Offline by default.** Knowledge that requires a network connection to exist is not infrastructure — it's a service.
The closest analogy is SQLite vs. PostgreSQL: local-first, embedded, always available, no connection required. When you need distribution, you add it. But the baseline is sovereignty.
## Four Surfaces, One Source
Sovereign storage doesn't mean isolated. Ontoref exposes the same ontological knowledge across four surfaces simultaneously:
**CLI (Nushell):** The developer's native interface. `ontoref describe project`, `ontoref graph ontology`, `ontoref sync diff --docs`. Fast, scriptable, CI-composable. The surface for humans who live in the terminal and for automated checks that run in pipelines.
**UI (axum):** A web interface for visual inspection, onboarding, and management. Not a dashboard over a remote API — a local server that renders the same NCL-backed knowledge. The surface for project managers, new contributors, or anyone who prefers navigating a graph visually.
**MCP:** The agent surface. Ontoref's MCP endpoint is the semantic layer that sits above the transport protocol. As Talisman's framework correctly identifies, MCP moves bytes — it doesn't establish shared meaning. Ontoref provides that meaning through its ontological layer, exposed via MCP to agents that need structured knowledge about the project to work accurately rather than hallucinate context.
**GraphQL:** The integration surface. SPARQL is the semantic web standard, but GraphQL is what most developers know and every tooling ecosystem supports. A GraphQL API over the ontological DAG removes the barrier of expertise without sacrificing structure.
Four surfaces, one canonical source: the NCL files in the repository.
## jj and Radicle
The storage model pairs naturally with a specific class of VCS tooling.
**Jujutsu (jj):** A Git-compatible VCS where the working copy is always a commit — there is no dirty state. This aligns directly with ontoref's approach to continuous state capture: every project state is capturable, every transition is a first-class event. jj's operation log is the VCS equivalent of ontoref's reflection session history.
**Radicle:** P2P, local-first, sovereign code collaboration. If the code is sovereign, the knowledge about that code should be too. Radicle as transport means the federation of ontoref-enabled projects doesn't depend on any centralized platform — no GitHub, no GitLab, no hosted Gitea. Nodes discover each other and exchange knowledge directly.
Together: sovereign code and sovereign knowledge, distributed without a central broker.
## Federation Without a Central Broker
The enterprise model for knowledge federation is a central knowledge graph that all projects point to. One team maintains it. One platform hosts it. Every query routes through it.
Ontoref's federation model is different:
```
project-A queries: which projects implement "zero-trust-auth"?
project-B, project-C respond from their own ontologies
no central server that knows everything
```
This is possible because each project is self-describing. The ontological DAG in `.ontology/core.ncl` is queryable without any external dependency. Federation via NATS connects the daemons — each project's daemon registers itself, publishes its capabilities, and responds to queries from other daemons in the federation.
The result: ecosystem-level visibility without ecosystem-level centralization. You can discover which projects share your architectural constraints, which implement compatible practices, which are in conflicting states — without any of them requiring a shared platform.
## Why This Architecture Now
Two shifts made this viable:
**Agentic AI as the consumer.** Before agents, knowledge infrastructure was built for human consumption — ontologists maintaining graphs for analysts to query. Agents are different: they need machine-readable, structured, typed knowledge to operate accurately. Ontoref's multi-surface model is designed for a world where the primary consumer of project knowledge is not a human reading a wiki but an agent making decisions in a context window.
**Local-first as the default.** The decade-long trend toward cloud-hosted everything is reversing for knowledge infrastructure. The risks are clear: vendor lock-in, platform discontinuity, sovereignty loss, and the compounding cost of external dependencies for what should be internal knowledge. Local-first with optional federation is the architecture that survives platform changes.
## The Moat Argument
Talisman's framing — "your ontology is your moat" — is more accurate than she might intend it to be. A moat works because it's attached to the thing it protects. A moat stored in someone else's database is not a moat. It's a service agreement.
When the knowledge of what your project is, why it exists as it does, and what decisions have lasting consequences lives alongside the code that implements those decisions — versioned, queryable, sovereign, federated — it is genuinely yours. It accumulates. It survives team changes. It informs agents. It can be queried by projects that depend on you.
That's the kind of moat worth building.
---
*Ontoref is open source. The protocol specification, Nushell automation, and Rust crates are at [github.com/jesusperezlorenzo/ontoref](https://github.com/jesusperezlorenzo/ontoref).*

View file

@ -0,0 +1,106 @@
---
# Post metadata
id: "dags-en-todos-lados-ninguno-sabe-lo-que-es"
title: "Los DAGs están en todos lados. Ninguno sabe lo que es."
slug: "dags-en-todos-lados-ninguno-sabe-lo-que-es"
subtitle: "Cada build system, pipeline CI y runbook usa DAGs para ejecución. Ontoref los usa para conocimiento."
excerpt: "Pipelines CI/CD, compiladores, runbooks, orquestadores de datos — todos usan grafos acíclicos dirigidos. Todos sin excepción los usan como modelos de ejecución: esto antes que aquello, ordenación topológica, resolución de dependencias. Ninguno los usa para representar qué es el sistema, por qué existe, o qué trade-offs lo definen. Ese es el gap que llena ontoref."
# Publication info
author: "Jesús Pérez"
date: "2026-05-10"
published: false
featured: false
# Categorization
category: "ontoref"
tags: ["ontoref", "dag", "ontology", "knowledge-graphs", "arquitectura-software"]
# Display
read_time: "6 min read"
sort_order: 2
css_class: "category-ontoref"
category_description: "Ontoref — protocolo y herramientas para el auto-conocimiento estructurado en proyectos de software"
category_published: true
---
# Los DAGs están en todos lados. Ninguno sabe lo que es.
Cada build system usa DAGs. Cada pipeline CI/CD usa DAGs. Compiladores, orquestadores de datos, runbooks, gestores de paquetes, operadores de Kubernetes — todos DAGs. Los grafos acíclicos dirigidos son tan ubicuos en la infraestructura de software que la pregunta "¿por qué DAGs?" ya ni registra como pregunta.
Pero hay una segunda pregunta que nadie hace: *¿qué representan esos DAGs?*
La respuesta es siempre la misma: **orden de ejecución**. Esto antes que aquello. Ordenación topológica. Resolución de dependencias. El grafo describe *cómo* se computa algo, no *qué* es.
```
CI/CD: test → build → deploy
Compilador: parse → typecheck → codegen → link
Runbook: check_health → drain → restart → verify
```
Estos grafos son inertes respecto al sistema sobre el que operan. Un pipeline CI no sabe qué es el proyecto, por qué se construyó así, o qué trade-offs definen su arquitectura. Sabe que los tests deben pasar antes del deploy. Eso es todo lo que sabe.
## El gap semántico
Esta inercia no es un defecto — es apropiada. Los build systems deben ser rápidos y mecánicos. Los runbooks deben ser ejecutables sin requerir conocimiento filosófico sobre el sistema. Los grafos de ejecución y los grafos de conocimiento son herramientas distintas para propósitos distintos.
El problema es que la industria del software ha recurrido a DAGs para *todo excepto la representación de conocimiento*. El conocimiento sobre qué es un sistema — sus principios, sus decisiones arquitectónicas, sus tensiones activas, su superficie de capacidades — vive en documentos. Wikis. READMEs. Tickets. Tradición verbal.
Todo esto es no-estructurado, no-tipado, no-consultable, y garantizado a derivar del sistema real en meses. Describe lo que el proyecto era, no lo que es.
## El doble DAG de ontoref
Ontoref usa DAGs en dos modos fundamentalmente distintos, y la distinción importa:
**DAG ontológico — aristas con tipo semántico:**
```
Practice "ncl-schemas" --implements--> Principle "type-safety"
Practice "ncl-schemas" --enforces--> Constraint "zero-runtime-deps"
Practice "ncl-schemas" --enables--> Capability "agent-queryable-state"
Practice "ncl-schemas" --tension--> Practice "adoption-friction"
```
Estas aristas no son "depende de". Son relaciones tipadas: `implements`, `enforces`, `enables`, `tension`. El grafo es el conocimiento — compromisos formales sobre qué es el proyecto y cómo se relacionan sus conceptos. El equivalente sería un compilador que sabe por qué existe y qué trade-offs arquitectónicos tomó.
**DAG de reflexión — contratos ejecutables con restricción de actor:**
```
step "validate-state" (actor: any) --> step "run-mode" (actor: developer)
step "run-mode" (actor: developer) --> step "report" (actor: agent|developer)
```
Esto no es solo ordenación de ejecución. El DAG se valida como contrato antes de ejecutarse, registra su propio progreso a través de transiciones de estado, y codifica quién puede ejecutar cada paso. Un agente que intenta ejecutar un paso restringido a developer recibe un rechazo tipado, no un error de runtime.
## La conexión faltante
Lo que hace esto no-trivial es que los dos DAGs se comunican:
- El DAG ontológico declara lo que el proyecto ES — constraints activos, prácticas en tensión, dimensiones de estado actuales
- El DAG de reflexión OPERA sobre ese estado — detectando deriva, ejecutando modos, registrando transiciones
- Las migraciones propagan cambios en el DAG ontológico a proyectos consumidores descendentes
En todos los demás sistemas que usan DAGs, el grafo se evalúa y se descarta. En ontoref, la evaluación modifica el estado que la siguiente ejecución lee. El grafo tiene memoria.
## Por qué esto no existía antes
Tres cosas tuvieron que alinearse:
**Los agentes como consumidores.** Antes de la IA agéntica, no había ningún consumidor automatizado que necesitara conocimiento estructurado sobre un proyecto. Los humanos podían leer el wiki. Los agentes no pueden — necesitan conocimiento del proyecto tipado, consultable y machine-readable para trabajar con precisión en lugar de alucinar contexto. El DAG de conocimiento de ontoref es lo que los agentes consumen vía MCP.
**Lenguajes de configuración con contratos.** Expresar una ontología sin un triplestore externo requería un lenguaje de configuración con tipos y contratos. Nickel (NCL) provee exactamente eso: un lenguaje de configuración tipado y lazy donde las violaciones de schema se detectan en tiempo de evaluación, no de runtime. RDF/OWL habría requerido infraestructura dedicada y expertise especializado.
**Escala de repositorio, no de empresa.** Los knowledge graphs enterprise son iniciativas de varios años. Ontoref opera a la escala de un repositorio único — adopción incremental, sin enforcement, sin equipo dedicado. La unidad más pequeña que puede adoptarlo es un proyecto de uno.
## La consecuencia
Cuando un proyecto tiene un DAG de conocimiento que sus agentes pueden consumir, la aritmética de precisión cambia por completo. Los números de la investigación presentada en KGC 2026:
- Retrieval anclado en ontología: **3.4× más preciso** que vector RAG en queries enterprise
- Queries validadas por ontología: la precisión salta del **16% al 72%** en question-answering sobre SQL
La brecha se cierra no con un modelo más grande sino con conocimiento estructurado contra el que el modelo puede razonar. Un DAG donde las aristas significan algo.
---
*Ontoref es open source. La especificación del protocolo, la automatización en Nushell, y los crates de Rust están en [github.com/jesusperezlorenzo/ontoref](https://github.com/jesusperezlorenzo/ontoref).*

View file

@ -0,0 +1,97 @@
---
# Post metadata
id: "la-ia-herramienta-conocimiento-quien-mantiene-vivo"
title: "La IA es una herramienta de conocimiento. ¿Pero quién mantiene vivo el conocimiento?"
slug: "la-ia-herramienta-conocimiento-quien-mantiene-vivo"
subtitle: "Talisman tiene razón — y se detiene exactamente donde empieza el problema difícil"
excerpt: "La charla de Jessica Talisman en KGC 2026 es la articulación más clara de por qué fallan las estrategias de AI: las organizaciones invierten en infraestructura de datos y esperan que emerja el razonamiento. Tiene razón. Pero su solución — construir infraestructura de conocimiento — se detiene exactamente donde empieza el problema difícil: ¿quién evita que el conocimiento derive?"
# Publication info
author: "Jesús Pérez"
date: "2026-05-10"
published: false
featured: false
# Categorization
category: "ontoref"
tags: ["ontoref", "knowledge-graphs", "ontology", "ai-infrastructure", "reflection"]
# Display
read_time: "7 min read"
sort_order: 1
css_class: "category-ontoref"
category_description: "Ontoref — protocolo y herramientas para el auto-conocimiento estructurado en proyectos de software"
category_published: true
---
# La IA es una herramienta de conocimiento. ¿Pero quién mantiene vivo el conocimiento?
Jessica Talisman dio una charla en KGC 2026 llamada "Stop Betting, Start Building." Los datos con los que abre son contundentes:
- **89%** de las empresas no reportan impacto de productividad de la IA después de tres años (NBER, Feb 2026, n=5.937 ejecutivos)
- Los developers experimentados son **19% más lentos** con herramientas de IA, no más rápidos (METR RCT, 2025)
- El trabajador promedio que usa IA gana **14 minutos** netos por semana en productividad (Foxit/Sapio, marzo 2026)
El mercado es alcista. La evidencia, no.
Su diagnóstico es correcto: *la IA es una herramienta de conocimiento, no de datos.* Las organizaciones invierten en data lakes, vector stores y pipelines ETL esperando que el contexto y el razonamiento emerjan de tokens crudos. No va a ocurrir. Los modelos fueron entrenados sobre linked data, triples RDF y vocabularios controlados — las quince principales fuentes de entrenamiento de C4 son intensivas en knowledge graphs. Luego se despliegan en entornos despojados de toda esa estructura, y nos preguntamos por qué alucinan.
Su prescripción también es correcta: construye infraestructura de conocimiento. Primero vocabularios controlados. Taxonomías. Tesauros. Ontologías — compromisos formales, clases, propiedades, constraints. Knowledge graphs encima. Y goviernalos. Para siempre, no como un proyecto.
Tiene razón. Y se detiene exactamente donde empieza el problema difícil.
## El gap de governance
El sexto paso en su stack es "Govern — esto es infraestructura, stewardship, versionado, crecimiento, para siempre." Nombra el problema. Pero nombrarlo no es un mecanismo.
La pregunta real es: *¿quién asegura que la ontología no derive respecto al sistema que describe?*
El software evoluciona. Una decisión arquitectónica tomada en marzo invalida un nodo del grafo en octubre. Se incorpora un nuevo equipo con modelos mentales distintos. Una librería se reemplaza y un constraint se convierte en ficción. Los knowledge graphs acumulan deuda igual que los codebases — silenciosamente, sin alarmas.
En el mundo enterprise de knowledge graphs, la respuesta al governance es headcount: contratar ontólogos, bibliotecarios, knowledge engineers. Funciona a escala organizacional con presupuestos dedicados. No es una respuesta viable para un proyecto de software, un entorno de infraestructura, o un individuo que intenta mantener auto-conocimiento estructurado.
## Lo que realmente falta: el loop operacional
Toda ontología seria sin un cierre operacional se convierte en arqueología en dieciocho meses. Hermosa, formalmente correcta, y describiendo un sistema que ya no existe.
La pieza que falta no es más conocimiento. Es un loop de retroalimentación que:
1. **Observa** el estado actual del sistema contra su intención declarada
2. **Detecta** la deriva antes de que se vuelva permanente
3. **Ejecuta** operaciones que reducen esa deriva
4. **Registra** decisiones con peso arquitectónico duradero
5. **Propaga** los cambios a todo lo que depende de este conocimiento
Este es el Yang al Yin de la ontología. Talisman describe el Yin con precisión. El Yang es lo que evita que sea un artefacto.
## Ontoref: las dos mitades
Ontoref es un protocolo para el auto-conocimiento estructurado en proyectos de software. Opera como dos capas coexistentes que no pueden funcionar la una sin la otra:
**Ontología (lo que ES):** nodos y aristas tipadas que representan prácticas, principios, tensiones, capacidades. No documentación — compromisos formales. Un nodo `Practice` que `implements` un `Principle`, `enforces` un `Constraint`, `enables` una `Capability`, y está en `tension` activa con otra `Practice`. El proyecto como knowledge graph.
**Reflection (lo que DEVIENE):** DAGs ejecutables — modos operacionales que corren contra el estado ontológico, detectan deriva, registran transiciones y propagan cambios. El comando `sync diff --docs` que detecta cuándo la documentación de un crate ha derivado de su nodo ontológico. El FSM en `state.ncl` que registra dónde está cada dimensión del proyecto versus adónde pretende ir. El sistema de migraciones que asegura que los cambios de protocolo lleguen a cada proyecto consumidor.
La tensión entre estas dos capas no es un defecto de diseño — está nombrada explícitamente en la ontología del propio proyecto como su identidad central:
> *"Ontology captures what IS. Reflection captures what BECOMES. Both must coexist without one dominating."*
Sin Reflection, la Ontología cristaliza. Se convierte en una fotografía de lo que el proyecto quería ser en el momento en que se escribió. Sin Ontología, Reflection es ejecución sin verdad — sabe cómo operar pero no sobre qué opera.
## Los números de precisión
Los datos de Talisman sobre lo que el conocimiento estructurado hace a la precisión de la IA merecen repetirse:
- Question-answering sobre SQL enterprise: **16% → 72%** cuando una ontología verifica y repara queries generadas por LLM (Allemang & Sequeda, data.world AI Lab, 2024)
- GraphRAG vs. vector RAG: **3.4× precisión** en 43 queries enterprise (Diffbot KG-LM Benchmark, 2023)
- Vector RAG colapsa a **0% precisión más allá de cinco entidades por query**. El retrieval anclado en KG sostiene el rendimiento mucho más allá
La brecha de precisión no se cierra con un modelo más grande. Se cierra con un schema definido, una ontología, y una query validada.
Pero solo si la ontología se mantiene viva. Solo si algo o alguien ejecuta el loop operacional que evita que derive hacia la ficción.
Esa es la parte que el framework de Talisman no provee. Para eso existe Reflection.
---
*Ontoref es open source. La especificación del protocolo, la automatización en Nushell, y los crates de Rust están en [github.com/jesusperezlorenzo/ontoref](https://github.com/jesusperezlorenzo/ontoref).*

View file

@ -0,0 +1,106 @@
---
# Post metadata
id: "tu-ontologia-deberia-vivir-con-tu-codigo"
title: "Tu ontología debería vivir con tu código"
slug: "tu-ontologia-deberia-vivir-con-tu-codigo"
subtitle: "Soberanía, federación, y por qué la infraestructura de conocimiento sin hogar no es infraestructura"
excerpt: "Los knowledge graphs enterprise viven en un triplestore mantenido por un equipo dedicado en la plataforma de un vendor. Cuando el equipo cambia, el presupuesto se recorta, o el vendor pivota, el conocimiento desaparece. Ontoref toma el enfoque opuesto: conocimiento soberano y local-first que vive junto a su sujeto, versionado, consultable a través de cuatro superficies, y federado sin un broker central."
# Publication info
author: "Jesús Pérez"
date: "2026-05-10"
published: false
featured: false
# Categorization
category: "ontoref"
tags: ["ontoref", "soberania", "federacion", "radicle", "jujutsu", "infraestructura-conocimiento"]
# Display
read_time: "6 min read"
sort_order: 4
css_class: "category-ontoref"
category_description: "Ontoref — protocolo y herramientas para el auto-conocimiento estructurado en proyectos de software"
category_published: true
---
# Tu ontología debería vivir con tu código
Jessica Talisman, en su charla de KGC 2026, dice algo que merece destacarse: *"Tu ontología es tu moat — tu IP."*
Tiene razón. Y está describiendo el modelo enterprise de knowledge graphs, donde el moat está retenido en el triplestore de un vendor, mantenido por un equipo dedicado, en una plataforma con suscripción. Cuando el equipo cambia, el presupuesto se recorta, o el vendor pivota, el moat se drena.
Ontoref toma el enfoque opuesto.
## Qué significa conocimiento soberano
En ontoref, el conocimiento de qué es un proyecto — sus principios, prácticas, tensiones, decisiones arquitectónicas, estado operacional — vive como archivos NCL junto al código. En el repositorio. Versionado con la misma disciplina que el source.
Esto no es un enfoque de documentación. Es una decisión de almacenamiento con consecuencias arquitectónicas:
- **Ningún vendor puede quitártelo.** La ontología está en tu repo, no en su base de datos.
- **Sin migración de plataforma.** GitHub, Gitea, Radicle — el conocimiento se mueve con el código.
- **Sin infraestructura dedicada.** El daemon corre localmente; los archivos son la fuente de verdad.
- **Offline por defecto.** El conocimiento que requiere conexión de red para existir no es infraestructura — es un servicio.
La analogía más cercana es SQLite vs. PostgreSQL: local-first, embebido, siempre disponible, sin conexión requerida. Cuando necesitas distribución, la añades. Pero el baseline es soberanía.
## Cuatro superficies, una fuente
Almacenamiento soberano no significa aislado. Ontoref expone el mismo conocimiento ontológico a través de cuatro superficies simultáneamente:
**CLI (Nushell):** La interfaz nativa del developer. `ontoref describe project`, `ontoref graph ontology`, `ontoref sync diff --docs`. Rápido, scriptable, composable en CI. La superficie para humanos que viven en la terminal y para checks automatizados que corren en pipelines.
**UI (axum):** Una interfaz web para inspección visual, onboarding y gestión. No un dashboard sobre una API remota — un servidor local que renderiza el mismo conocimiento respaldado por NCL. La superficie para project managers, nuevos contribuidores, o cualquiera que prefiera navegar un grafo visualmente.
**MCP:** La superficie de agentes. El endpoint MCP de ontoref es la capa semántica que se sienta encima del protocolo de transporte. Como identifica correctamente el framework de Talisman, MCP mueve bytes — no establece significado compartido. Ontoref provee ese significado a través de su capa ontológica, expuesta vía MCP a agentes que necesitan conocimiento estructurado del proyecto para trabajar con precisión en lugar de alucinar contexto.
**GraphQL:** La superficie de integración. SPARQL es el estándar del semantic web, pero GraphQL es lo que la mayoría de developers conocen y cada ecosistema de herramientas soporta. Una API GraphQL sobre el DAG ontológico elimina la barrera de expertise sin sacrificar estructura.
Cuatro superficies, una fuente canónica: los archivos NCL en el repositorio.
## jj y Radicle
El modelo de almacenamiento se combina naturalmente con una clase específica de herramientas VCS.
**Jujutsu (jj):** Un VCS compatible con Git donde el working copy es siempre un commit — no hay dirty state. Esto se alinea directamente con el enfoque de ontoref de captura continua de estado: cada estado del proyecto es capturable, cada transición es un evento de primera clase. El operation log de jj es el equivalente VCS del historial de sesiones de reflexión de ontoref.
**Radicle:** Colaboración de código P2P, local-first, soberana. Si el código es soberano, el conocimiento sobre ese código también debería serlo. Radicle como transporte significa que la federación de proyectos con ontoref no depende de ninguna plataforma centralizada — sin GitHub, sin GitLab, sin Gitea hosteado. Los nodos se descubren entre sí e intercambian conocimiento directamente.
Juntos: código soberano y conocimiento soberano, distribuidos sin un broker central.
## Federación sin broker central
El modelo enterprise para federación de conocimiento es un knowledge graph central al que todos los proyectos apuntan. Un equipo lo mantiene. Una plataforma lo aloja. Cada query lo atraviesa.
El modelo de federación de ontoref es distinto:
```
proyecto-A consulta: ¿qué proyectos implementan "zero-trust-auth"?
proyecto-B, proyecto-C responden desde sus propias ontologías
sin servidor central que lo sepa todo
```
Esto es posible porque cada proyecto se describe a sí mismo. El DAG ontológico en `.ontology/core.ncl` es consultable sin ninguna dependencia externa. La federación vía NATS conecta los daemons — el daemon de cada proyecto se registra, publica sus capacidades, y responde a queries de otros daemons en la federación.
El resultado: visibilidad a nivel de ecosistema sin centralización a nivel de ecosistema. Puedes descubrir qué proyectos comparten tus constraints arquitectónicos, cuáles implementan prácticas compatibles, cuáles están en estados conflictivos — sin que ninguno requiera una plataforma compartida.
## Por qué esta arquitectura ahora
Dos cambios hicieron esto viable:
**La IA agéntica como consumidor.** Antes de los agentes, la infraestructura de conocimiento se construía para consumo humano — ontólogos manteniendo grafos para que los analistas los consultaran. Los agentes son diferentes: necesitan conocimiento machine-readable, estructurado y tipado para operar con precisión. El modelo multi-superficie de ontoref está diseñado para un mundo donde el consumidor principal del conocimiento del proyecto no es un humano leyendo un wiki sino un agente tomando decisiones en una ventana de contexto.
**Local-first como el default.** La tendencia de una década hacia todo en la nube está revirtiendo para la infraestructura de conocimiento. Los riesgos son claros: vendor lock-in, discontinuidad de plataforma, pérdida de soberanía, y el coste compuesto de dependencias externas para lo que debería ser conocimiento interno. Local-first con federación opcional es la arquitectura que sobrevive a los cambios de plataforma.
## El argumento del moat
El framing de Talisman — "tu ontología es tu moat" — es más acertado de lo que ella puede pretender. Un moat funciona porque está unido a lo que protege. Un moat almacenado en la base de datos de otra persona no es un moat. Es un acuerdo de servicio.
Cuando el conocimiento de qué es tu proyecto, por qué existe como existe, y qué decisiones tienen consecuencias duraderas vive junto al código que implementa esas decisiones — versionado, consultable, soberano, federado — es genuinamente tuyo. Se acumula. Sobrevive a cambios de equipo. Informa a agentes. Puede ser consultado por proyectos que dependen de ti.
Ese es el tipo de moat que vale la pena construir.
---
*Ontoref es open source. La especificación del protocolo, la automatización en Nushell, y los crates de Rust están en [github.com/jesusperezlorenzo/ontoref](https://github.com/jesusperezlorenzo/ontoref).*

View file

@ -0,0 +1,122 @@
---
# Post metadata
id: "un-protocolo-tres-sujetos"
title: "Un protocolo, tres sujetos"
slug: "un-protocolo-tres-sujetos"
subtitle: "La misma pregunta aplica a tu proyecto, tu infraestructura, y a ti mismo"
excerpt: "Ontoref comenzó como un protocolo para el auto-conocimiento de proyectos de software. La misma arquitectura — DAG ontológico para lo que ES, DAG de reflexión para lo que DEVIENE — aplica sin modificación a entornos de infraestructura y a individuos. El sujeto cambia. La pregunta es idéntica: ¿qué eres, qué tensiones te definen, dónde estás versus dónde pretendes estar?"
# Publication info
author: "Jesús Pérez"
date: "2026-05-10"
published: false
featured: false
# Categorization
category: "ontoref"
tags: ["ontoref", "ontology", "infraestructura", "conocimiento-personal", "reflection"]
# Display
read_time: "7 min read"
sort_order: 3
css_class: "category-ontoref"
category_description: "Ontoref — protocolo y herramientas para el auto-conocimiento estructurado en proyectos de software"
category_published: true
---
# Un protocolo, tres sujetos
La pregunta que ontoref hace a un proyecto de software es precisa: *¿Qué eres? ¿Qué principios te definen? ¿Qué tensiones estás gestionando activamente? ¿Qué decisiones has tomado con consecuencias duraderas? ¿Dónde estás versus dónde pretendes estar?*
Resulta que es la misma pregunta que los entornos de infraestructura necesitan responder. Y los individuos.
El sujeto cambia. La pregunta es idéntica.
## Lo que los tres tienen en común
Cualquier sistema con identidad e intención comparte el mismo problema estructural:
- **Identidad** — qué es, qué principios lo definen
- **Tensiones activas** — trade-offs que no se resuelven, se gestionan
- **Decisiones duraderas** — elecciones con consecuencias que restringen el futuro
- **Estado observable** — dónde está versus dónde pretende estar
- **Deriva** — la distancia entre intención y realidad, creciendo silenciosamente con el tiempo
- **Operaciones** — las acciones que reducen esa deriva
Los proyectos de software tienen todo esto. Los entornos de infraestructura también. Las personas también.
Y todos se benefician de la misma arquitectura: una capa ontológica que captura lo que ES, y una capa de reflexión que captura lo que DEVIENE y opera para mantener ambas alineadas.
## Infraestructura
La infraestructura ya usa DAGs — Terraform, Pulumi, Ansible. Todos describen *cómo* se provisiona el sistema. Ninguno captura *por qué existe como existe*.
El "por qué" es donde vive el conocimiento:
```
Servicio "auth" --tension--> Principio "stateless"
razón: necesita session state, pero el principio lo prohíbe
Constraint "zero-downtime" --enforces--> Práctica "blue-green"
razón: ADR-infra-003, después del incidente de deploy de 2025
```
Esta información no está en el plan de Terraform. Está en la memoria del engineer que estuvo allí, en un hilo de Slack de hace ocho meses, o en ningún lado. Cuando ese engineer se va, la infraestructura pierde su propia historia — sabe lo que es, no por qué.
El ontoref de infraestructura captura las decisiones arquitectónicas de la propia infraestructura, con el mismo formalismo de ADR que los proyectos de software. Registra dimensiones de estado — qué entornos están estables, cuáles en migración activa, qué está bloqueado y por qué. El FSM no registra el estado de deploy; registra el *estado epistémico* — qué tan bien se entiende a sí misma la infraestructura.
La federación vía NATS significa que cada entorno (dev, staging, prod) se describe a sí mismo y puede ser consultado desde los proyectos que dependen de él. GitOps soberano: el conocimiento de infraestructura vive con el código de infraestructura, no en la base de datos de un vendor.
## Personal
Esta es la aplicación más radical. Pero se sigue directamente de la misma lógica.
Una persona con valores, roles e intenciones a largo plazo enfrenta exactamente el mismo problema estructural que un proyecto de software:
```
Valor "profundidad" --tension--> Rol "padre"
razón: el tiempo es finito y los dos lo demandan completamente
Decisión "dejé empresa X" --constraint--> Elecciones de carrera futuras
con el mismo peso arquitectónico que un ADR-001
Estado "aprendiendo Rust" --blocker--> "contribuir a ontoref-core"
una dimensión FSM personal, no un ítem de lista de tareas
```
El knowledge graph aquí no es un sistema de productividad. No es un segundo cerebro de notas. Es un grafo de *compromisos* — tipados, provenados, con tensiones activas nombradas en lugar de suprimidas.
El problema de deriva es idéntico: quién estás llegando a ser versus quién pretendías ser. Los modos de reflexión para una persona pueden ser revisiones semanales, evaluaciones anuales de estado, o revisiones disparadas cuando una decisión mayor introduce nuevos constraints. El loop operacional es el mismo — observa, detecta deriva, ejecuta, registra.
## El mismo stack, sujeto distinto
Los tres dominios corren en la misma infraestructura:
**Almacenamiento soberano:** El conocimiento vive junto a su sujeto — en el repo del proyecto, en el código de infraestructura, en una bóveda personal. Ningún vendor lo retiene. Sin dependencia de plataforma. Archivos NCL versionados con la misma disciplina que el código.
**Acceso multi-superficie:**
| Superficie | Consumidor |
|---|---|
| CLI (Nushell) | Developer, scripts, CI |
| UI (axum) | Revisión visual, onboarding |
| MCP | Agentes AI |
| GraphQL | Herramientas externas, dashboards |
**Alineación VCS:** El modelo de jj — donde el working copy es siempre un commit — se alinea naturalmente con el enfoque de ontoref de captura continua de estado. No hay "dirty state"; cada momento es capturable. Radicle como transporte hace el stack soberano y P2P — colaboración de código sin plataforma central.
**Federación:** Proyectos consultando los knowledge graphs de otros sin un broker central. Entornos de infraestructura exponiendo su auto-conocimiento a los proyectos que dependen de ellos. Individuos cuyo knowledge graph personal informa sus contribuciones a los proyectos en los que trabajan.
## Por qué la escala importa
La unidad más pequeña que puede adoptar ontoref es una persona. Un developer, un proyecto, un entorno de infraestructura. Sin equipo requerido. Sin conversación de presupuesto.
La unidad más grande es un ecosistema de proyectos federados y auto-descritos que pueden consultarse mutuamente sin que ninguno sea la fuente central autoritativa.
Ese rango — de individuo a ecosistema — es posible porque el protocolo es el mismo a cada escala. El DAG ontológico y el DAG de reflexión no cambian de forma según el tamaño del sujeto. Cambian en densidad y profundidad, no en estructura.
Las organizaciones que Talisman argumenta deberían invertir en infraestructura de conocimiento intentan construirla top-down, con equipos dedicados y programas de varios años. Ontoref lo hace disponible bottom-up — empezando con un solo proyecto, un solo entorno, una sola persona, cada uno describiéndose a sí mismo en el mismo lenguaje.
---
*Ontoref es open source. La especificación del protocolo, la automatización en Nushell, y los crates de Rust están en [github.com/jesusperezlorenzo/ontoref](https://github.com/jesusperezlorenzo/ontoref).*

View file

@ -0,0 +1,101 @@
# A Two-Week Sprint for Knowledge
## From Agile to Design Thinking
**Jessica Talisman, MLS**
*Apr 21, 2026*
---
In 1986, Hirotaka Takeuchi and Ikujiro Nonaka published "The New New Product Development Game" in the *Harvard Business Review*, describing how Honda, Canon, and Fuji-Xerox organized teams to build copiers and automobiles. Jeff Sutherland read that article, combined it with lean manufacturing principles and object oriented programming, and in 1993 adapted it for software development at Easel Corporation. Ken Schwaber formalized the method at the OOPSLA conference in 1995. By 2001, seventeen software developers gathered at a ski lodge in Snowbird, Utah, and signed the Agile Manifesto, declaring that they were "uncovering better ways of developing software." Not knowledge or meaning. A manufacturing framework for software.
The Agile Manifesto's own language is explicit about its scope, yet the industry has applied Agile methodology to virtually everything in a technological environment — including knowledge work, semantic systems and now artificial intelligence. And within the same breath, organizations and experts regale the industry with talk of innovation and the emergence of a new paradigm in ways of working. Yet those ways of working are locked inside of Agile methodologies and Scrum, designed for the factory floor and the codebase — not for the messy, recursive, deeply human work of making meaning out of complexity.
## Scrum Is a Manufacturing Schedule
Let's return to the Takeuchi and Nonaka paper, where the title — "The New New Product Development Game" — describes how product teams at Japanese firms developed physical goods such as the FX-3500 copier and the Honda City car. They draw on the rugby metaphor because the team "tries to go the distance as a unit, passing the ball back and forth." The unit of delivery is a working prototype of a material artifact. The feedback loop is a machine that works or fails.
Sutherland's 2014 book, *Scrum: The Art of Doing Twice the Work in Half the Time*, reinforces this with military and manufacturing case studies such as the F-86 fighter production and Toyota. Sutherland's book rebuilds the argument with the same assumption — that the thing being built has discrete, inspectable, testable parts that, when finished, are finished.
The 2020 Scrum Guide defines the Increment as "a concrete stepping stone toward the Product Goal," one that is "additive to all prior Increments and thoroughly verified, ensuring that all Increments work together." These words are sensible if you are building a mobile application, with a login screen and a settings page. But for knowledge systems, these words are destructive, because the components of a semantic architecture are not additive in the way Scrum dictates. A controlled vocabulary is not a subcomponent of a taxonomy the way a login screen is a subcomponent of an app. A taxonomy without disciplined, vocabulary control as its underpinnings does not function well, if at all. You cannot ship half a taxonomy and call it an Increment.
You see, Scrum assumes tame problems that can be time-boxed, estimated with planning poker, decomposed into user stories, tracked on a velocity chart and declared done against a checklist. If only knowledge were this tame. Knowledge organization is anything but tame. More than fifty years old, in 1973 Horst Rittel and Melvin Webber warned that applying the engineering paradigm uniformly to all problems was "bound to fail." And yet here we are, applying a manufacturing schedule and methodologies to knowledge, expecting that knowledge will fit into the confines of old ways of working — an artifact of the Third Industrial Revolution imposed on the demands of the Fourth.
In 2016, Klaus Schwab wrote about this historical pattern. The First Industrial Revolution, beginning around 1760, gave us steam and mechanical production. The Second, in the late nineteenth century, gave us electricity and the assembly line — the world that produced Toyota, lean manufacturing, and the production logic that Scrum still carries as its core ethos. The Third, beginning in the 1960s, gave us semiconductors, mainframe computing, and eventually the internet — the world in which Agile was born, by software developers, for software development. The Fourth Industrial Revolution, Schwab argued, is fundamentally different, in kind, as it is defined by the fusion of artificial intelligence, cyber-physical systems and knowledge technologies that blur the boundaries between the physical, digital and biological worlds.
The problems of this era are not necessarily engineering problems with stable requirements. We are wrestling with problems such as how to ground machine intelligence in meaning, how to organize evolving knowledge and how to build systems that can explain themselves. These are wicked problems that demand new epistemologies and frameworks for problem solving. Agile may have been the right methodology for the Third Industrial Revolution but we are no longer in the Third Industrial Revolution. The frameworks have not caught up, and the cost of that lag is exacerbated every time an organization forces Third Industrial Revolution principles applied to Fourth Industrial Revolution work.
In other words, this is not a product problem or a platform problem — it is learning to embrace new ways of working. This is an infrastructure problem that is governed by epistemology — a fundamental philosophy pertaining to knowledge.
## Why Knowledge Cannot Be Sprinted
An ontology is, per Thomas Gruber's foundational definition, "a specification of a conceptualization." A conceptualization is a whole, not parts of a whole. Specifying half of it produces logical incoherence, not a minimally viable product. For example, ANSI/NISO Z39.19, the standard for constructing controlled vocabularies, requires resolving synonymy, homonymy and scope before terms enter production, because any inconsistency propagates downstream into every subsequent use. SKOS, the W3C standard for encoding knowledge organization systems, requires that hierarchical relationships form coherent structures with transitive closure and OWL 2 requires that class axioms be satisfiable. These are preconditions, necessary for logical systems to reason and express meaning.
In practice, I have watched organizations attempt Agile delivery of knowledge systems in two patterns and neither ends well. In the first, the team holds stand-ups, ceremonies and retrospectives. The taxonomist brings tickets and adds fifteen terms to the product vocabulary. Tickets close and a velocity chart goes up. Nothing is integrated, because integration is the hard part and integration does not fit into two weeks so that goes to the backlog. Six months later, the vocabulary is a collection of inconsistently scoped terms, the ontology has orphan classes and the knowledge graph — if it exists — cannot reliably answer queries. But the burndown chart is immaculate and leadership is blindly satisfied because boxes were checked.
In the second pattern, the team borrows from other methodologies while still calling it Agile. The taxonomist conducts multi-week domain analysis and stakeholder interviews — that is ethnography. She does card sorting and concept modeling — that is design research. She iterates with subject matter experts — that is participatory design. She ships, eventually, a coherent model. The sprint cadence was irrelevant to the work. Martin Fowler named this phenomenon in his 2018 keynote: "faux-agile" — the Agile Industrial Complex.
Ron Jeffries, another original signatory of the Agile Manifesto, told developers the same year, 2018, to abandon the word entirely. Dave Thomas declared in 2014 that "Agile" had become "a magnet for anyone with points to espouse, hours to bill or products to sell." When three of the seventeen authors of a framework say it has been hollowed out, one has to wonder if Agile is appropriate for all technology work and if it is time to revisit ways of working.
## Innovation Does Not Fit in a Backlog
The deeper issue is that Agile and Scrum were never designed to inspire or foster innovation. They were designed to deliver known requirements with increasing efficiency. The Scrum Guide's entire apparatus — product backlog, sprint planning, Definition of Done — presupposes that the problem has been formulated by a product owner who can write it in a user story. But knowledge work, like design, is wicked. Rittel and Webber's first property of a wicked problem is that "There is no definitive formulation." The second property states that there is no stopping rule. And the fifth property illuminates that every solution is a "one-shot operation" because there is no opportunity to learn by trial-and-error without consequence.
Given the nature of wicked problems, it would seem that Agile is ill fitted for wicked problems. And for that matter, ill fitted for the dynamic nature of knowledge, where knowledge infrastructures are never one-shot operations, necessitate stopping rules and without a doubt defy definitive formulations.
Therefore, knowledge organization in an enterprise domain is wicked in exactly this sense. The problem of how to represent "customer" in a telecommunications company's semantic model has no pre-existing correct answer. Its formulation depends on who is doing what with the representation, and that changes as you build. The scope of the vocabulary shifts as you interview stakeholders. The taxonomy reorganizes as you encounter edge cases. The ontology's axioms tighten or loosen as the reasoner surfaces contradictions. Don't be mistaken — this does not bend to the Agile principle of welcoming changing requirements, as the requirements are constituted *through* the work. The problem is under construction, often evolving, during the solving.
Organizations that manage all work through Scrum are, in effect, optimizing for predictability in domains that demand exploration. The results resist innovation and take on the appearance of productivity. Velocity charts rise while the underlying intellectual architecture is flattened into bite-sized deployments, uncoupled from the continuum that is knowledge.
## Design Thinking Belongs in the Swamp
Donald Schön drew the distinction that "In the varied topography of professional practice, there is a high, hard ground where practitioners can make effective use of research-based theory and technique, and there is a swampy lowland where situations are confusing 'messes' incapable of technical solution." Agile lives on the high ground while knowledge work lives in the swamp. Schön's term for how competent practitioners navigate the swamp was "reflection-in-action" — the iterative, tacit recalibration that happens while the work is happening.
Design thinking, in the lineage of Herbert Simon, Richard Buchanan and Schön, is the methodology of the swamp. It assumes the problem is under construction. Its five phases — empathize, define, ideate, prototype, test — are not a linear pipeline but a recursive, iterative process that expects the definition of the problem to change as understanding deepens.
Research in the *Handbook of Human-Centered Artificial Intelligence* confirms this alignment — integrating design thinking into AI development makes systems more user-focused, iterative and impactful, emphasizing data governance, transparency, explainability and bias mitigation. Carnegie Mellon University's Tepper School of Business now teaches a dedicated program on design thinking with AI, arguing that the combination addresses "complex, systemic problems" that linear frameworks cannot manage successfully. IBM's enterprise design thinking practice, developed over two decades, has been applied across client engagements precisely because it keeps human experience at the center while technology evolves underneath — design thinking is "timeless," while technology is "timestamped."
A 2026 study in *Scientific Reports* examined the relationship between higher order thinking, generative AI chatbot use and engineering creativity, finding that higher order thinking had significantly greater importance for creative outcomes than AI tool use alone — reinforcing that cognitive depth, not tool velocity, drives innovation. Research in the *Proceedings of the Design Society* further argues that generative AI is shifting the designer's role from executor to paradigm-setter, making design thinking more essential as AI handles production tasks while humans must guide framing, ethics and meaning.
To be clear, these frameworks are not different flavors of the same methodology. Scrum and design thinking represent totally different epistemologies. Scrum assumes the problem was handed to you while design thinking assumes the problem must be found and wrestled with.
## Is AI a Knowledge Tool?
This brings us to the question the industry avoids and I, for one, have been itching to ask. Is AI a knowledge tool? How do you classify it?
A large language model generates plausible sequences of text. Returning to the Bender, Gebru, McMillan-Major, and Mitchell paper mentioned earlier, the authors defined a language model as a system for stitching together linguistic forms according to probabilistic patterns, "without any reference to meaning." Without reference to meaning, as LLMs out-of-the-box are absent of external grounding. They optimize for plausibility, not truth and therefore as a standalone, can be best categorized as an information-processing tool.
It becomes a knowledge tool only when grounded in a semantic layer — controlled vocabularies, taxonomies, ontologies, knowledge graphs — that provides reference and provenance. The industry's own corrective measures and associated marketplace confirms this as fact. Retrieval-augmented generation, knowledge graphs for LLMs and what recent discourse calls "context engineering" are, underneath the packaging, the semantic infrastructure that should have been built first. But it was bolted on after the fact to prevent hallucination that was pretty much guaranteed the moment the model was deployed without one.
The RAG paper from 2020 called out this problem, identifying that parametric memory is opaque and cannot be updated and that retrieval from structured sources is how provenance is captured and encoded. But it seems the industry has a memory problem or it was too early for most to hear the message.
Ironically, provenance is a core element of knowledge management and ontology work, as provenance is a principled tenant of the discipline and problem space. Without provenance, knowledge cannot exist. Organizations are only now discovering they need it, because their generative systems cannot explain themselves. Call it lineage, call it traces. But if you are building for knowledge, call it provenance, as provenance includes traces, lineage, citations, the temporal and the spatial.
So if generative AI is an information processing tool, I posture that for AI to become a knowledge tool, there must be provenance and descriptive logic. And to be frank, nobody sprints their way into provenance — it is architected and sustained.
## Knowledge Is a Spiral, Not a Sprint
The irony does not escape the moment. The same Nonaka who co-authored the 1986 paper that inspired Scrum went on to develop the SECI model of knowledge creation in 1995, describing knowledge as a spiral between tacit and explicit forms, moving through socialization, externalization, combination and internalization.
The spiral does not have an end, as is true with spirals, emblematic of life. Each rotation creates new knowledge that becomes the substrate for the next rotation. There is no Definition of Done because the definition of what is being done is itself being constructed. Scrum took the team structure from Nonaka's first paper and discarded his next decade of work because it did not fit neatly with Scrum. The mechanisms of knowledge work, as Nonaka spent a decade arguing, are metaphor, tacit skill, apprenticeship, shared space and the patient conversion of hunches into models and models back into practice.
The Ontology Pipeline™ mirrors this spiral. You start with a controlled vocabulary — a disciplined process involving agreements as to what terms mean. You mature it into a taxonomy by introducing hierarchy, structuring terms into parent-child relations. Thereafter, the taxonomy is matured into a thesaurus by adding associative relationships and synonymy, encoded in SKOS. The thesaurus is baseline work from which an ontology is developed, adding formal semantics using OWL and RDF. You architect a knowledge graph, assembling components to include the knowledge assets developed through the Ontology Pipeline™ iterative framework.
Each stage prepares the ground for the next and stages cannot be skipped or bypassed. You cannot ship the knowledge graph without an ontology, and the ontology must be logically consistent and coherent to make sense of data. A knowledge graph without vocabulary control risks becoming a confident liar or failing to work with natural language. Because knowledge assets are dimensionally interconnected, knowledge is indeed a spiral, unable to be contained by manufacturing logic.
## The Pattern and the Cure
Organizations budget for platforms — Confluence, SharePoint, LLM subscriptions, vector databases, a RAG vendor. But they do not budget for descriptive logic and knowledge, perhaps because the problem is not solved by a single tool and is seemingly too amorphous in an Agile, data-centric world. The controlled vocabulary is cut unless it delivers value for the database. Often, the taxonomy is cut because it does not demo well and its value sits in the plumbing, delivering its punch over time. The result is an ever-growing pile of documents with a chatbot stapled to the front, confidently answering questions in the voice of a system that has no reference for any of the terms sprinkled throughout the documents.
Then the system hallucinates. It contradicts itself across sessions. It surfaces outdated policy as current and surfaces private Outlook messages from deactivated employees. I have seen this in real life more times than I can count. And the optics are worse when a vendor tool is responsible for revealing private chats and email transactions. Usually the executive sponsor asks why the investment has not worked, ignoring the evidence of faux pas such as the surfacing of private communications. All in a day's work, when burndown charts and velocity set the precedent for ways of working and reward systems.
Executives ignore the fact that the system was never grounded. It was never grounded because grounding requires the slow, expensive, disciplined work of knowledge organization — work that does not fit into a sprint, does not demo well and fails to produce a velocity chart. The organization chooses to bypass investment in the descriptive knowledge apparatus because the lines are too blurry to clearly define according to Agile speak. Because knowledge is infrastructure, not a product or a platform. Knowledge is the gas in the tank, the fuel for meaning, the material that justifies logical reasoning.
Design thinking gives that knowledge work a home. It is comfortable with ambiguity. It treats problem discovery as an equal to problem solving. It certainly does not demand a shippable increment every fourteen days. Instead, design thinking demands that you understand what you are building before you build it — and that you keep revising that understanding as the work proceeds. Library and information science, with over a century of standards for knowledge organization, provides the methods while design thinking provides the epistemology. Together, they offer what Agile never could — a framework suited to the nature of knowledge itself.
A two-week sprint to deliver what? If you are building knowledge systems, two weeks will get you nothing coherent. Knowledge is not built fast and broken into shape. It is built carefully, with provenance, governance and patience. Knowledge is not a product to be manufactured and no — you cannot buy off-the-shelf knowledge that perfectly matches a domain or organization. Knowledge is nuanced, specific to each organization, domain and person.
In the spirit of VC-speak, knowledge is the moat, and that moat is not Agile.
---
*Jessica Talisman, MLS — Intentional Arrangement*

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,160 @@
#set document(
title: "A Two-Week Sprint for Knowledge",
author: "Jessica Talisman, MLS",
)
#set page(
paper: "a4",
margin: (top: 2.8cm, bottom: 3cm, left: 3cm, right: 3cm),
footer: context [
#set text(size: 9pt, fill: luma(140))
#h(1fr)
#counter(page).display("1")
],
)
#set text(
font: ("Georgia", "Linux Libertine"),
size: 11pt,
lang: "en",
hyphenate: true,
)
#set par(
justify: true,
leading: 0.75em,
spacing: 1.2em,
)
#set heading(numbering: none)
#show heading.where(level: 1): it => [
#v(0.6em)
#block(
text(size: 13.5pt, weight: "semibold", it.body)
)
#v(0.3em)
]
#show heading.where(level: 2): it => [
#v(1.2em)
#block(
text(size: 12pt, weight: "semibold", it.body)
)
#v(0.2em)
]
// ── Title block ──────────────────────────────────────────────────────────────
#align(center)[
#v(1.6em)
#text(size: 22pt, weight: "bold")[A Two-Week Sprint for Knowledge]
#v(0.5em)
#text(size: 14pt, style: "italic", fill: luma(80))[From Agile to Design Thinking]
#v(1em)
#line(length: 60%, stroke: 0.5pt + luma(180))
#v(0.8em)
#text(size: 11pt)[*Jessica Talisman, MLS*]
#v(0.2em)
#text(size: 10pt, fill: luma(120))[Apr 21, 2026 · Intentional Arrangement]
#v(2em)
]
// ── Body ─────────────────────────────────────────────────────────────────────
In 1986, Hirotaka Takeuchi and Ikujiro Nonaka published "The New New Product Development Game" in the _Harvard Business Review_, describing how Honda, Canon, and Fuji-Xerox organized teams to build copiers and automobiles. Jeff Sutherland read that article, combined it with lean manufacturing principles and object oriented programming, and in 1993 adapted it for software development at Easel Corporation. Ken Schwaber formalized the method at the OOPSLA conference in 1995. By 2001, seventeen software developers gathered at a ski lodge in Snowbird, Utah, and signed the Agile Manifesto, declaring that they were "uncovering better ways of developing software." Not knowledge or meaning. A manufacturing framework for software.
The Agile Manifesto's own language is explicit about its scope, yet the industry has applied Agile methodology to virtually everything in a technological environment --- including knowledge work, semantic systems and now artificial intelligence. And within the same breath, organizations and experts regale the industry with talk of innovation and the emergence of a new paradigm in ways of working. Yet those ways of working are locked inside of Agile methodologies and Scrum, designed for the factory floor and the codebase --- not for the messy, recursive, deeply human work of making meaning out of complexity.
== Scrum Is a Manufacturing Schedule
Let's return to the Takeuchi and Nonaka paper, where the title --- "The New New Product Development Game" --- describes how product teams at Japanese firms developed physical goods such as the FX-3500 copier and the Honda City car. They draw on the rugby metaphor because the team "tries to go the distance as a unit, passing the ball back and forth." The unit of delivery is a working prototype of a material artifact. The feedback loop is a machine that works or fails.
Sutherland's 2014 book, _Scrum: The Art of Doing Twice the Work in Half the Time_, reinforces this with military and manufacturing case studies such as the F-86 fighter production and Toyota. Sutherland's book rebuilds the argument with the same assumption --- that the thing being built has discrete, inspectable, testable parts that, when finished, are finished.
The 2020 Scrum Guide defines the Increment as "a concrete stepping stone toward the Product Goal," one that is "additive to all prior Increments and thoroughly verified, ensuring that all Increments work together." These words are sensible if you are building a mobile application, with a login screen and a settings page. But for knowledge systems, these words are destructive, because the components of a semantic architecture are not additive in the way Scrum dictates. A controlled vocabulary is not a subcomponent of a taxonomy the way a login screen is a subcomponent of an app. A taxonomy without disciplined, vocabulary control as its underpinnings does not function well, if at all. You cannot ship half a taxonomy and call it an Increment.
You see, Scrum assumes tame problems that can be time-boxed, estimated with planning poker, decomposed into user stories, tracked on a velocity chart and declared done against a checklist. If only knowledge were this tame. Knowledge organization is anything but tame. More than fifty years old, in 1973 Horst Rittel and Melvin Webber warned that applying the engineering paradigm uniformly to all problems was "bound to fail." And yet here we are, applying a manufacturing schedule and methodologies to knowledge, expecting that knowledge will fit into the confines of old ways of working --- an artifact of the Third Industrial Revolution imposed on the demands of the Fourth.
In 2016, Klaus Schwab wrote about this historical pattern. The First Industrial Revolution, beginning around 1760, gave us steam and mechanical production. The Second, in the late nineteenth century, gave us electricity and the assembly line --- the world that produced Toyota, lean manufacturing, and the production logic that Scrum still carries as its core ethos. The Third, beginning in the 1960s, gave us semiconductors, mainframe computing, and eventually the internet --- the world in which Agile was born, by software developers, for software development. The Fourth Industrial Revolution, Schwab argued, is fundamentally different, in kind, as it is defined by the fusion of artificial intelligence, cyber-physical systems and knowledge technologies that blur the boundaries between the physical, digital and biological worlds.
The problems of this era are not necessarily engineering problems with stable requirements. We are wrestling with problems such as how to ground machine intelligence in meaning, how to organize evolving knowledge and how to build systems that can explain themselves. These are wicked problems that demand new epistemologies and frameworks for problem solving. Agile may have been the right methodology for the Third Industrial Revolution but we are no longer in the Third Industrial Revolution. The frameworks have not caught up, and the cost of that lag is exacerbated every time an organization forces Third Industrial Revolution principles applied to Fourth Industrial Revolution work.
In other words, this is not a product problem or a platform problem --- it is learning to embrace new ways of working. This is an infrastructure problem that is governed by epistemology --- a fundamental philosophy pertaining to knowledge.
== Why Knowledge Cannot Be Sprinted
An ontology is, per Thomas Gruber's foundational definition, "a specification of a conceptualization." A conceptualization is a whole, not parts of a whole. Specifying half of it produces logical incoherence, not a minimally viable product. For example, ANSI/NISO Z39.19, the standard for constructing controlled vocabularies, requires resolving synonymy, homonymy and scope before terms enter production, because any inconsistency propagates downstream into every subsequent use. SKOS, the W3C standard for encoding knowledge organization systems, requires that hierarchical relationships form coherent structures with transitive closure and OWL 2 requires that class axioms be satisfiable. These are preconditions, necessary for logical systems to reason and express meaning.
In practice, I have watched organizations attempt Agile delivery of knowledge systems in two patterns and neither ends well. In the first, the team holds stand-ups, ceremonies and retrospectives. The taxonomist brings tickets and adds fifteen terms to the product vocabulary. Tickets close and a velocity chart goes up. Nothing is integrated, because integration is the hard part and integration does not fit into two weeks so that goes to the backlog. Six months later, the vocabulary is a collection of inconsistently scoped terms, the ontology has orphan classes and the knowledge graph --- if it exists --- cannot reliably answer queries. But the burndown chart is immaculate and leadership is blindly satisfied because boxes were checked.
In the second pattern, the team borrows from other methodologies while still calling it Agile. The taxonomist conducts multi-week domain analysis and stakeholder interviews --- that is ethnography. She does card sorting and concept modeling --- that is design research. She iterates with subject matter experts --- that is participatory design. She ships, eventually, a coherent model. The sprint cadence was irrelevant to the work. Martin Fowler named this phenomenon in his 2018 keynote: "faux-agile" --- the Agile Industrial Complex.
Ron Jeffries, another original signatory of the Agile Manifesto, told developers the same year, 2018, to abandon the word entirely. Dave Thomas declared in 2014 that "Agile" had become "a magnet for anyone with points to espouse, hours to bill or products to sell." When three of the seventeen authors of a framework say it has been hollowed out, one has to wonder if Agile is appropriate for all technology work and if it is time to revisit ways of working.
== Innovation Does Not Fit in a Backlog
The deeper issue is that Agile and Scrum were never designed to inspire or foster innovation. They were designed to deliver known requirements with increasing efficiency. The Scrum Guide's entire apparatus --- product backlog, sprint planning, Definition of Done --- presupposes that the problem has been formulated by a product owner who can write it in a user story. But knowledge work, like design, is wicked. Rittel and Webber's first property of a wicked problem is that "There is no definitive formulation." The second property states that there is no stopping rule. And the fifth property illuminates that every solution is a "one-shot operation" because there is no opportunity to learn by trial-and-error without consequence.
Given the nature of wicked problems, it would seem that Agile is ill fitted for wicked problems. And for that matter, ill fitted for the dynamic nature of knowledge, where knowledge infrastructures are never one-shot operations, necessitate stopping rules and without a doubt defy definitive formulations.
Therefore, knowledge organization in an enterprise domain is wicked in exactly this sense. The problem of how to represent "customer" in a telecommunications company's semantic model has no pre-existing correct answer. Its formulation depends on who is doing what with the representation, and that changes as you build. The scope of the vocabulary shifts as you interview stakeholders. The taxonomy reorganizes as you encounter edge cases. The ontology's axioms tighten or loosen as the reasoner surfaces contradictions. Don't be mistaken --- this does not bend to the Agile principle of welcoming changing requirements, as the requirements are constituted _through_ the work. The problem is under construction, often evolving, during the solving.
Organizations that manage all work through Scrum are, in effect, optimizing for predictability in domains that demand exploration. The results resist innovation and take on the appearance of productivity. Velocity charts rise while the underlying intellectual architecture is flattened into bite-sized deployments, uncoupled from the continuum that is knowledge.
== Design Thinking Belongs in the Swamp
Donald Schön drew the distinction that "In the varied topography of professional practice, there is a high, hard ground where practitioners can make effective use of research-based theory and technique, and there is a swampy lowland where situations are confusing 'messes' incapable of technical solution." Agile lives on the high ground while knowledge work lives in the swamp. Schön's term for how competent practitioners navigate the swamp was "reflection-in-action" --- the iterative, tacit recalibration that happens while the work is happening.
Design thinking, in the lineage of Herbert Simon, Richard Buchanan and Schön, is the methodology of the swamp. It assumes the problem is under construction. Its five phases --- empathize, define, ideate, prototype, test --- are not a linear pipeline but a recursive, iterative process that expects the definition of the problem to change as understanding deepens.
Research in the _Handbook of Human-Centered Artificial Intelligence_ confirms this alignment --- integrating design thinking into AI development makes systems more user-focused, iterative and impactful, emphasizing data governance, transparency, explainability and bias mitigation. Carnegie Mellon University's Tepper School of Business now teaches a dedicated program on design thinking with AI, arguing that the combination addresses "complex, systemic problems" that linear frameworks cannot manage successfully. IBM's enterprise design thinking practice, developed over two decades, has been applied across client engagements precisely because it keeps human experience at the center while technology evolves underneath --- design thinking is "timeless," while technology is "timestamped."
A 2026 study in _Scientific Reports_ examined the relationship between higher order thinking, generative AI chatbot use and engineering creativity, finding that higher order thinking had significantly greater importance for creative outcomes than AI tool use alone --- reinforcing that cognitive depth, not tool velocity, drives innovation. Research in the _Proceedings of the Design Society_ further argues that generative AI is shifting the designer's role from executor to paradigm-setter, making design thinking more essential as AI handles production tasks while humans must guide framing, ethics and meaning.
To be clear, these frameworks are not different flavors of the same methodology. Scrum and design thinking represent totally different epistemologies. Scrum assumes the problem was handed to you while design thinking assumes the problem must be found and wrestled with.
== Is AI a Knowledge Tool?
This brings us to the question the industry avoids and I, for one, have been itching to ask. Is AI a knowledge tool? How do you classify it?
A large language model generates plausible sequences of text. Returning to the Bender, Gebru, McMillan-Major, and Mitchell paper mentioned earlier, the authors defined a language model as a system for stitching together linguistic forms according to probabilistic patterns, "without any reference to meaning." Without reference to meaning, as LLMs out-of-the-box are absent of external grounding. They optimize for plausibility, not truth and therefore as a standalone, can be best categorized as an information-processing tool.
It becomes a knowledge tool only when grounded in a semantic layer --- controlled vocabularies, taxonomies, ontologies, knowledge graphs --- that provides reference and provenance. The industry's own corrective measures and associated marketplace confirms this as fact. Retrieval-augmented generation, knowledge graphs for LLMs and what recent discourse calls "context engineering" are, underneath the packaging, the semantic infrastructure that should have been built first. But it was bolted on after the fact to prevent hallucination that was pretty much guaranteed the moment the model was deployed without one.
The RAG paper from 2020 called out this problem, identifying that parametric memory is opaque and cannot be updated and that retrieval from structured sources is how provenance is captured and encoded. But it seems the industry has a memory problem or it was too early for most to hear the message.
Ironically, provenance is a core element of knowledge management and ontology work, as provenance is a principled tenant of the discipline and problem space. Without provenance, knowledge cannot exist. Organizations are only now discovering they need it, because their generative systems cannot explain themselves. Call it lineage, call it traces. But if you are building for knowledge, call it provenance, as provenance includes traces, lineage, citations, the temporal and the spatial.
So if generative AI is an information processing tool, I posture that for AI to become a knowledge tool, there must be provenance and descriptive logic. And to be frank, nobody sprints their way into provenance --- it is architected and sustained.
== Knowledge Is a Spiral, Not a Sprint
The irony does not escape the moment. The same Nonaka who co-authored the 1986 paper that inspired Scrum went on to develop the SECI model of knowledge creation in 1995, describing knowledge as a spiral between tacit and explicit forms, moving through socialization, externalization, combination and internalization.
The spiral does not have an end, as is true with spirals, emblematic of life. Each rotation creates new knowledge that becomes the substrate for the next rotation. There is no Definition of Done because the definition of what is being done is itself being constructed. Scrum took the team structure from Nonaka's first paper and discarded his next decade of work because it did not fit neatly with Scrum. The mechanisms of knowledge work, as Nonaka spent a decade arguing, are metaphor, tacit skill, apprenticeship, shared space and the patient conversion of hunches into models and models back into practice.
The Ontology Pipeline™ mirrors this spiral. You start with a controlled vocabulary --- a disciplined process involving agreements as to what terms mean. You mature it into a taxonomy by introducing hierarchy, structuring terms into parent-child relations. Thereafter, the taxonomy is matured into a thesaurus by adding associative relationships and synonymy, encoded in SKOS. The thesaurus is baseline work from which an ontology is developed, adding formal semantics using OWL and RDF. You architect a knowledge graph, assembling components to include the knowledge assets developed through the Ontology Pipeline™ iterative framework.
Each stage prepares the ground for the next and stages cannot be skipped or bypassed. You cannot ship the knowledge graph without an ontology, and the ontology must be logically consistent and coherent to make sense of data. A knowledge graph without vocabulary control risks becoming a confident liar or failing to work with natural language. Because knowledge assets are dimensionally interconnected, knowledge is indeed a spiral, unable to be contained by manufacturing logic.
== The Pattern and the Cure
Organizations budget for platforms --- Confluence, SharePoint, LLM subscriptions, vector databases, a RAG vendor. But they do not budget for descriptive logic and knowledge, perhaps because the problem is not solved by a single tool and is seemingly too amorphous in an Agile, data-centric world. The controlled vocabulary is cut unless it delivers value for the database. Often, the taxonomy is cut because it does not demo well and its value sits in the plumbing, delivering its punch over time. The result is an ever-growing pile of documents with a chatbot stapled to the front, confidently answering questions in the voice of a system that has no reference for any of the terms sprinkled throughout the documents.
Then the system hallucinates. It contradicts itself across sessions. It surfaces outdated policy as current and surfaces private Outlook messages from deactivated employees. I have seen this in real life more times than I can count. And the optics are worse when a vendor tool is responsible for revealing private chats and email transactions. Usually the executive sponsor asks why the investment has not worked, ignoring the evidence of faux pas such as the surfacing of private communications. All in a day's work, when burndown charts and velocity set the precedent for ways of working and reward systems.
Executives ignore the fact that the system was never grounded. It was never grounded because grounding requires the slow, expensive, disciplined work of knowledge organization --- work that does not fit into a sprint, does not demo well and fails to produce a velocity chart. The organization chooses to bypass investment in the descriptive knowledge apparatus because the lines are too blurry to clearly define according to Agile speak. Because knowledge is infrastructure, not a product or a platform. Knowledge is the gas in the tank, the fuel for meaning, the material that justifies logical reasoning.
Design thinking gives that knowledge work a home. It is comfortable with ambiguity. It treats problem discovery as an equal to problem solving. It certainly does not demand a shippable increment every fourteen days. Instead, design thinking demands that you understand what you are building before you build it --- and that you keep revising that understanding as the work proceeds. Library and information science, with over a century of standards for knowledge organization, provides the methods while design thinking provides the epistemology. Together, they offer what Agile never could --- a framework suited to the nature of knowledge itself.
A two-week sprint to deliver what? If you are building knowledge systems, two weeks will get you nothing coherent. Knowledge is not built fast and broken into shape. It is built carefully, with provenance, governance and patience. Knowledge is not a product to be manufactured and no --- you cannot buy off-the-shelf knowledge that perfectly matches a domain or organization. Knowledge is nuanced, specific to each organization, domain and person.
In the spirit of VC-speak, knowledge is the moat, and that moat is not Agile.
#v(2em)
#line(length: 100%, stroke: 0.4pt + luma(200))
#v(0.6em)
#text(size: 9.5pt, fill: luma(120))[
_Jessica Talisman, MLS_ is a Semantic Engineer, Information Architect, and knowledge infrastructure strategist with more than 25 years of experience. She is the founder of the Ontology Pipeline™ and publishes _Intentional Arrangement_ on Substack.
]

View file

@ -0,0 +1,160 @@
#set document(
title: "Un Sprint de Dos Semanas para el Conocimiento",
author: "Jessica Talisman, MLS",
)
#set page(
paper: "a4",
margin: (top: 2.8cm, bottom: 3cm, left: 3cm, right: 3cm),
footer: context [
#set text(size: 9pt, fill: luma(140))
#h(1fr)
#counter(page).display("1")
],
)
#set text(
font: ("Georgia", "Linux Libertine"),
size: 11pt,
lang: "es",
hyphenate: true,
)
#set par(
justify: true,
leading: 0.75em,
spacing: 1.2em,
)
#set heading(numbering: none)
#show heading.where(level: 1): it => [
#v(0.6em)
#block(
text(size: 13.5pt, weight: "semibold", it.body)
)
#v(0.3em)
]
#show heading.where(level: 2): it => [
#v(1.2em)
#block(
text(size: 12pt, weight: "semibold", it.body)
)
#v(0.2em)
]
// ── Título ───────────────────────────────────────────────────────────────────
#align(center)[
#v(1.6em)
#text(size: 22pt, weight: "bold", hyphenate: false)[Un Sprint de Dos Semanas para el Conocimiento]
#v(0.5em)
#text(size: 14pt, style: "italic", fill: luma(80))[De Agile al Pensamiento de Diseño]
#v(1em)
#line(length: 60%, stroke: 0.5pt + luma(180))
#v(0.8em)
#text(size: 11pt)[*Jessica Talisman, MLS*]
#v(0.2em)
#text(size: 10pt, fill: luma(120))[21 de abril de 2026 · Intentional Arrangement]
#v(2em)
]
// ── Cuerpo ───────────────────────────────────────────────────────────────────
En 1986, Hirotaka Takeuchi e Ikujiro Nonaka publicaron "The New New Product Development Game" en _Harvard Business Review_, describiendo cómo Honda, Canon y Fuji-Xerox organizaban sus equipos para fabricar copiadoras y automóviles. Jeff Sutherland leyó ese artículo, lo combinó con los principios de manufactura lean y la programación orientada a objetos, y en 1993 lo adaptó al desarrollo de software en Easel Corporation. Ken Schwaber formalizó el método en la conferencia OOPSLA de 1995. En 2001, diecisiete desarrolladores de software se reunieron en un albergue de esquí en Snowbird, Utah, y firmaron el Manifiesto Ágil, declarando que estaban "descubriendo mejores maneras de desarrollar software." No conocimiento ni significado. Un marco de manufactura aplicado al software.
El propio lenguaje del Manifiesto Ágil es explícito sobre su alcance, y sin embargo la industria ha aplicado la metodología ágil a prácticamente todo lo que existe en un entorno tecnológico --- incluyendo el trabajo del conocimiento, los sistemas semánticos y ahora la inteligencia artificial. Y en el mismo aliento, organizaciones y expertos colman la industria con discursos sobre innovación y el surgimiento de un nuevo paradigma en las formas de trabajo. Sin embargo, esas formas de trabajo permanecen encerradas dentro de las metodologías ágiles y Scrum, diseñadas para la línea de producción y la base de código --- no para el trabajo profundamente humano, recursivo y desordenado de construir significado a partir de la complejidad.
== Scrum es un calendario de manufactura
Volvamos al artículo de Takeuchi y Nonaka, cuyo título --- "The New New Product Development Game" --- describe cómo los equipos de producto en empresas japonesas desarrollaban bienes físicos como la copiadora FX-3500 y el Honda City. Los autores recurren a la metáfora del rugby porque el equipo "intenta recorrer la distancia como una unidad, pasándose el balón de un lado al otro." La unidad de entrega es un prototipo funcional de un artefacto material. El ciclo de retroalimentación es una máquina que funciona o falla.
El libro de Sutherland de 2014, _Scrum: The Art of Doing Twice the Work in Half the Time_, refuerza esto con casos de estudio militares y de manufactura como la producción del caza F-86 y Toyota. El libro reconstruye el argumento sobre la misma premisa: que aquello que se construye tiene partes discretas, inspeccionables y verificables que, una vez terminadas, están terminadas.
La Guía Scrum de 2020 define el Incremento como "un peldaño concreto hacia el Objetivo del Producto", uno que es "aditivo respecto a todos los Incrementos anteriores y rigurosamente verificado, asegurando que todos los Incrementos funcionen en conjunto." Estas palabras tienen sentido si se está construyendo una aplicación móvil, con una pantalla de inicio de sesión y una página de configuración. Pero para los sistemas de conocimiento, estas palabras son destructivas, porque los componentes de una arquitectura semántica no son aditivos de la manera en que Scrum prescribe. Un vocabulario controlado no es un subcomponente de una taxonomía del mismo modo en que una pantalla de inicio de sesión lo es de una aplicación. Una taxonomía sin un control de vocabulario disciplinado como fundamento no funciona bien, o directamente no funciona. No se puede entregar media taxonomía y llamarla un Incremento.
Scrum asume problemas dóciles que pueden enmarcarse en el tiempo, estimarse con planning poker, descomponerse en historias de usuario, rastrearse en un gráfico de velocidad y declararse resueltos frente a una lista de verificación. Si tan solo el conocimiento fuera así de dócil. La organización del conocimiento está muy lejos de serlo. Con más de cincuenta años de antigüedad, en 1973 Horst Rittel y Melvin Webber advirtieron que aplicar el paradigma de la ingeniería de manera uniforme a todos los problemas estaba "condenado al fracaso." Y sin embargo aquí estamos, aplicando un calendario de manufactura y sus metodologías al conocimiento, esperando que el conocimiento quepa en los confines de viejas formas de trabajar --- un artefacto de la Tercera Revolución Industrial impuesto sobre las exigencias de la Cuarta.
En 2016, Klaus Schwab escribió sobre este patrón histórico. La Primera Revolución Industrial, que comenzó alrededor de 1760, nos dio el vapor y la producción mecánica. La Segunda, a finales del siglo XIX, nos dio la electricidad y la cadena de montaje --- el mundo que produjo Toyota, la manufactura lean y la lógica de producción que Scrum aún lleva como ethos central. La Tercera, que comenzó en la década de 1960, nos dio los semiconductores, la computación mainframe y finalmente internet --- el mundo en que nació Agile, por desarrolladores de software, para el desarrollo de software. La Cuarta Revolución Industrial, argumentó Schwab, es fundamentalmente diferente en su naturaleza, pues está definida por la fusión de la inteligencia artificial, los sistemas ciberfísicos y las tecnologías del conocimiento que difuminan las fronteras entre los mundos físico, digital y biológico.
Los problemas de esta era no son necesariamente problemas de ingeniería con requisitos estables. Estamos lidiando con preguntas como de qué manera anclar la inteligencia artificial en el significado, cómo organizar el conocimiento en evolución y cómo construir sistemas capaces de explicarse a mismos. Estos son problemas retorcidos que exigen nuevas epistemologías y marcos para la resolución de problemas. Agile puede haber sido la metodología correcta para la Tercera Revolución Industrial, pero ya no estamos en ella. Los marcos no se han puesto al día, y el coste de ese rezago se agrava cada vez que una organización impone los principios de la Tercera Revolución Industrial al trabajo de la Cuarta.
En otras palabras, esto no es un problema de producto ni de plataforma --- es el desafío de aprender a abrazar nuevas formas de trabajar. Es un problema de infraestructura gobernado por la epistemología: una filosofía fundamental sobre el conocimiento.
== Por qué el conocimiento no puede ejecutarse en sprints
Una ontología es, según la definición fundacional de Thomas Gruber, "una especificación de una conceptualización." Una conceptualización es un todo, no partes de un todo. Especificar la mitad produce incoherencia lógica, no un producto mínimamente viable. Por ejemplo, la norma ANSI/NISO Z39.19 para la construcción de vocabularios controlados exige resolver la sinonimia, la homonimia y el alcance antes de que los términos entren en producción, porque cualquier inconsistencia se propaga hacia abajo en cada uso posterior. SKOS, el estándar del W3C para codificar sistemas de organización del conocimiento, requiere que las relaciones jerárquicas formen estructuras coherentes con cierre transitivo, y OWL 2 exige que los axiomas de clase sean satisfacibles. Estas son precondiciones necesarias para que los sistemas lógicos razonen y expresen significado.
En la práctica, he observado organizaciones intentando entregar sistemas de conocimiento de forma ágil en dos patrones, y ninguno termina bien. En el primero, el equipo celebra stand-ups, ceremonias y retrospectivas. El taxonomista trae tickets y añade quince términos al vocabulario del producto. Los tickets se cierran y el gráfico de velocidad sube. Nada se integra, porque la integración es la parte difícil y no cabe en dos semanas, de modo que va al backlog. Seis meses después, el vocabulario es una colección de términos con alcances inconsistentes, la ontología tiene clases huérfanas y el grafo de conocimiento --- si existe --- no puede responder consultas de manera confiable. Pero el burndown chart es impecable y el liderazgo está ciegamente satisfecho porque las casillas fueron marcadas.
En el segundo patrón, el equipo toma prestado de otras metodologías sin dejar de llamarlo ágil. La taxonomista conduce análisis de dominio y entrevistas con partes interesadas durante varias semanas --- eso es etnografía. Realiza card sorting y modelado conceptual --- eso es investigación en diseño. Itera con expertos en la materia --- eso es diseño participativo. Finalmente entrega un modelo coherente. El ritmo del sprint fue irrelevante para el trabajo. Martin Fowler denominó este fenómeno en su conferencia magistral de 2018: "faux-agile" --- el Complejo Industrial Ágil.
Ron Jeffries, otro signatario original del Manifiesto Ágil, les dijo a los desarrolladores ese mismo año, 2018, que abandonaran la palabra por completo. Dave Thomas declaró en 2014 que "Ágil" se había convertido en "un imán para cualquiera con puntos que exponer, horas que facturar o productos que vender." Cuando tres de los diecisiete autores de un marco dicen que ha sido vaciado de contenido, uno tiene que preguntarse si Agile es apropiado para todo el trabajo tecnológico y si ha llegado el momento de revisar las formas de trabajar.
== La innovación no cabe en un backlog
El problema más profundo es que Agile y Scrum nunca fueron diseñados para inspirar o fomentar la innovación. Fueron diseñados para entregar requisitos conocidos con eficiencia creciente. Todo el aparato de la Guía Scrum --- product backlog, sprint planning, Definición de Hecho --- presupone que el problema ha sido formulado por un product owner capaz de escribirlo en una historia de usuario. Pero el trabajo del conocimiento, como el diseño, es retorcido. La primera propiedad de un problema retorcido según Rittel y Webber es que "No existe una formulación definitiva." La segunda propiedad establece que no hay regla de parada. Y la quinta propiedad ilumina que cada solución es una "operación de un solo disparo" porque no hay oportunidad de aprender por ensayo y error sin consecuencias.
Dada la naturaleza de los problemas retorcidos, parecería que Agile está mal equipado para enfrentarlos. Y por la misma razón, mal equipado para la naturaleza dinámica del conocimiento, donde las infraestructuras de conocimiento nunca son operaciones de un solo disparo, requieren reglas de parada y desafían sin lugar a dudas las formulaciones definitivas.
Por lo tanto, la organización del conocimiento en un dominio empresarial es retorcida exactamente en este sentido. El problema de cómo representar "cliente" en el modelo semántico de una empresa de telecomunicaciones no tiene una respuesta correcta preexistente. Su formulación depende de quién hace qué con esa representación, y eso cambia a medida que se construye. El alcance del vocabulario se desplaza conforme se entrevista a las partes interesadas. La taxonomía se reorganiza al encontrar casos límite. Los axiomas de la ontología se tensan o aflojan cuando el razonador descubre contradicciones. No hay que equivocarse --- esto no se pliega al principio ágil de bienvenida a los requisitos cambiantes, porque los requisitos se constituyen _a través_ del trabajo. El problema está en construcción, evolucionando frecuentemente, durante su resolución.
Las organizaciones que gestionan todo el trabajo a través de Scrum están, en efecto, optimizando para la previsibilidad en dominios que exigen exploración. Los resultados inhiben la innovación y adoptan la apariencia de productividad. Los gráficos de velocidad suben mientras la arquitectura intelectual subyacente se aplana en entregas fragmentadas, desconectadas del continuo que es el conocimiento.
== El pensamiento de diseño pertenece al pantano
Donald Schön trazó la distinción de que "En la variada topografía de la práctica profesional, hay una meseta alta y firme donde los profesionales pueden hacer uso efectivo de la teoría y la técnica basadas en la investigación, y hay unas tierras bajas pantanosas donde las situaciones son 'enredos' confusos incapaces de solución técnica." Agile vive en la meseta mientras que el trabajo del conocimiento vive en el pantano. El término de Schön para describir cómo los profesionales competentes navegan el pantano fue "reflexión-en-acción" --- la recalibración iterativa y tácita que ocurre mientras el trabajo transcurre.
El pensamiento de diseño, en el linaje de Herbert Simon, Richard Buchanan y Schön, es la metodología del pantano. Asume que el problema está en construcción. Sus cinco fases --- empatizar, definir, idear, prototipar, probar --- no son un flujo lineal sino un proceso recursivo e iterativo que espera que la definición del problema cambie a medida que la comprensión se profundiza.
La investigación en el _Handbook of Human-Centered Artificial Intelligence_ confirma esta alineación --- integrar el pensamiento de diseño en el desarrollo de IA hace que los sistemas sean más centrados en el usuario, iterativos e impactantes, enfatizando la gobernanza de datos, la transparencia, la explicabilidad y la mitigación del sesgo. La Tepper School of Business de Carnegie Mellon University imparte ahora un programa dedicado a pensamiento de diseño con IA, argumentando que la combinación aborda "problemas complejos y sistémicos" que los marcos lineales no pueden gestionar con éxito. La práctica de diseño empresarial de IBM, desarrollada a lo largo de dos décadas, se ha aplicado en compromisos con clientes precisamente porque mantiene la experiencia humana en el centro mientras la tecnología evoluciona por debajo --- el pensamiento de diseño es "atemporal", mientras que la tecnología está "fechada".
Un estudio de 2026 en _Scientific Reports_ examinó la relación entre el pensamiento de orden superior, el uso de chatbots de IA generativa y la creatividad en ingeniería, encontrando que el pensamiento de orden superior tenía una importancia significativamente mayor para los resultados creativos que el uso de herramientas de IA por solo --- reforzando que la profundidad cognitiva, no la velocidad de las herramientas, impulsa la innovación. La investigación en las _Proceedings of the Design Society_ argumenta además que la IA generativa está desplazando el rol del diseñador de ejecutor a configurador de paradigmas, haciendo que el pensamiento de diseño sea más esencial a medida que la IA gestiona las tareas de producción mientras los humanos deben guiar el encuadre, la ética y el significado.
Para ser claros, estos marcos no son distintos sabores de la misma metodología. Scrum y el pensamiento de diseño representan epistemologías completamente diferentes. Scrum asume que el problema te fue entregado, mientras que el pensamiento de diseño asume que el problema debe encontrarse y lidiarse con él.
== ¿Es la IA una herramienta de conocimiento?
Esto nos lleva a la pregunta que la industria evita y que yo, por mi parte, llevo tiempo deseando formular. ¿Es la IA una herramienta de conocimiento? ¿Cómo se clasifica?
Un modelo de lenguaje de gran escala genera secuencias plausibles de texto. Volviendo al artículo de Bender, Gebru, McMillan-Major y Mitchell mencionado anteriormente, los autores definieron un modelo de lenguaje como un sistema para ensamblar formas lingüísticas según patrones probabilísticos, "sin ninguna referencia al significado." Sin referencia al significado, dado que los LLMs sin configuración adicional carecen de anclaje externo. Optimizan para la plausibilidad, no para la verdad, y por lo tanto, como sistemas autónomos, pueden clasificarse mejor como herramientas de procesamiento de información.
Se convierte en una herramienta de conocimiento únicamente cuando se ancla en una capa semántica --- vocabularios controlados, taxonomías, ontologías, grafos de conocimiento --- que proporciona referencia y procedencia. Las propias medidas correctivas de la industria y el mercado asociado confirman esto como un hecho. La generación aumentada por recuperación, los grafos de conocimiento para LLMs y lo que el discurso reciente denomina "ingeniería de contexto" son, bajo el empaquetado, la infraestructura semántica que debería haberse construido primero. Pero fue añadida a posteriori para prevenir alucinaciones que estaban prácticamente garantizadas desde el momento en que el modelo fue desplegado sin ella.
El artículo sobre RAG de 2020 señaló este problema, identificando que la memoria paramétrica es opaca y no puede actualizarse, y que la recuperación desde fuentes estructuradas es el modo en que la procedencia se captura y codifica. Pero parece que la industria tiene un problema de memoria, o el mensaje llegó demasiado pronto para que la mayoría pudiera escucharlo.
Irónicamente, la procedencia es un elemento central de la gestión del conocimiento y el trabajo ontológico, pues constituye un principio fundacional de la disciplina y su espacio de problemas. Sin procedencia, el conocimiento no puede existir. Las organizaciones están descubriendo ahora que la necesitan, porque sus sistemas generativos no pueden explicarse a mismos. Llámalo linaje, llámalo trazabilidad. Pero si construyes para el conocimiento, llámalo procedencia, porque la procedencia abarca trazas, linaje, citaciones, lo temporal y lo espacial.
De modo que si la IA generativa es una herramienta de procesamiento de información, sostengo que para que la IA se convierta en una herramienta de conocimiento debe haber procedencia y lógica descriptiva. Y para ser francos, nadie llega a la procedencia haciendo sprints --- esta se arquitecta y se sostiene.
== El conocimiento es una espiral, no un sprint
La ironía no pasa desapercibida en este momento. El mismo Nonaka que coescribió el artículo de 1986 que inspiró Scrum desarrolló el modelo SECI de creación de conocimiento en 1995, describiendo el conocimiento como una espiral entre formas tácitas y explícitas, que avanza a través de la socialización, la externalización, la combinación y la internalización.
La espiral no tiene fin, como es propio de las espirales, emblema de la vida. Cada rotación crea nuevo conocimiento que se convierte en el sustrato de la siguiente. No existe una Definición de Hecho porque la definición de lo que se está haciendo está siendo construida en misma. Scrum tomó la estructura de equipo del primer artículo de Nonaka y descartó su siguiente década de trabajo porque no encajaba limpiamente en Scrum. Los mecanismos del trabajo del conocimiento, como Nonaka argumentó durante una década, son la metáfora, la habilidad tácita, el aprendizaje por proximidad, el espacio compartido y la paciente conversión de intuiciones en modelos y de modelos de vuelta en práctica.
El Ontology Pipeline™ refleja esta espiral. Se comienza con un vocabulario controlado --- un proceso disciplinado que implica acuerdos sobre lo que significan los términos. Se madura hacia una taxonomía introduciendo jerarquía y estructurando los términos en relaciones padre-hijo. Posteriormente, la taxonomía se madura hacia un tesauro añadiendo relaciones asociativas y sinonimia, codificadas en SKOS. El tesauro es el trabajo base a partir del cual se desarrolla una ontología, incorporando semántica formal mediante OWL y RDF. Se arquitecta un grafo de conocimiento, ensamblando componentes que incluyen los activos de conocimiento desarrollados a través del marco iterativo del Ontology Pipeline™.
Cada etapa prepara el terreno para la siguiente y las etapas no pueden saltarse ni eludirse. No se puede entregar el grafo de conocimiento sin una ontología, y la ontología debe ser lógicamente consistente y coherente para dotar de sentido a los datos. Un grafo de conocimiento sin control de vocabulario arriesga convertirse en un mentiroso convincente o en fallar al trabajar con lenguaje natural. Porque los activos de conocimiento son dimensionalmente interdependientes, el conocimiento es efectivamente una espiral, incapaz de ser contenida por la lógica de la manufactura.
== El patrón y el remedio
Las organizaciones presupuestan para plataformas --- Confluence, SharePoint, suscripciones a LLMs, bases de datos vectoriales, un proveedor de RAG. Pero no presupuestan para la lógica descriptiva y el conocimiento, quizás porque el problema no se resuelve con una sola herramienta y resulta demasiado amorfo en un mundo centrado en los datos y en Agile. El vocabulario controlado se recorta a menos que genere valor inmediato para la base de datos. Con frecuencia, la taxonomía se recorta porque no hace una buena demostración y su valor reside en la fontanería, entregando su impacto con el tiempo. El resultado es una pila siempre creciente de documentos con un chatbot pegado en la entrada, respondiendo preguntas con confianza en la voz de un sistema que no tiene referencia para ninguno de los términos dispersos a lo largo de los documentos.
Entonces el sistema alucina. Se contradice a mismo entre sesiones. Presenta como vigente una política desactualizada y muestra mensajes privados de Outlook de empleados cuyas cuentas fueron desactivadas. He visto esto en la vida real más veces de las que puedo contar. Y la percepción es peor cuando una herramienta de proveedor es responsable de revelar chats y transacciones de correo privadas. Habitualmente, el patrocinador ejecutivo pregunta por qué la inversión no ha funcionado, ignorando la evidencia de los errores, como la exposición de comunicaciones privadas. Trabajo de un día cualquiera, cuando los burndown charts y la velocidad marcan el precedente de las formas de trabajar y los sistemas de recompensa.
Los ejecutivos ignoran el hecho de que el sistema nunca fue anclado en el conocimiento. Nunca lo fue porque anclar requiere el trabajo lento, costoso y disciplinado de la organización del conocimiento --- trabajo que no cabe en un sprint, que no hace una buena demostración y que no genera un gráfico de velocidad. La organización elige eludir la inversión en el aparato de conocimiento descriptivo porque las líneas son demasiado difusas para definirse claramente según el vocabulario de Agile. Porque el conocimiento es infraestructura, no un producto ni una plataforma. El conocimiento es la gasolina del depósito, el combustible del significado, el material que justifica el razonamiento lógico.
El pensamiento de diseño le da al trabajo del conocimiento un hogar. Es cómodo con la ambigüedad. Trata el descubrimiento del problema como un igual a la resolución del problema. Desde luego no exige un incremento entregable cada catorce días. En cambio, el pensamiento de diseño exige que entiendas lo que estás construyendo antes de construirlo --- y que sigas revisando esa comprensión a medida que el trabajo avanza. La biblioteconomía y la ciencia de la información, con más de un siglo de estándares para la organización del conocimiento, proporcionan los métodos mientras el pensamiento de diseño proporciona la epistemología. Juntos ofrecen lo que Agile nunca pudo --- un marco adecuado a la naturaleza del conocimiento mismo.
¿Un sprint de dos semanas para entregar qué? Si estás construyendo sistemas de conocimiento, dos semanas no producirán nada coherente. El conocimiento no se construye rápido y se moldea a la fuerza. Se construye con cuidado, con procedencia, gobernanza y paciencia. El conocimiento no es un producto que pueda manufacturarse y no --- no se puede comprar conocimiento estándar que encaje perfectamente con un dominio u organización. El conocimiento es matizado, específico a cada organización, dominio y persona.
En el espíritu del argot del capital de riesgo: el conocimiento es el foso, y ese foso no es Ágil.
#v(2em)
#line(length: 100%, stroke: 0.4pt + luma(200))
#v(0.6em)
#text(size: 9.5pt, fill: luma(120))[
_Jessica Talisman, MLS_ es Ingeniera Semántica, Arquitecta de Información y estratega de infraestructuras de conocimiento con más de 25 años de experiencia. Es fundadora del Ontology Pipeline™ y publica _Intentional Arrangement_ en Substack.
]

View file

@ -0,0 +1,101 @@
# Un Sprint de Dos Semanas para el Conocimiento
## De Agile al Pensamiento de Diseño
**Jessica Talisman, MLS**
*21 de abril de 2026*
---
En 1986, Hirotaka Takeuchi e Ikujiro Nonaka publicaron "The New New Product Development Game" en *Harvard Business Review*, describiendo cómo Honda, Canon y Fuji-Xerox organizaban sus equipos para fabricar copiadoras y automóviles. Jeff Sutherland leyó ese artículo, lo combinó con los principios de manufactura lean y la programación orientada a objetos, y en 1993 lo adaptó al desarrollo de software en Easel Corporation. Ken Schwaber formalizó el método en la conferencia OOPSLA de 1995. En 2001, diecisiete desarrolladores de software se reunieron en un albergue de esquí en Snowbird, Utah, y firmaron el Manifiesto Ágil, declarando que estaban "descubriendo mejores maneras de desarrollar software." No conocimiento ni significado. Un marco de manufactura aplicado al software.
El propio lenguaje del Manifiesto Ágil es explícito sobre su alcance, y sin embargo la industria ha aplicado la metodología ágil a prácticamente todo lo que existe en un entorno tecnológico — incluyendo el trabajo del conocimiento, los sistemas semánticos y ahora la inteligencia artificial. Y en el mismo aliento, organizaciones y expertos colman la industria con discursos sobre innovación y el surgimiento de un nuevo paradigma en las formas de trabajo. Sin embargo, esas formas de trabajo permanecen encerradas dentro de las metodologías ágiles y Scrum, diseñadas para la línea de producción y la base de código — no para el trabajo profundamente humano, recursivo y desordenado de construir significado a partir de la complejidad.
## Scrum es un calendario de manufactura
Volvamos al artículo de Takeuchi y Nonaka, cuyo título — "The New New Product Development Game" — describe cómo los equipos de producto en empresas japonesas desarrollaban bienes físicos como la copiadora FX-3500 y el Honda City. Los autores recurren a la metáfora del rugby porque el equipo "intenta recorrer la distancia como una unidad, pasándose el balón de un lado al otro." La unidad de entrega es un prototipo funcional de un artefacto material. El ciclo de retroalimentación es una máquina que funciona o falla.
El libro de Sutherland de 2014, *Scrum: The Art of Doing Twice the Work in Half the Time*, refuerza esto con casos de estudio militares y de manufactura como la producción del caza F-86 y Toyota. El libro reconstruye el argumento sobre la misma premisa: que aquello que se construye tiene partes discretas, inspeccionables y verificables que, una vez terminadas, están terminadas.
La Guía Scrum de 2020 define el Incremento como "un peldaño concreto hacia el Objetivo del Producto", uno que es "aditivo respecto a todos los Incrementos anteriores y rigurosamente verificado, asegurando que todos los Incrementos funcionen en conjunto." Estas palabras tienen sentido si se está construyendo una aplicación móvil, con una pantalla de inicio de sesión y una página de configuración. Pero para los sistemas de conocimiento, estas palabras son destructivas, porque los componentes de una arquitectura semántica no son aditivos de la manera en que Scrum prescribe. Un vocabulario controlado no es un subcomponente de una taxonomía del mismo modo en que una pantalla de inicio de sesión lo es de una aplicación. Una taxonomía sin un control de vocabulario disciplinado como fundamento no funciona bien, o directamente no funciona. No se puede entregar media taxonomía y llamarla un Incremento.
Scrum asume problemas dóciles que pueden enmarcarse en el tiempo, estimarse con planning poker, descomponerse en historias de usuario, rastrearse en un gráfico de velocidad y declararse resueltos frente a una lista de verificación. Si tan solo el conocimiento fuera así de dócil. La organización del conocimiento está muy lejos de serlo. Con más de cincuenta años de antigüedad, en 1973 Horst Rittel y Melvin Webber advirtieron que aplicar el paradigma de la ingeniería de manera uniforme a todos los problemas estaba "condenado al fracaso." Y sin embargo aquí estamos, aplicando un calendario de manufactura y sus metodologías al conocimiento, esperando que el conocimiento quepa en los confines de viejas formas de trabajar — un artefacto de la Tercera Revolución Industrial impuesto sobre las exigencias de la Cuarta.
En 2016, Klaus Schwab escribió sobre este patrón histórico. La Primera Revolución Industrial, que comenzó alrededor de 1760, nos dio el vapor y la producción mecánica. La Segunda, a finales del siglo XIX, nos dio la electricidad y la cadena de montaje — el mundo que produjo Toyota, la manufactura lean y la lógica de producción que Scrum aún lleva como ethos central. La Tercera, que comenzó en la década de 1960, nos dio los semiconductores, la computación mainframe y finalmente internet — el mundo en que nació Agile, por desarrolladores de software, para el desarrollo de software. La Cuarta Revolución Industrial, argumentó Schwab, es fundamentalmente diferente en su naturaleza, pues está definida por la fusión de la inteligencia artificial, los sistemas ciberfísicos y las tecnologías del conocimiento que difuminan las fronteras entre los mundos físico, digital y biológico.
Los problemas de esta era no son necesariamente problemas de ingeniería con requisitos estables. Estamos lidiando con preguntas como de qué manera anclar la inteligencia artificial en el significado, cómo organizar el conocimiento en evolución y cómo construir sistemas capaces de explicarse a sí mismos. Estos son problemas retorcidos que exigen nuevas epistemologías y marcos para la resolución de problemas. Agile puede haber sido la metodología correcta para la Tercera Revolución Industrial, pero ya no estamos en ella. Los marcos no se han puesto al día, y el coste de ese rezago se agrava cada vez que una organización impone los principios de la Tercera Revolución Industrial al trabajo de la Cuarta.
En otras palabras, esto no es un problema de producto ni de plataforma — es el desafío de aprender a abrazar nuevas formas de trabajar. Es un problema de infraestructura gobernado por la epistemología: una filosofía fundamental sobre el conocimiento.
## Por qué el conocimiento no puede ejecutarse en sprints
Una ontología es, según la definición fundacional de Thomas Gruber, "una especificación de una conceptualización." Una conceptualización es un todo, no partes de un todo. Especificar la mitad produce incoherencia lógica, no un producto mínimamente viable. Por ejemplo, la norma ANSI/NISO Z39.19 para la construcción de vocabularios controlados exige resolver la sinonimia, la homonimia y el alcance antes de que los términos entren en producción, porque cualquier inconsistencia se propaga hacia abajo en cada uso posterior. SKOS, el estándar del W3C para codificar sistemas de organización del conocimiento, requiere que las relaciones jerárquicas formen estructuras coherentes con cierre transitivo, y OWL 2 exige que los axiomas de clase sean satisfacibles. Estas son precondiciones necesarias para que los sistemas lógicos razonen y expresen significado.
En la práctica, he observado organizaciones intentando entregar sistemas de conocimiento de forma ágil en dos patrones, y ninguno termina bien. En el primero, el equipo celebra stand-ups, ceremonias y retrospectivas. El taxonomista trae tickets y añade quince términos al vocabulario del producto. Los tickets se cierran y el gráfico de velocidad sube. Nada se integra, porque la integración es la parte difícil y no cabe en dos semanas, de modo que va al backlog. Seis meses después, el vocabulario es una colección de términos con alcances inconsistentes, la ontología tiene clases huérfanas y el grafo de conocimiento — si existe — no puede responder consultas de manera confiable. Pero el burndown chart es impecable y el liderazgo está ciegamente satisfecho porque las casillas fueron marcadas.
En el segundo patrón, el equipo toma prestado de otras metodologías sin dejar de llamarlo ágil. La taxonomista conduce análisis de dominio y entrevistas con partes interesadas durante varias semanas — eso es etnografía. Realiza card sorting y modelado conceptual — eso es investigación en diseño. Itera con expertos en la materia — eso es diseño participativo. Finalmente entrega un modelo coherente. El ritmo del sprint fue irrelevante para el trabajo. Martin Fowler denominó este fenómeno en su conferencia magistral de 2018: "faux-agile" — el Complejo Industrial Ágil.
Ron Jeffries, otro signatario original del Manifiesto Ágil, les dijo a los desarrolladores ese mismo año, 2018, que abandonaran la palabra por completo. Dave Thomas declaró en 2014 que "Ágil" se había convertido en "un imán para cualquiera con puntos que exponer, horas que facturar o productos que vender." Cuando tres de los diecisiete autores de un marco dicen que ha sido vaciado de contenido, uno tiene que preguntarse si Agile es apropiado para todo el trabajo tecnológico y si ha llegado el momento de revisar las formas de trabajar.
## La innovación no cabe en un backlog
El problema más profundo es que Agile y Scrum nunca fueron diseñados para inspirar o fomentar la innovación. Fueron diseñados para entregar requisitos conocidos con eficiencia creciente. Todo el aparato de la Guía Scrum — product backlog, sprint planning, Definición de Hecho — presupone que el problema ha sido formulado por un product owner capaz de escribirlo en una historia de usuario. Pero el trabajo del conocimiento, como el diseño, es retorcido. La primera propiedad de un problema retorcido según Rittel y Webber es que "No existe una formulación definitiva." La segunda propiedad establece que no hay regla de parada. Y la quinta propiedad ilumina que cada solución es una "operación de un solo disparo" porque no hay oportunidad de aprender por ensayo y error sin consecuencias.
Dada la naturaleza de los problemas retorcidos, parecería que Agile está mal equipado para enfrentarlos. Y por la misma razón, mal equipado para la naturaleza dinámica del conocimiento, donde las infraestructuras de conocimiento nunca son operaciones de un solo disparo, requieren reglas de parada y desafían sin lugar a dudas las formulaciones definitivas.
Por lo tanto, la organización del conocimiento en un dominio empresarial es retorcida exactamente en este sentido. El problema de cómo representar "cliente" en el modelo semántico de una empresa de telecomunicaciones no tiene una respuesta correcta preexistente. Su formulación depende de quién hace qué con esa representación, y eso cambia a medida que se construye. El alcance del vocabulario se desplaza conforme se entrevista a las partes interesadas. La taxonomía se reorganiza al encontrar casos límite. Los axiomas de la ontología se tensan o aflojan cuando el razonador descubre contradicciones. No hay que equivocarse — esto no se pliega al principio ágil de bienvenida a los requisitos cambiantes, porque los requisitos se constituyen *a través* del trabajo. El problema está en construcción, evolucionando frecuentemente, durante su resolución.
Las organizaciones que gestionan todo el trabajo a través de Scrum están, en efecto, optimizando para la previsibilidad en dominios que exigen exploración. Los resultados inhiben la innovación y adoptan la apariencia de productividad. Los gráficos de velocidad suben mientras la arquitectura intelectual subyacente se aplana en entregas fragmentadas, desconectadas del continuo que es el conocimiento.
## El pensamiento de diseño pertenece al pantano
Donald Schön trazó la distinción de que "En la variada topografía de la práctica profesional, hay una meseta alta y firme donde los profesionales pueden hacer uso efectivo de la teoría y la técnica basadas en la investigación, y hay unas tierras bajas pantanosas donde las situaciones son 'enredos' confusos incapaces de solución técnica." Agile vive en la meseta mientras que el trabajo del conocimiento vive en el pantano. El término de Schön para describir cómo los profesionales competentes navegan el pantano fue "reflexión-en-acción" — la recalibración iterativa y tácita que ocurre mientras el trabajo transcurre.
El pensamiento de diseño, en el linaje de Herbert Simon, Richard Buchanan y Schön, es la metodología del pantano. Asume que el problema está en construcción. Sus cinco fases — empatizar, definir, idear, prototipar, probar — no son un flujo lineal sino un proceso recursivo e iterativo que espera que la definición del problema cambie a medida que la comprensión se profundiza.
La investigación en el *Handbook of Human-Centered Artificial Intelligence* confirma esta alineación — integrar el pensamiento de diseño en el desarrollo de IA hace que los sistemas sean más centrados en el usuario, iterativos e impactantes, enfatizando la gobernanza de datos, la transparencia, la explicabilidad y la mitigación del sesgo. La Tepper School of Business de Carnegie Mellon University imparte ahora un programa dedicado a pensamiento de diseño con IA, argumentando que la combinación aborda "problemas complejos y sistémicos" que los marcos lineales no pueden gestionar con éxito. La práctica de diseño empresarial de IBM, desarrollada a lo largo de dos décadas, se ha aplicado en compromisos con clientes precisamente porque mantiene la experiencia humana en el centro mientras la tecnología evoluciona por debajo — el pensamiento de diseño es "atemporal", mientras que la tecnología está "fechada".
Un estudio de 2026 en *Scientific Reports* examinó la relación entre el pensamiento de orden superior, el uso de chatbots de IA generativa y la creatividad en ingeniería, encontrando que el pensamiento de orden superior tenía una importancia significativamente mayor para los resultados creativos que el uso de herramientas de IA por sí solo — reforzando que la profundidad cognitiva, no la velocidad de las herramientas, impulsa la innovación. La investigación en las *Proceedings of the Design Society* argumenta además que la IA generativa está desplazando el rol del diseñador de ejecutor a configurador de paradigmas, haciendo que el pensamiento de diseño sea más esencial a medida que la IA gestiona las tareas de producción mientras los humanos deben guiar el encuadre, la ética y el significado.
Para ser claros, estos marcos no son distintos sabores de la misma metodología. Scrum y el pensamiento de diseño representan epistemologías completamente diferentes. Scrum asume que el problema te fue entregado, mientras que el pensamiento de diseño asume que el problema debe encontrarse y lidiarse con él.
## ¿Es la IA una herramienta de conocimiento?
Esto nos lleva a la pregunta que la industria evita y que yo, por mi parte, llevo tiempo deseando formular. ¿Es la IA una herramienta de conocimiento? ¿Cómo se clasifica?
Un modelo de lenguaje de gran escala genera secuencias plausibles de texto. Volviendo al artículo de Bender, Gebru, McMillan-Major y Mitchell mencionado anteriormente, los autores definieron un modelo de lenguaje como un sistema para ensamblar formas lingüísticas según patrones probabilísticos, "sin ninguna referencia al significado." Sin referencia al significado, dado que los LLMs sin configuración adicional carecen de anclaje externo. Optimizan para la plausibilidad, no para la verdad, y por lo tanto, como sistemas autónomos, pueden clasificarse mejor como herramientas de procesamiento de información.
Se convierte en una herramienta de conocimiento únicamente cuando se ancla en una capa semántica — vocabularios controlados, taxonomías, ontologías, grafos de conocimiento — que proporciona referencia y procedencia. Las propias medidas correctivas de la industria y el mercado asociado confirman esto como un hecho. La generación aumentada por recuperación, los grafos de conocimiento para LLMs y lo que el discurso reciente denomina "ingeniería de contexto" son, bajo el empaquetado, la infraestructura semántica que debería haberse construido primero. Pero fue añadida a posteriori para prevenir alucinaciones que estaban prácticamente garantizadas desde el momento en que el modelo fue desplegado sin ella.
El artículo sobre RAG de 2020 señaló este problema, identificando que la memoria paramétrica es opaca y no puede actualizarse, y que la recuperación desde fuentes estructuradas es el modo en que la procedencia se captura y codifica. Pero parece que la industria tiene un problema de memoria, o el mensaje llegó demasiado pronto para que la mayoría pudiera escucharlo.
Irónicamente, la procedencia es un elemento central de la gestión del conocimiento y el trabajo ontológico, pues constituye un principio fundacional de la disciplina y su espacio de problemas. Sin procedencia, el conocimiento no puede existir. Las organizaciones están descubriendo ahora que la necesitan, porque sus sistemas generativos no pueden explicarse a sí mismos. Llámalo linaje, llámalo trazabilidad. Pero si construyes para el conocimiento, llámalo procedencia, porque la procedencia abarca trazas, linaje, citaciones, lo temporal y lo espacial.
De modo que si la IA generativa es una herramienta de procesamiento de información, sostengo que para que la IA se convierta en una herramienta de conocimiento debe haber procedencia y lógica descriptiva. Y para ser francos, nadie llega a la procedencia haciendo sprints — esta se arquitecta y se sostiene.
## El conocimiento es una espiral, no un sprint
La ironía no pasa desapercibida en este momento. El mismo Nonaka que coescribió el artículo de 1986 que inspiró Scrum desarrolló el modelo SECI de creación de conocimiento en 1995, describiendo el conocimiento como una espiral entre formas tácitas y explícitas, que avanza a través de la socialización, la externalización, la combinación y la internalización.
La espiral no tiene fin, como es propio de las espirales, emblema de la vida. Cada rotación crea nuevo conocimiento que se convierte en el sustrato de la siguiente. No existe una Definición de Hecho porque la definición de lo que se está haciendo está siendo construida en sí misma. Scrum tomó la estructura de equipo del primer artículo de Nonaka y descartó su siguiente década de trabajo porque no encajaba limpiamente en Scrum. Los mecanismos del trabajo del conocimiento, como Nonaka argumentó durante una década, son la metáfora, la habilidad tácita, el aprendizaje por proximidad, el espacio compartido y la paciente conversión de intuiciones en modelos y de modelos de vuelta en práctica.
El Ontology Pipeline™ refleja esta espiral. Se comienza con un vocabulario controlado — un proceso disciplinado que implica acuerdos sobre lo que significan los términos. Se madura hacia una taxonomía introduciendo jerarquía y estructurando los términos en relaciones padre-hijo. Posteriormente, la taxonomía se madura hacia un tesauro añadiendo relaciones asociativas y sinonimia, codificadas en SKOS. El tesauro es el trabajo base a partir del cual se desarrolla una ontología, incorporando semántica formal mediante OWL y RDF. Se arquitecta un grafo de conocimiento, ensamblando componentes que incluyen los activos de conocimiento desarrollados a través del marco iterativo del Ontology Pipeline™.
Cada etapa prepara el terreno para la siguiente y las etapas no pueden saltarse ni eludirse. No se puede entregar el grafo de conocimiento sin una ontología, y la ontología debe ser lógicamente consistente y coherente para dotar de sentido a los datos. Un grafo de conocimiento sin control de vocabulario arriesga convertirse en un mentiroso convincente o en fallar al trabajar con lenguaje natural. Porque los activos de conocimiento son dimensionalmente interdependientes, el conocimiento es efectivamente una espiral, incapaz de ser contenida por la lógica de la manufactura.
## El patrón y el remedio
Las organizaciones presupuestan para plataformas — Confluence, SharePoint, suscripciones a LLMs, bases de datos vectoriales, un proveedor de RAG. Pero no presupuestan para la lógica descriptiva y el conocimiento, quizás porque el problema no se resuelve con una sola herramienta y resulta demasiado amorfo en un mundo centrado en los datos y en Agile. El vocabulario controlado se recorta a menos que genere valor inmediato para la base de datos. Con frecuencia, la taxonomía se recorta porque no hace una buena demostración y su valor reside en la fontanería, entregando su impacto con el tiempo. El resultado es una pila siempre creciente de documentos con un chatbot pegado en la entrada, respondiendo preguntas con confianza en la voz de un sistema que no tiene referencia para ninguno de los términos dispersos a lo largo de los documentos.
Entonces el sistema alucina. Se contradice a sí mismo entre sesiones. Presenta como vigente una política desactualizada y muestra mensajes privados de Outlook de empleados cuyas cuentas fueron desactivadas. He visto esto en la vida real más veces de las que puedo contar. Y la percepción es peor cuando una herramienta de proveedor es responsable de revelar chats y transacciones de correo privadas. Habitualmente, el patrocinador ejecutivo pregunta por qué la inversión no ha funcionado, ignorando la evidencia de los errores, como la exposición de comunicaciones privadas. Trabajo de un día cualquiera, cuando los burndown charts y la velocidad marcan el precedente de las formas de trabajar y los sistemas de recompensa.
Los ejecutivos ignoran el hecho de que el sistema nunca fue anclado en el conocimiento. Nunca lo fue porque anclar requiere el trabajo lento, costoso y disciplinado de la organización del conocimiento — trabajo que no cabe en un sprint, que no hace una buena demostración y que no genera un gráfico de velocidad. La organización elige eludir la inversión en el aparato de conocimiento descriptivo porque las líneas son demasiado difusas para definirse claramente según el vocabulario de Agile. Porque el conocimiento es infraestructura, no un producto ni una plataforma. El conocimiento es la gasolina del depósito, el combustible del significado, el material que justifica el razonamiento lógico.
El pensamiento de diseño le da al trabajo del conocimiento un hogar. Es cómodo con la ambigüedad. Trata el descubrimiento del problema como un igual a la resolución del problema. Desde luego no exige un incremento entregable cada catorce días. En cambio, el pensamiento de diseño exige que entiendas lo que estás construyendo antes de construirlo — y que sigas revisando esa comprensión a medida que el trabajo avanza. La biblioteconomía y la ciencia de la información, con más de un siglo de estándares para la organización del conocimiento, proporcionan los métodos mientras el pensamiento de diseño proporciona la epistemología. Juntos ofrecen lo que Agile nunca pudo — un marco adecuado a la naturaleza del conocimiento mismo.
¿Un sprint de dos semanas para entregar qué? Si estás construyendo sistemas de conocimiento, dos semanas no producirán nada coherente. El conocimiento no se construye rápido y se moldea a la fuerza. Se construye con cuidado, con procedencia, gobernanza y paciencia. El conocimiento no es un producto que pueda manufacturarse y no — no se puede comprar conocimiento estándar que encaje perfectamente con un dominio u organización. El conocimiento es matizado, específico a cada organización, dominio y persona.
En el espíritu del argot del capital de riesgo: el conocimiento es el foso, y ese foso no es Ágil.
---
*Jessica Talisman, MLS — Intentional Arrangement*

View file

@ -0,0 +1,118 @@
# Extractions — Steal This Deck (Talisman, KGC 2026)
Source: Jessica Talisman, "Stop Betting, Start Building", Knowledge Graph Conference 2026.
Newsletter: Intentional Arrangement (Substack). Received: 9 May 2026.
Full deck: `Steal_This_Deck.pdf`
---
## Framing / Intro hooks
**Para abrir cualquier presentación de ontoref:**
> *"AI is a knowledge tool. Not a data tool. That breaks every assumption underneath many AI strategies."*
Adaptado a ontoref: los agentes AI que trabajan sobre tu repositorio operan sobre correlación estadística si no hay infraestructura de conocimiento. Ontoref es esa infraestructura, pero para proyectos de software.
> *"Agentic AI is not a model upgrade away. It is a shared language, from which to act."*
Este cierre del deck es el mejor hook de intro para ontoref: un modelo más grande no sabe qué es tu proyecto, qué decisiones tomaste, ni en qué estado está. Eso requiere conocimiento estructurado, no un token window mayor.
---
## Datos citable para el problema
Todos con fuente verificable — útiles en posts y slides sin necesitar justificación propia:
| Dato | Fuente |
|---|---|
| 89% de empresas reportan **cero impacto de productividad** de AI en 3 años | NBER, Feb 2026, n=5.937 execs |
| Developers experimentados fueron **19% más lentos** con herramientas AI | METR RCT, 2025 |
| Ganancia neta semanal del trabajador AI-promedio: **14 minutos** | Foxit/Sapio, March 2026 |
| 1.7× más issues en PRs escritos por AI | CodeRabbit, 2025 |
| 46% del código en GitHub escrito por Copilot | Octoverse 2025 |
**El argumento que emerge:** *AI is not saving time. It is generating volume. Without a knowledge backbone, its only measurable output is noise.*
---
## El argumento central — para slides y posts
**El stack de conocimiento (Talisman lo llama "knowledge infrastructure"):**
```
1. Controlled vocabularies — términos con significado único y autoritativo
2. Taxonomies — organización jerárquica
3. Thesauri — equivalencias, relaciones cruzadas
4. Ontologies — compromisos formales: clases, propiedades, constraints
5. Knowledge graphs — live, queryable, governable
```
> *"You cannot skip layers. You cannot start at five and reverse-engineer to one."*
**Ontoref implementa este stack para proyectos de software:**
- Schemas NCL → vocabularios controlados
- `.ontology/core.ncl` → nodos Practice/Concept con edges tipados
- DAG-formalized knowledge → el knowledge graph
- ADRs + migrations + `state.ncl` → governance
---
## El argumento de precisión — para posts técnicos
> *"The accuracy gap is not closed by a bigger model. It is closed by a defined schema, an ontology, and a validated query."*
Con datos:
- **16% → 72%** en question-answering sobre SQL enterprise con ontology checks (Allemang & Sequeda, data.world AI Lab, 2024)
- **3.4×** GraphRAG vs vector RAG en 43 queries enterprise (Diffbot KG-LM Benchmark, 2023)
- Vector RAG colapsa a **0% past 5 entities per query**. KG-grounded retrieval sostiene.
> *"Defining your terms is the cheapest accuracy and cost lever in the LLM stack."*
Ontoref es exactamente esto: `ontoref describe` y el sistema Q&A (ADR-003) son el vocabulario controlado que reduce el espacio de alucinación cuando un agente trabaja sobre el proyecto.
---
## El argumento de MCP — crítico para el posicionamiento técnico
> *"MCP and A2A are transport. Not semantics. They move bytes between endpoints. They do not establish shared meaning."*
>
> *"Two agents connected by MCP exchange text bytes. They do not truly share context."*
**Ontoref cierra este gap:** tiene superficie MCP (`ontoref-daemon/src/mcp/`) encima de una capa ontológica. El protocolo mueve bytes; ontoref provee el significado que hace que esos bytes sean conocimiento, no texto.
Esto es diferenciación directa respecto a "simplemente exponer tu repo por MCP".
---
## El argumento cultural — para posts de opinión
> *"The industry is optimized to ship solutions, not to own problems."*
| Celebrado | Huérfano |
|---|---|
| Launching new platforms | Maintaining existing platforms |
| AI-generated content | Taxonomy work |
| Ontologies built on the fly | Domain expertise |
> *"Knowledge work IS the maintenance. Yet it keeps getting cut."*
Ontoref formaliza exactamente lo que siempre se corta: las decisiones arquitectónicas (ADRs), el estado del proyecto (`state.ncl`), la memoria operacional (`.coder/`). Lo hace queryable y machine-readable para que no dependa de que alguien lo recuerde.
---
## El close — para cualquier formato
> *"The organizations that close the perception/reality gap first won't be the ones with the best models. They'll be the ones who finally did the work."*
Adaptado: los proyectos donde los agentes AI trabajan mejor no son los que tienen el modelo más grande. Son los que tienen conocimiento estructurado sobre sí mismos.
---
## Atribución
Jessica Talisman, MLS — Semantic Engineer, Information Architect, Knowledge Infrastructure Strategist.
Newsletter: *Intentional Arrangement* (Substack).
Talk: "Stop Betting, Start Building", Knowledge Graph Conference 2026, Technology Track, May 6, 2026.
Al usar cualquiera de estos puntos en público, citar la fuente — es un argumento de autoridad que refuerza, no debilita, la posición de ontoref.

View file

@ -0,0 +1,989 @@
---
theme: default
title: Ontología y Reflection — lo que la IA no puede entender por ti
titleTemplate: '%s - OntoRef'
layout: cover
keywords: Ontología,Reflection,IA,Infraestructura,OntoRef,Nushell,Nickel
download: true
exportFilename: ontoref-openspace
monaco: true
remoteAssets: true
selectable: true
record: true
colorSchema: dark
lineNumbers: false
themeConfig:
primary: '#f74c00'
logoHeader: '/ferris.svg'
fonts:
mono: 'Victor Mono'
drawings:
enabled: true
persist: false
presenterOnly: false
syncAll: true
scripts:
- setup/image-overlay.ts
class: 'justify-center flex flex-cols'
---
<h1 class="font-medium text-3xl leading-tight">La IA gestiona tu infra...<br><span class="text-orange-400">¿Sabes lo que está pasando?</span></h1>
<div class="flex justify-center mt-6 text-gray-400 text-lg italic">
Ontología y Reflection como base de un proyecto que se entiende a sí mismo.
</div>
<div class="flex justify-center mt-8">
<img class="w-40" src="/jesusperez_w.svg">
</div>
<style scoped>
h1, h2, p { z-index: 10; }
</style>
<Footer />
<!--
Abrir con pausa. El título es una pregunta directa.
No es retórica — la mayoría no sabe. Y el punto es exactamente ese.
Esta charla no es sobre herramientas. Es sobre qué pasa cuando externalizas comprensión.
-->
---
layout: default
---
# El problema no es la herramienta
<div class="grid grid-cols-2 gap-8 mt-4">
<div>
**Lo que la IA hace bien**
<div class="mt-3 space-y-3">
<div class="flex items-start gap-2">
<span class="text-green-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300 text-sm">Genera YAML correcto sintácticamente</div>
</div>
<div class="flex items-start gap-2">
<span class="text-green-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300 text-sm">Completa Terraform que nunca rompe el plan</div>
</div>
<div class="flex items-start gap-2">
<span class="text-green-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300 text-sm">Rellena gaps de configuración con defaults razonables</div>
</div>
<div class="flex items-start gap-2">
<span class="text-green-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300 text-sm">Traduce intención en forma declarativa</div>
</div>
</div>
</div>
<div>
<v-click>
**Lo que la IA no puede hacer**
<div class="mt-3 space-y-3">
<div class="flex items-start gap-2">
<span class="text-red-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300 text-sm">Saber <em>por qué</em> ese servicio no puede escalar a N réplicas</div>
</div>
<div class="flex items-start gap-2">
<span class="text-red-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300 text-sm">Conocer la tensión que existe entre latencia y consistencia en <em>tu</em> sistema</div>
</div>
<div class="flex items-start gap-2">
<span class="text-red-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300 text-sm">Recordar la decisión que tomaste en diciembre y por qué descartaste la alternativa</div>
</div>
<div class="flex items-start gap-2">
<span class="text-red-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300 text-sm">Entender qué no puede romperse sin coordinación entre equipos</div>
</div>
</div>
</v-click>
<v-click>
<div class="mt-6 border-l-4 border-orange-500 pl-4 text-sm text-gray-300">
Declarativo ≠ comprensión.<br>
Describes el estado deseado, no el <em>por qué</em>.
</div>
</v-click>
</div>
</div>
<Footer />
<!--
La IA genera forma correcta para el problema equivocado.
El YAML válido es el problema mínimo. El problema real es que nadie sabe qué asumió.
-->
---
layout: default
---
# Las 2am — cuando el Terraform no habla
<div class="grid grid-cols-2 gap-8 mt-4">
<div>
**Lo que ves cuando falla**
<div class="mt-3 font-mono text-xs text-gray-300">
```text
Error: Error creating Instance:
googleapi: Error 400: Invalid value
for field 'resource.machineType'
# El plan pasó. El apply falló.
# ¿Qué asumió el agente?
# ¿Qué región eligió? ¿Por qué ese tipo?
# ¿Hay una restricción de org que no sabía?
```
</div>
<v-click>
<div class="mt-4 font-mono text-xs text-gray-400">
```yaml
# Lo que generó la IA (hace 6 semanas)
resource "google_compute_instance" "api" {
machine_type = "e2-medium" # "razonable"
zone = "us-central1-a" # "por defecto"
}
# Lo que tu política de org requiere
# → solo europe-west1, solo n2-standard-*
# → la IA no lo sabía porque tú tampoco lo escribiste
```
</div>
</v-click>
</div>
<div>
<v-click>
**El patrón que se repite**
<div class="mt-3 space-y-3 text-sm">
<div class="border-l-2 border-red-500 pl-3">
<div class="text-red-300 font-semibold">Sin modelo explícito</div>
<div class="text-gray-400">La IA inventa contexto plausible. Tú asumes que sabe. Falla en prod.</div>
</div>
<div class="border-l-2 border-orange-500 pl-3">
<div class="text-orange-300 font-semibold">Con Stack Overflow</div>
<div class="text-gray-400">Copias código sin entender el supuesto. Al menos tú lo buscaste.</div>
</div>
<div class="border-l-2 border-yellow-500 pl-3">
<div class="text-yellow-300 font-semibold">Con la IA</div>
<div class="text-gray-400">El agente genera con confianza. La confianza es contagiosa. El error también.</div>
</div>
</div>
</v-click>
<v-click>
<div class="mt-6 text-base font-semibold text-gray-200 border-l-4 border-orange-500 pl-4">
La diferencia con Stack Overflow:<br>
<span class="text-gray-400 font-normal text-sm">Al menos ahí buscabas activamente.<br>Con la IA, delegas también la búsqueda.</span>
</div>
</v-click>
</div>
</div>
<Footer />
<!--
Este slide es el que el público siente. Todos tienen una historia de "la IA lo generó y pareció bien".
La pregunta es: ¿cuándo lo descubriste? ¿Staging? ¿Prod? ¿6 meses después?
-->
---
layout: default
---
# Tu proyecto vive en tu cabeza
<div class="grid grid-cols-2 gap-8 mt-4">
<div>
**Lo que vive solo en tu cabeza**
<div class="mt-3 space-y-2 text-sm text-gray-300">
<div class="flex items-start gap-2">
<span class="text-orange-400 text-sm mt-1"></span>
<div>Por qué este servicio no puede tener más de 3 réplicas</div>
</div>
<div class="flex items-start gap-2">
<span class="text-orange-400 text-sm mt-1"></span>
<div>Qué pasó cuando intentamos escalar en enero y por qué fallamos</div>
</div>
<div class="flex items-start gap-2">
<span class="text-orange-400 text-sm mt-1"></span>
<div>La tensión entre el equipo de plataforma y el de producto sobre quién controla el networking</div>
</div>
<div class="flex items-start gap-2">
<span class="text-orange-400 text-sm mt-1"></span>
<div>Qué alternativa se rechazó y por qué</div>
</div>
<div class="flex items-start gap-2">
<span class="text-orange-400 text-sm mt-1"></span>
<div>Qué invariante no puede violarse sin romper el acuerdo con el equipo de seguridad</div>
</div>
</div>
<v-click>
<div class="mt-5 border-l-4 border-red-500 pl-4 text-sm text-gray-400">
Lo que vive solo en tu cabeza <strong class="text-red-300">no se puede validar</strong>.<br>
No puede consultarlo un agente.<br>No puede consultarlo un compañero nuevo.<br>
No puedes consultarlo tú mismo en 6 meses.
</div>
</v-click>
</div>
<div>
<v-click>
**La trampa del README**
<div class="mt-3 font-mono text-xs text-gray-400">
```markdown
# Mi Proyecto
Este servicio hace X. Está desplegado en GCP.
Usa Terraform. Tiene 3 entornos: dev, staging, prod.
## Arquitectura
Ver diagrama (desactualizado desde marzo).
## Decisiones
- Elegimos PostgreSQL porque MySQL no nos gustaba.
- Usamos gRPC internamente.
```
</div>
<div class="mt-4 space-y-2 text-sm text-gray-400">
<div class="flex items-start gap-2">
<span class="text-red-400 text-sm"></span>
<div>No dice qué no puede cambiar</div>
</div>
<div class="flex items-start gap-2">
<span class="text-red-400 text-sm"></span>
<div>No captura las alternativas rechazadas ni el por qué</div>
</div>
<div class="flex items-start gap-2">
<span class="text-red-400 text-sm"></span>
<div>No es consultable ni verificable — es prosa</div>
</div>
<div class="flex items-start gap-2">
<span class="text-red-400 text-sm"></span>
<div>No refleja el estado actual — <br> refleja el estado de cuando se escribió</div>
</div>
</div>
</v-click>
</div>
</div>
<Footer />
<!--
La pregunta clave que lanzar al público: ¿cuándo fue la última vez que actualizaste tu README?
Un README es documentación. Una ontología es un modelo vivo del sistema.
-->
---
layout: center
---
# La tesis
<div class="mt-8 text-center space-y-6">
<div class="text-3xl font-semibold text-gray-100">
La ontología no es para la IA.
</div>
<div class="text-3xl font-semibold text-orange-400">
Es para ti.
</div>
<v-click>
<div class="mt-8 text-lg text-gray-400 max-w-2xl mx-auto leading-8">
Formalizar invariantes, tensiones y estado actual<br>
te fuerza a saber qué es verdad <em>ahora mismo</em>.
</div>
</v-click>
<v-click>
<div class="mt-6 text-lg text-gray-400 max-w-2xl mx-auto leading-8">
La IA se beneficia como efecto secundario.<br>
Opera sobre lo que <em>tú ya clarificaste</em>.
</div>
</v-click>
<v-click>
<div class="mt-8 text-base text-gray-500 max-w-2xl mx-auto border-t border-gray-700 pt-6">
El valor real: el proceso de <strong class="text-gray-300">escribir la ontología</strong><br>
es donde te aclaras tú, no la IA.
</div>
</v-click>
</div>
<Footer />
<!--
Pausa larga después del primer par de líneas. Dejar que aterrice.
La gente espera que digas "la IA necesita contexto". La inversión es el punto.
-->
---
layout: default
---
# Ontología ≠ lista de decisiones
<div class="grid grid-cols-2 gap-8 mt-4">
<div>
**Una lista de decisiones**
<div class="mt-3 font-mono text-xs text-gray-400">
```text
- Elegimos Kubernetes
- Usamos Helm para despliegues
- PostgreSQL como base de datos primaria
- gRPC entre servicios internos
- OAuth2 para autenticación externa
```
</div>
<div class="mt-4 text-sm text-gray-500">
Captura el <em>qué</em>. No captura el <em>por qué</em>,<br>
las alternativas rechazadas, ni las consecuencias.
</div>
</div>
<div>
<v-click>
**Una ontología**
<div class="mt-3 space-y-2 text-sm">
<div class="border-l-4 border-blue-400 pl-3 py-1 bg-gray-900 rounded-r">
<div class="text-blue-300 font-mono text-xs font-semibold">INVARIANTES</div>
<div class="text-gray-400 text-xs">Lo que no puede cambiar sin decisión explícita y coordinación</div>
</div>
<div class="border-l-4 border-orange-400 pl-3 py-1 bg-gray-900 rounded-r">
<div class="text-orange-300 font-mono text-xs font-semibold">TENSIONES ACTIVAS</div>
<div class="text-gray-400 text-xs">Trade-offs que aún no se han resuelto — viven en el sistema ahora mismo</div>
</div>
<div class="border-l-4 border-purple-400 pl-3 py-1 bg-gray-900 rounded-r">
<div class="text-purple-300 font-mono text-xs font-semibold">ESTADO ACTUAL → DESEADO</div>
<div class="text-gray-400 text-xs">Dimensiones de madurez. Dónde estás, dónde quieres estar, qué te bloquea</div>
</div>
<div class="border-l-4 border-green-400 pl-3 py-1 bg-gray-900 rounded-r">
<div class="text-green-300 font-mono text-xs font-semibold">GATES ACTIVAS</div>
<div class="text-gray-400 text-xs">Condiciones que deben cumplirse antes de cruzar una frontera arquitectural</div>
</div>
</div>
</v-click>
<v-click>
<div class="mt-4 text-sm text-gray-300">
Es un grafo consultable. No prosa.<br>
<span class="text-gray-500 text-xs">La IA puede leerlo. Tu CI puede verificarlo. Tú puedes razonarlo.</span>
</div>
</v-click>
</div>
</div>
<Footer />
<!--
La diferencia no es el nivel de detalle — es la estructura.
Un grafo tiene propiedades que la prosa no tiene: es consultable, verificable, traversable.
-->
---
layout: default
---
# Ontoref — tres archivos, tres preguntas
<div class="grid grid-cols-3 gap-5 mt-6">
<div class="border border-gray-700 rounded p-4 bg-gray-900">
<div class="text-blue-300 font-mono font-semibold text-sm mb-3">core.ncl</div>
<div class="text-orange-400 text-xs mb-3 italic">"¿Qué ES este proyecto?"</div>
<div class="font-mono text-xs text-gray-400">
```text
invariantes:
- backend-agnostic-core
- zero-external-runtime
tensiones:
- formalization vs
adoption-friction
- ontology vs reflection
prácticas:
- nickel-as-schema-layer
- nushell-as-operator
```
</div>
<div class="mt-3 text-xs text-gray-600">
Inmutable sin ADR nuevo.
</div>
</div>
<div class="border border-gray-700 rounded p-4 bg-gray-900">
<div class="text-orange-300 font-mono font-semibold text-sm mb-3">state.ncl</div>
<div class="text-orange-400 text-xs mb-3 italic">"¿Dónde ESTAMOS?"</div>
<div class="font-mono text-xs text-gray-400">
```text
dimensiones:
backend-maturity:
actual: multi-stable
deseado: all-production
bloqueador:
missing-nats-tests
nickel-depth:
actual: schema-input
deseado: bidirectional
catalizador:
export pipeline
```
</div>
<div class="mt-3 text-xs text-gray-600">
Evoluciona con el proyecto.
</div>
</div>
<div class="border border-gray-700 rounded p-4 bg-gray-900">
<div class="text-green-300 font-mono font-semibold text-sm mb-3">gate.ncl</div>
<div class="text-orange-400 text-xs mb-3 italic">"¿Cuándo estamos LISTOS?"</div>
<div class="font-mono text-xs text-gray-400">
```text
gates:
core-dependency-gate:
activa: true
permeabilidad: Low
protege:
- backend-agnostic-core
nickel-primacy-gate:
activa: true
permeabilidad: Low
protege:
- nickel-as-schema-layer
```
</div>
<div class="mt-3 text-xs text-gray-600">
Condiciones de cruce.
</div>
</div>
</div>
<v-click>
<div class="mt-5 font-mono text-xs text-center text-gray-500">
Nickel tipado · exportable como JSON · consultable desde Nu · verificable en CI
</div>
</v-click>
<Footer />
<!--
Tres preguntas, tres responsabilidades. La separación es deliberada.
core.ncl responde al SER. state.ncl al DEVENIR. gate.ncl al CRUZAR.
Cada uno puede cambiar a ritmos diferentes sin contaminar los otros.
-->
---
layout: default
---
# Reflection — lo que se CONVIERTE
<div class="grid grid-cols-2 gap-8 mt-2">
<div>
**Ontología = lo que ES**
<div class="mt-3 text-sm text-gray-400 space-y-2">
<div>→ Invariantes — verdad estructural</div>
<div>→ Tensiones — trade-offs vivos</div>
<div>→ Estado — dónde estamos ahora</div>
</div>
<div class="mt-5 text-sm font-semibold text-gray-200">Reflection = lo que DEVIENE</div>
<div class="mt-3 text-sm text-gray-400 space-y-2">
<div>→ Modos operacionales — secuencias con DAG de dependencias</div>
<div>→ ADRs con constraints tipadas — decisiones vivas, no prosa</div>
<div>→ Backlog estructurado — tensiones → tareas</div>
<div>→ Historial de sesiones — quién hizo qué y cuándo</div>
</div>
<v-click>
<div class="mt-5 border-l-4 border-orange-500 pl-4 text-sm text-gray-300">
La ontología captura el <em>ser</em>.<br>
La reflection captura el <em>operar sobre ese ser</em>.
</div>
</v-click>
</div>
<div>
<v-click>
<div class="-mt-3 font-mono text-xs">
<b>Un modo como especificación ejecutable</b>
```bash
# reflection/modes/new_service.ncl
{
id = "new_service",
trigger = "When adding a new service boundary",
steps = [
{
id = "draft-adr",
actor = "developer",
action = "Draft ADR for service boundary decision",
},
{
id = "verify-invariants",
actor = "agent",
action = "Check no invariant is violated",
depends_on = [{ step = "draft-adr" }],
},
{
id = "seal-config",
actor = "developer",
action = "Apply initial config seal",
depends_on = [{ step = "verify-invariants" }],
},
],
}
```
</div>
<div class="mt-2 text-xs text-gray-500">
No es un runbook. Es un grafo acíclico tipado.<br>
La IA lo ejecuta. El sistema lo valida.
</div>
</v-click>
</div>
</div>
<Footer />
<!--
La dualidad ontología/reflection es la tensión central de ontoref.
No pueden fusionarse — tienen ritmos de cambio diferentes y audiencias diferentes.
La reflection sin ontología es procedimiento sin semántica. La ontología sin reflection es diagrama muerto.
-->
---
layout: default
---
# La IA como actor, no como oráculo
<div class="grid grid-cols-2 gap-8 mt-4">
<div>
**Sin ontología formalizada**
<div class="mt-3 font-mono text-xs">
```bash
# Lo que el agente recibe
"Ayúdame a escalar este servicio"
# Lo que el agente asume
- que puede añadir réplicas libremente
- que el estado actual es el deseado
- que no hay restricciones de org
- que la decisión de diciembre es irrelevante
# Lo que genera
resource "google_compute_instance" {
count = 5 # razonable, ¿no?
}
```
</div>
</div>
<div>
<v-click>
**Con ontología formalizada**
<div class="mt-3 font-mono text-xs">
```bash
# Lo que el agente recibe (SessionStart hook)
INVARIANT: max-replicas-per-zone = 2
TENSION: scaling vs consistency [unresolved]
CONSTRAINT (adr-003): no horizontal scaling
without explicit capacity review
STATE: backend-maturity = single-stable
→ desired: multi-stable
→ blocker: capacity-review-pending
# Lo que el agente puede hacer
- Preguntar si procede dado el bloqueador
- Generar la config dentro de los límites
- Indicar que el ADR debe revisarse antes
```
</div>
</v-click>
<v-click>
<div class="mt-4 border-l-4 border-green-500 pl-4 text-sm text-gray-300">
El agente opera sobre lo que tú ya clarificaste.<br>
<span class="text-gray-500 text-xs">Deja de inventarse contexto porque tú dejaste de tenerlo implícito.</span>
</div>
</v-click>
</div>
</div>
<Footer />
<!--
El SessionStart hook en ontoref inyecta el contexto ontológico al inicio de cada sesión.
El agente no es más inteligente — tiene más información real y menos que inventar.
La mejora no es de la IA. Es tuya: formalizaste algo que antes vivía solo en tu cabeza.
-->
---
layout: default
---
# ¿Quién mantiene esto actualizado?
<div class="mt-4 text-lg text-gray-400 text-center">La pregunta que siempre sale.</div>
<div class="grid grid-cols-2 gap-8 mt-6">
<div>
**El problema real con el README**
<div class="mt-3 space-y-2 text-sm text-gray-400">
<div class="flex items-start gap-2">
<span class="text-red-400"></span>
<div>Nadie tiene incentivo para actualizarlo —<br> no está en el path crítico</div>
</div>
<div class="flex items-start gap-2">
<span class="text-red-400"></span>
<div>No hay señal de cuando está desactualizado</div>
</div>
<div class="flex items-start gap-2">
<span class="text-red-400"></span>
<div>Actualizar requiere fricción de escritura libre</div>
</div>
<div class="flex items-start gap-2">
<span class="text-red-400"></span>
<div>No hay verificación — puede divergir silenciosamente</div>
</div>
</div>
</div>
<div>
<v-click>
**El diseño de ontoref**
<div class="mt-3 space-y-2 text-sm">
<div class="border-l-4 border-blue-400 pl-3 py-1 bg-gray-900 rounded-r">
<div class="text-blue-300 font-semibold text-xs">Pre-commit como sincronización forzada</div>
<div class="text-gray-400 text-xs"><code>nickel typecheck</code> en cada commit — la capa declarativa nunca queda rota</div>
</div>
<div class="border-l-4 border-orange-400 pl-3 py-1 bg-gray-900 rounded-r">
<div class="text-orange-300 font-semibold text-xs">Modos como procedimientos obligatorios</div>
<div class="text-gray-400 text-xs">Añadir un servicio ejecuta el modo — el ADR se escribe como parte del proceso</div>
</div>
<div class="border-l-4 border-purple-400 pl-3 py-1 bg-gray-900 rounded-r">
<div class="text-purple-300 font-semibold text-xs">Drift detection</div>
<div class="text-gray-400 text-xs"><code>sha256(nickel export)</code> — sabes cuándo el estado sellado difiere del actual</div>
</div>
<div class="border-l-4 border-green-400 pl-3 py-1 bg-gray-900 rounded-r">
<div class="text-green-300 font-semibold text-xs">ADRs como constraints, no prosa</div>
<div class="text-gray-400 text-xs">El constraint es machine-readable — el CI puede verificarlo, no solo leerlo</div>
</div>
</div>
</v-click>
<v-click>
<div class="mt-4 text-xs text-gray-500 border-l-2 border-gray-700 pl-3">
La ontología se mantiene porque es el path.<br>
No porque alguien sea disciplinado.
</div>
</v-click>
</div>
</div>
<Footer />
<!--
Esta es la objeción más honesta del público. Abrazarla, no esquivarla.
La respuesta no es "la gente debería ser más disciplinada".
La respuesta es: el sistema lo hace obligatorio, no opcional.
-->
---
layout: default
---
# Para el debate
<div class="grid grid-cols-2 gap-8 mt-6">
<div>
**Propuesta 1 — Ontología**
<div class="mt-4 space-y-4">
<div class="border border-gray-700 rounded p-3 bg-gray-900">
<div class="text-orange-300 text-sm font-semibold">¿No es suficiente con un buen README?</div>
<div class="text-gray-500 text-xs mt-1">¿Cuándo fue la última vez que el tuyo estaba al día?</div>
</div>
<div class="border border-gray-700 rounded p-3 bg-gray-900">
<div class="text-orange-300 text-sm font-semibold">¿Qué diferencia una ontología de una lista de decisiones?</div>
<div class="text-gray-500 text-xs mt-1">Estructura, consultabilidad, verificabilidad.</div>
</div>
<div class="border border-gray-700 rounded p-3 bg-gray-900">
<div class="text-orange-300 text-sm font-semibold">¿Quién mantiene esto cuando el proyecto cambia rápido?</div>
<div class="text-gray-500 text-xs mt-1">El diseño lo hace obligatorio, no opcional.</div>
</div>
</div>
</div>
<div>
**Propuesta 2 — Infra + IA**
<div class="mt-4 space-y-4">
<div class="border border-gray-700 rounded p-3 bg-gray-900">
<div class="text-orange-300 text-sm font-semibold">¿Es diferente a copiar Stack Overflow?</div>
<div class="text-gray-500 text-xs mt-1">En SO buscabas. Con la IA, delegas también la búsqueda.</div>
</div>
<div class="border border-gray-700 rounded p-3 bg-gray-900">
<div class="text-orange-300 text-sm font-semibold">¿Dónde está el mínimo de comprensión necesario?</div>
<div class="text-gray-500 text-xs mt-1">Saber qué no puede romperse. Sin eso, no tienes control.</div>
</div>
<div class="border border-gray-700 rounded p-3 bg-gray-900">
<div class="text-orange-300 text-sm font-semibold">¿Las plataformas internas resuelven esto o lo esconden?</div>
<div class="text-gray-500 text-xs mt-1">Una plataforma sin ontología solo mueve la ignorancia un nivel arriba.</div>
</div>
</div>
</div>
</div>
<Footer />
<!--
Open space: las mejores respuestas vendrán del público.
Tu rol aquí es provocar, no resolver. Lanza las preguntas y deja el silencio.
-->
---
layout: cover
name: end
class: 'justify-center flex flex-cols'
---
<div class="standalone-slide text-shadow-lg text-xl">
<div class="text-3xl font-semibold text-gray-100 mb-2">
La IA puede declarar tu infra.
</div>
<div class="text-3xl font-semibold text-orange-400 mb-8">
No puede entenderla por ti.
</div>
<v-click>
<div class="text-xl text-gray-300 leading-10 max-w-2xl mx-auto">
La ontología es el modelo que te fuerza a saber qué es verdad ahora.<br>
La reflection es la operación sobre ese modelo.<br>
La IA es el actor que opera <em>sobre lo que ya clarificaste</em>.
</div>
</v-click>
<v-click>
<div class="mt-10 text-2xl text-gray-200">
Gracias. ¿Preguntas?
</div>
<small class="mt-4 block text-gray-500">jesusperez.pro · ontoref.dev</small>
</v-click>
</div>
<style>
.standalone-slide {
text-align: center;
}
</style>
<Footer />
<!--
Cierre lento. Las tres líneas son la síntesis completa.
No terminar con "ontoref es la solución" — terminar con la inversión cognitiva.
El punto no es adoptar ontoref. El punto es tomar posesión de tu propio modelo.
-->

3
assets/presentation/jj_rad.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
pnpm run dev

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,761 @@
---
theme: default
title: "炼 lian-build"
titleTemplate: '%s — Ephemeral BuildKit Substrate'
layout: cover
keywords: Rust,BuildKit,CI,sccache,cargo-chef,lian-build,lamina
download: true
exportFilename: lian-build-presentation
monaco: true
remoteAssets: true
selectable: true
colorSchema: dark
lineNumbers: true
themeConfig:
primary: '#ce422b'
logoHeader: '/ferris.svg'
fonts:
mono: 'Victor Mono'
background: /jude-infantini-mI-QcAP95Ok-unsplash.jpg
class: 'justify-center flex flex-cols photo-bg'
---
<h1 class="absolute top-15 left-3/10 font-bold mt-3 text-5xl">lian-build</h1>
<h2 class="absolute top-30 left-1/10 font-medium my-11 text-2xl opacity-80">
Ephemeral BuildKit — from <code>docker build</code> to a substrate
</h2>
<div class="absolute top-57 left-2/10 text-sm opacity-60 font-mono">
BuildKit · cargo-chef · sccache · NATS · Nickel · lamina
</div>
<div class="absolute top-65 left-3/10"><img src="/lian-h.svg" width="420"></div>
<img class="absolute bottom-10 right-10 w-32" src="/ferris.svg">
<style scoped>
h1, h2, div { z-index: 10; }
code { background: rgba(206,66,43,0.2); padding: 0.1em 0.3em; border-radius: 3px; }
</style>
---
# The Problem
**Rust CI: 8 minutes cold. Every. Single. Build.**
<div>
### What happens today without a substrate
<div class="absolute right-2 top-4 w-110 box-highlight mt-2">
Every developer and CI pipeline reinvents the same wheel — and pays full price each time.
</div>
```
push → CI triggers
└─ docker build .
├─ FROM rust:latest # 1.8 GB pull
├─ COPY Cargo.toml Cargo.lock # layer invalidated
├─ RUN cargo build --deps # 46 min compiling serde, tokio…
├─ COPY src/ # always changes
└─ RUN cargo build # 24 min compiling your code
```
</div>
<div class="grid grid-cols-2 gap-6 mt-4">
<div>
### The compounding failures
- **Cache bust cascade**`Cargo.lock`<br> change invalidates every downstream layer
- **No cross-run reuse** — parallel PRs duplicate identical dep compilation
</div>
<div>
- **Registry pull cost** — base image re-pulled<br> if not pinned
- **OOM silent failure** — exit 137, no retry, <br> build marked failed
</div>
</div>
---
# Why Not `docker build`?
<div class="grid grid-cols-2 gap-6 mt-15">
<div>
### `docker build` limitations
```bash
# No runner control
docker build . # uses daemon defaults
# no VM sizing, no OOM retry
# No external cache injection
# --cache-from only reads local/registry layers
# Can't mount S3-backed sccache bucket
# No SSH forwarding into RUN steps
# (without BuildKit secret/SSH mounts)
# No structured events
# build started/finished = exit code only
```
</div>
<div>
<div class="box-highlight mt-11 text-sm">
<code>docker build</code> is a convenience wrapper. When you need <em>control</em>, you need BuildKit directly.
</div>
</div>
</div>
---
# Why Not `docker build`?
<div>
### What we actually need
| Need | `docker` | BuildKit |
|------|---------------|----------|
| Cache mounts | ✗ | `--mount=type=cache` |
| SSH into build | partial | `--mount=type=ssh` |
| Secret injection | ✗ | `--mount=type=secret` |
| Remote daemon | cumbersome | `buildctl --addr` |
| Structured output | exit code | `--progress=json` |
| Parallel stages | limited | LLB graph native |
</div>
---
# Why BuildKit
**BuildKit is not a build tool. It's a graph execution engine.**
```
Your Dockerfile ──► LLB (Low-Level Build) graph ──► parallel DAG execution
├─ content-addressed cache (every node keyed by its inputs)
├─ prunable: unchanged nodes cost nothing
├─ remote execution: daemon can run anywhere buildctld runs
└─ mount primitives: cache / secret / ssh / bind
```
<div class="grid grid-cols-3 gap-4 mt-4 text-sm">
<div class="border border-rust-orange/30 rounded p-3">
### Cache mounts
```dockerfile
RUN --mount=type=cache,\
target=/usr/local/cargo/registry \
cargo build
```
Registry downloads survive across builds *inside* the daemon.
</div>
<div class="border border-rust-orange/30 rounded p-3">
### Secret mounts
```dockerfile
RUN --mount=type=secret,\
id=sccache_creds \
SCCACHE_S3_USE_SSL=true \
cargo build
```
Credentials never written to layer.
</div>
<div class="border border-rust-orange/30 rounded p-3">
### Remote daemon
```bash
buildctl \
--addr ssh://runner:1234 \
build \
--frontend dockerfile.v0 \
--local context=. \
--output type=image,name=…
```
Daemon on ephemeral VM, client local.
</div>
</div>
---
# cargo-chef — Dependency Layer Surgery
**Problem:** any `src/` change busts the dependency compilation layer.
**cargo-chef solution:** separate the dependency graph compilation from your code.
```dockerfile
# Stage 1 — planner: extract dependency recipe (no actual compilation)
FROM rust:1.82 AS planner
RUN cargo install cargo-chef
COPY . .
RUN cargo chef prepare --recipe-path recipe.json # only Cargo.toml/Cargo.lock matter
# Stage 2 — cooker: compile all deps from the recipe (expensive, cached)
FROM rust:1.82 AS cooker
RUN cargo install cargo-chef
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json # deps compiled, layer stable
# Stage 3 — final: only your code compiles (fast, always runs)
FROM rust:1.82 AS final
COPY --from=cooker /app/target target
COPY --from=cooker $CARGO_HOME $CARGO_HOME
COPY . .
RUN cargo build --release
```
---
# cargo-chef — Dependency Layer Surgery
<div class="box-highlight mt-3 text-sm">
The <em>planner</em> stage is cheap — it only reads metadata.
The <em>cooker</em> layer is stable as long as <code>Cargo.lock</code> doesn't change.
Source changes never touch the cooker.
</div>
---
# sccache — Compiler-Level Cache
**cargo-chef** caches at the *crate dependency graph* level.<br>
**sccache** caches at the *individual compilation unit* level.
<div class="grid grid-cols-2 gap-6 mt-4">
```
cargo build
└─ rustc src/sizing.rs → .rlib
└─ sccache wraps rustc:
hash(source + flags + toolchain) → S3 lookup
HIT → download cached .rlib (seconds)
MISS → compile + upload (minutes)
```
<div class="-mt12">
<h3 class="ml-15"> Orthogonal cache layers </h3>
| Layer | Tool | Granularity | Backend |
|-------|------|-------------|---------|
| Toolchain image | lamina | Docker layer | Registry |
| Dep compilation | cargo-chef | Cargo crate graph | Docker layer |
| Artifact cache | sccache | Individual `.rlib` | S3 / GCS / Redis |
</div>
</div>
<div class="grid grid-cols-2 gap-6 -mt-45">
<div>
### Secret mount pattern (lamina canonical)
```dockerfile
RUN --mount=type=secret,id=sccache_creds,\
target=/run/secrets/sccache_creds \
. /run/secrets/sccache_creds && \
RUSTC_WRAPPER=sccache \
SCCACHE_BUCKET=$SCCACHE_BUCKET \
cargo chef cook --release \
--recipe-path recipe.json
```
</div>
</div>
Credentials injected at build time, not baked into layer.
---
# cargo-chef + sccache + BuildKit Together
**Each tool solves a different cache miss problem.**
```
Cold build (first run)
──────────────────────────────────────────────────────────────────────────────
planner ──► recipe.json (always cheap: metadata only)
cooker ──► compile 200 deps from scratch (6 min — sccache MISS: upload)
final ──► compile your code (2 min — sccache MISS: upload)
Warm build — Cargo.lock unchanged, your code changed
──────────────────────────────────────────────────────────────────────────────
planner ──► recipe.json (cheap)
cooker ──► BuildKit layer HIT (identical recipe) (0 sec — skip entirely)
final ──► compile your code (sccache MISS if changed: 2 min)
Hot build — src/ minor change
──────────────────────────────────────────────────────────────────────────────
planner ──► recipe.json (cheap)
cooker ──► BuildKit layer HIT (0 sec)
final ──► rustc on changed files → sccache HIT (seconds per file)
```
<div class="box-highlight mt-3 text-sm">
Three cache layers, three different scopes. When one misses, the others still win.
BuildKit serializes the dependency; sccache and cargo-chef operate independently inside.
</div>
---
# lian-build — Architecture
<div class="absolute right-30 top-8"><img src="/lian-h.svg" width="140"></div>
**Single binary. Orchestrates remote BuildKit runs. Emits lifecycle events.**
```
lian-build CLI
├─ 1. resolve runner size sizing::resolve(.build-spec.ncl → P95 → lang default)
├─ 2. publish build.started NATS: <prefix>.<workspace>.build.started
├─ 3. spawn runner POST /api/v1/vm-pool → lease_id
│ └─ hcloud cax11cax41 (ARM) | proxmox | docker-local
├─ 4. rsync build context rsync -e ssh context/ runner:/workspace/
├─ 5. run buildctl over SSH buildctl --addr ssh://runner build …
│ └─ OOM exit 137? retry once on next size tier (ADR-039)
├─ 6. record_metrics POST /api/v1/metrics (cpu_p95, mem_p95)
├─ 7. destroy runner DELETE /api/v1/vm-pool/{lease_id} [always]
└─ 8. publish build.completed/failed
```
<div class="text-xs opacity-60 mt-2">Compute and registry are plug-in slots. Callers supply <code>BuildDirectives</code> in Nickel — no caller identity in core code.</div>
---
# Three-Tier Sizing Resolution
**First match wins. Explicit beats historical beats defaults.**
<div class="grid grid-cols-3 gap-4 mt-4 text-sm">
<div class="border border-rust-orange/40 rounded p-3">
### Tier 1 — Explicit
```nix
# .build-spec.ncl
# in build context
{
runner_type = "cax31",
# authoritative
# or raw resources:
cpu = 8,
memory_gb = 16,
time_budget_min = 90,
}
```
Validated against `schemas/build_spec.ncl`.<br> Repo-level contract.
</div>
<div class="border border-rust-orange/30 rounded p-3">
### Tier 2 — P95 Historical
```
GET /api/v1/p95?workspace=…
→ {
cpu_p95: 3.2, mem_p95: 6.1
}
effective = {
cpu: ceil(3.2 × 1.2) = 4,
mem_gb:ceil(6.1 × 1.2) = 8,
}
floor: min(2 cpu, 4 GB)
```
Measured from prior runs. <br>Advisory — operator must approve before production use.
</div>
<div class="border border-rust-orange/20 rounded p-3">
### Tier 3 — Lang Default
```rust
match language {
"rust" => (
4 cpu, 8 GB, 60 min),
"go" => (
2 cpu, 4 GB, 30 min),
"java" => (
4 cpu, 8 GB, 45 min),
_ => (
2 cpu, 4 GB, 30 min),
}
```
Conservative floor. Rust is more expensive than Go — that's structural.
</div>
</div>
---
# OOM Retry — Bounded Escalation
**Exit 137 or stderr "OOM"/"Killed" → walk one tier up. Once.**
<div class="grid grid-cols-2 gap-6 mt-4">
<div>
```rust
// MAX_OOM_RETRIES = 1 —
// ADR-039 constraint oom-retry-bounded
pub const MAX_OOM_RETRIES: u8 = 1;
const SIZE_TIERS: &[(&str, u32, u32)] = &[
("cax11", 2, 4),
("cax21", 4, 8), // ← most Rust builds here
("cax31", 8, 16), // ← OOM retry target
("cax41", 16, 32),
];
```
</div>
<div>
### Why bounded at 1
- Second OOM means **misconfiguration**, <br>not transient pressure
- Unbounded retry loops spend money <br>on dead ends
- Forces developer to set explicit `runner_type`<br> in `.build-spec.ncl`
- ADR-039 constraint —<br> changing this requires a new ADR
</div>
<div class="-mt35">
### Retry flow
```
build on cax21 → OOM (exit 137)
└─ retries_used(0) < MAX_OOM_RETRIES(1)
└─ next_size_tier(cax21) → cax31
└─ rebuild on cax31
├─ success → record_metrics, destroy
└─ OOM again → FAIL (retries exhausted)
```
</div>
</div>
---
<h1 class="-mt8"> Cache Namespace Model</h1>
**Isolation between CI and session actors — the core tension resolved.**
```
Registry
├── ci/<workspace>/* canonical — written by CI, read-only to sessions
│ ├── ci/lian-build/deps:sha256-… (cargo-chef cooker layer)
│ └── ci/lian-build/base:sha256-… (toolchain layer from lamina)
└── dev/<actor-id>-<workspace>/* ephemeral — per session actor
└── dev/jpl-lian-build/… (your WIP session cache)
```
<div class="grid grid-cols-2 gap-6 mt-0">
<div>
<h3 class="-mb4">Resolution rules</h3>
| <small>Actor</small> | <small>Reads</small> | <small>Writes</small> |
|------------|-------|--------|
| <small>`'ci`</small> | <small>`ci/*` + own `dev/*`</small> | <small>`ci/*`</small> |
| <small>`'human`</small> | <small>`ci/*` + own `dev/*`</small> | <small>own `dev/*`</small> |
| <small>`'agent`</small> | <small>`ci/*` + own `dev/*`</small> | <small>own `dev/*`</small> |
| <small>`'ci_aux`</small> | <small>`ci/*` only</small> | <small>`ci/*` (restricted)</small> |
</div>
<div class="mt-1">
### Nickel schema
```nix
# schemas/cache_policy.ncl
let SessionCacheDisposition = [|
'export, # write back to registry on success
'discard, # ephemeral, discard after run
'rollback, # revert to last good state on fail
|]
```
Sessions declare intent. lian-build enforces it.<br>
CI never imports from `dev/*`.
</div>
</div>
---
# BuildDirectives — Caller-Supplied Vocabulary
**Callers (provisioning, vapora, CI) supply directives in Nickel. Core has no caller identity.**
```nix
# schemas/build_directives.ncl — the contract surface
let BuildDirectives = {
workspace | String,
artifact | BuildArtifact,
compute_provider | ComputeProviderRef, # 'hcloud | 'proxmox | 'docker_local
registry_provider | RegistryProviderRef, # 'zot | 'harbor | 'ghcr | 'dockerhub
cache_policy | CachePolicy,
runner_override | RunnerOverride | optional,
nats_events | NatsEventConfig | optional,
}
```
<div class="grid grid-cols-2 gap-6 mt-4 text-sm">
<div>
### CI invocation
```nix
# ci/directives.ncl
let D = import "defaults/build_directives.ncl" in
D.make_ci_build {
workspace = "lian-build",
artifact = {
image = "registry/lian-build:${sha}" },
cache_policy = D.ci_cache_policy,
}
```
</div>
<div>
### Session invocation
```nix
# dev/session.ncl
let D = import "defaults/build_directives.ncl" in
D.make_session_build {
workspace = "lian-build",
actor_id = "jpl",
disposition = 'discard,
}
```
</div>
</div>
<div class="text-xs opacity-60 mt-3">Hard constraint (ADR-001): <code>src/</code> must not match <code>provisioning_workspace | vapora_ | woodpecker_</code> — caller logic stays in directives.</div>
---
layout: cover
background: ./jude-infantini-mI-QcAP95Ok-unsplash.jpg
class: 'text-center photo-bg'
---
# lamina
## The pre-baked layer library
<br>
### *The catalog that feeds lian-build*
---
<h1 class="p-b-5"> lamina — What It Is </h1>
**Docker base images (toolchain layers) <br> + pre-cooked cargo dep caches (dependency layers).**
No binary.
<br>
No `src/`. No `Cargo.toml`. Dockerfiles + Nickel schemas + Nushell scripts.
```
lamina/
├── rust/ ─── Rust toolchain layer (rustup + sccache + cargo-chef)
├── leptos/ ─── Leptos WASM layer (rust + wasm-pack + trunk)
├── ontoref/ ─── Nickel + ore tools layer
├── nushell/ ─── Nu shell layer
├── lian-build/ ─── Build directives per layer, ctx-test.nu script
│ ├── Dockerfile.rust # planner/cooker/final for rust layer
│ ├── build_directives.ncl # per-layer lian-build config
│ └── ctx-test.nu # local test runner (docker-local mode)
└── schemas/ ─── workflow.ncl, layer catalog contracts
```
---
# lamina — What It Is
<div class="grid grid-cols-2 gap-6 mt-4 text-sm">
<div>
### Layer types
| Type | What it provides | Cache scope |
|------|-----------------|-------------|
| Toolchain | rustup, cargo, sccache binary | Docker registry |
| Dep layer | compiled `.rlib` for your deps | Docker layer + S3 |
| Utility | additional tools (nu, nickel) | Docker layer |
</div>
<div>
### Catalogue invariant
Every layer in `catalog/` has a `workflow.ncl` that declares:
- `tools_provided` — binaries that must exist post-build
- `build_base` — which layer it depends on (DAG)
- `artifact_paths` — what gets promoted to registry
`catalog-validate.nu --check-dag` enforces the DAG.
</div>
</div>
---
# lamina + lian-build — End-to-End
**lamina provides the layers. lian-build provides the compute.**
```
lamina lian-build
────────────────────────────────── ──────────────────────────────────────────────
rust/Dockerfile BuildDirectives (Nickel)
planner ──────► --cache-from registry/lamina/rust-deps:sha
cooker (cargo-chef) --cache-to registry/lamina/rust-deps:sha
final --image registry/lamina/rust:latest
schema: Compute:
workflow.ncl spawn cax21 runner (hcloud ARM)
build_directives.ncl ──────────► rsync context/ → runner
buildctl --addr ssh://runner:1234
ctx-test.nu OOM? → cax31, retry once
--layer rust destroy runner always
--mode docker-local ◄────── NATS: lian-build.lamina.build.completed
(local dev without VM)
```
<div class="box-highlight mt-1 text-sm">
lamina layers become the <code>--cache-from</code> inputs for downstream project builds.<br>
A project's cargo deps compile <em>on top of</em> the lamina rust dep layer <br>— hitting sccache HIT for everything lamina already compiled.
</div>
---
# The Full Picture
**From `docker build .` to a controlled substrate.**
<div class="grid grid-cols-2 gap-8 mt-4 text-sm">
<div>
### Before
```
dev push
└─ CI: docker build .
├─ pull rust:latest (1.8 GB)
├─ cargo build --deps (6 min, always)
└─ cargo build src (2 min)
810 min every build.
No retry on OOM.
No observability.
No cross-build reuse.
No actor isolation.
```
</div>
<div>
### After
```
dev push
└─ lian-build dispatch
├─ sizing: .build-spec.ncl → cax21
├─ NATS: build.started
├─ spawn cax21 (hcloud ARM, 30 sec)
├─ rsync context (15 sec)
├─ buildctl:
│ ├─ FROM lamina/rust:latest
│ │ (registry HIT, 0 sec)
│ ├─ cooker: cargo-chef layer
│ │ (registry HIT, 0 sec)
│ └─ final: your code
│ (sccache: seconds)
├─ destroy runner
└─ NATS: build.completed
~2 min warm. OOM retry automatic.
Structured events. Multi-actor isolation.
```
</div>
</div>
---
layout: cover
background: ./images/cleo-heck-1-l3ds6xcVI-unsplash.jpg
class: 'text-center photo-bg'
---
<h1 class="font-bold text-5xl absolute top-4 left-4.3/10"><img src="/lian-v.svg" width="130"></h1>
<h2 class="mt-40 text-2xl opacity-80">Alchemical refinement</h2>
<div class="mt-8 text-sm opacity-60 font-mono">
lian-build · lamina · BuildKit · cargo-chef · sccache
</div>
<div class="mt-6 text-base">
Each build: <em class="!text-orange-500">ephemeral compute, content-addressed cache, structured events.</em>
<br>
Callers supply intent. `lian-build` supplies execution.
</div>
<img class="absolute bottom-8 right-8 w-24 opacity-80" src="/ferris-celebration.svg">
<style scoped>
h1, h2, div, p { z-index: 10; }
em { color: #ce422b; font-style: italic; }
</style>

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="28 19 110 50" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Lian Build">
<title>Lian Build</title>
<defs>
<linearGradient id="lh-fg" x1="0" y1="1" x2="0" y2="0">
<stop offset="0" stop-color="#e86c2f"/>
<stop offset="1" stop-color="#f5a623"/>
</linearGradient>
<linearGradient id="lh-cg" x1="0" y1="1" x2="0" y2="0">
<stop offset="0" stop-color="#f5a623"/>
<stop offset="1" stop-color="#fcd99e"/>
</linearGradient>
<style><![CDATA[
@import url('https://fonts.googleapis.com/css2?family=Jost:wght@400;500&display=swap');
.lh-flame{fill:url(#lh-fg);opacity:0;transform-origin:50% 100%;transform-box:fill-box;animation:lh-flame 23s cubic-bezier(.4,0,.2,1) infinite}
.lh-core{fill:url(#lh-cg);opacity:0;transform-origin:50% 100%;transform-box:fill-box;animation:lh-core 23s cubic-bezier(.4,0,.2,1) infinite}
.lh-cruc{fill:none;stroke:#a8a8a0;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:34;stroke-dashoffset:34;animation:lh-cruc 23s cubic-bezier(.4,0,.2,1) infinite}
.lh-word{fill:#bd9156;font-family:"Jost","Avenir Next",Futura,"Century Gothic","Helvetica Neue",system-ui,sans-serif;font-weight:500;letter-spacing:.22em;font-size:22px;opacity:0;animation:lh-word 23s cubic-bezier(.4,0,.2,1) infinite}
.lh-sub{fill:#f29a3d;font-family:"Jost","Avenir Next",Futura,"Century Gothic","Helvetica Neue",system-ui,sans-serif;font-weight:500;letter-spacing:.22em;font-size:14px;opacity:0;animation:lh-sub 23s cubic-bezier(.4,0,.2,1) infinite}
.lh-fbord{fill:none;stroke:#e86c2f;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;opacity:0;animation:lh-fbord 23s cubic-bezier(.4,0,.2,1) infinite}
.lh-code{opacity:0;animation:lh-code 23s cubic-bezier(.4,0,.2,1) infinite}
@keyframes lh-core{
0%{opacity:0;transform:scale(.5)} 1.3%{opacity:.75;transform:scale(1.05)}
3.5%{opacity:.55;transform:scale(.95)} 5.2%{opacity:.35;transform:scale(.92)}
22%{opacity:.55;transform:scale(1)} 37%{opacity:.24;transform:scale(.88)}
53%{opacity:.55;transform:scale(1)} 68%{opacity:.24;transform:scale(.88)}
83%{opacity:.55;transform:scale(1)} 99%{opacity:.35;transform:scale(.92)} 100%{opacity:0}
}
@keyframes lh-flame{
0%,6.5%{opacity:0;transform:scale(.4)} 8.7%{opacity:.9;transform:scale(1)}
10.4%{opacity:1;transform:scale(1.05)} 12%{opacity:1;transform:scale(1)}
99%{opacity:1;transform:scale(1)} 100%{opacity:0;transform:scale(.4)}
}
@keyframes lh-fbord{
0%,11.7%{opacity:0} 14.3%{opacity:1} 99%{opacity:1} 100%{opacity:0}
}
@keyframes lh-code{
0%,15.2%{opacity:0} 17.4%{opacity:1} 99%{opacity:1} 100%{opacity:0}
}
@keyframes lh-cruc{
0%,18.3%{stroke-dashoffset:34;opacity:0} 18.8%{opacity:.8}
22.6%{stroke-dashoffset:0;opacity:.8} 99%{stroke-dashoffset:0;opacity:.8}
100%{stroke-dashoffset:34;opacity:0}
}
@keyframes lh-sub{
0%,19.6%{opacity:0} 23.9%{opacity:.85} 99%{opacity:.85} 100%{opacity:0}
}
@keyframes lh-word{
0%,20.4%{opacity:0} 25.2%{opacity:1} 99%{opacity:1} 100%{opacity:0}
}
@media (prefers-reduced-motion:reduce){
.lh-flame,.lh-fbord,.lh-core,.lh-cruc,.lh-word,.lh-sub,.lh-code{animation:none}
.lh-flame{opacity:1} .lh-fbord{opacity:1} .lh-code{opacity:1}
.lh-core{opacity:.35;transform:scale(.95);transform-origin:50% 100%;transform-box:fill-box}
.lh-cruc{opacity:.8;stroke-dashoffset:0} .lh-word{opacity:1} .lh-sub{opacity:.85}
}
]]></style>
</defs>
<path class="lh-cruc" d="M 33 65 L 67 65"/>
<path class="lh-flame" d="M 50 62 C 33 57 33 37 50 23 C 67 37 67 57 50 62 Z"/>
<path class="lh-fbord" d="M 50 62 C 33 57 33 37 50 23 C 67 37 67 57 50 62 Z"/>
<path class="lh-core" d="M 50 57 C 41 52 41 40 50 32 C 59 40 59 52 50 57 Z"/>
<g class="lh-code" fill="none" stroke="#c25a1a" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<polyline points="46,42 43,46 46,50"/>
<line x1="51" y1="43" x2="48" y2="49"/>
<polyline points="54,42 57,46 54,50"/>
</g>
<text class="lh-word" x="80" y="46" text-anchor="start">lian</text>
<text class="lh-sub" x="83" y="64" text-anchor="start">build</text>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="85 19 56 100" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Lian Build">
<title>Lian Build</title>
<defs>
<linearGradient id="lv-fg" x1="0" y1="1" x2="0" y2="0">
<stop offset="0" stop-color="#e86c2f"/>
<stop offset="1" stop-color="#f5a623"/>
</linearGradient>
<linearGradient id="lv-cg" x1="0" y1="1" x2="0" y2="0">
<stop offset="0" stop-color="#f5a623"/>
<stop offset="1" stop-color="#fcd99e"/>
</linearGradient>
<style><![CDATA[
@import url('https://fonts.googleapis.com/css2?family=Jost:wght@400;500&display=swap');
.lv-flame{fill:url(#lv-fg);opacity:0;transform-origin:50% 100%;transform-box:fill-box;animation:lv-flame 23s cubic-bezier(.4,0,.2,1) infinite}
.lv-core{fill:url(#lv-cg);opacity:0;transform-origin:50% 100%;transform-box:fill-box;animation:lv-core 23s cubic-bezier(.4,0,.2,1) infinite}
.lv-cruc{fill:none;stroke:#a8a8a0;stroke-width:1.5;stroke-linecap:round;stroke-dasharray:36;stroke-dashoffset:36;animation:lv-cruc 23s cubic-bezier(.4,0,.2,1) infinite}
.lv-word{fill:#bd9156;font-family:"Jost","Avenir Next",Futura,"Century Gothic","Helvetica Neue",system-ui,sans-serif;font-weight:500;letter-spacing:.22em;font-size:22px;opacity:0;animation:lv-word 23s cubic-bezier(.4,0,.2,1) infinite}
.lv-sub{fill:#f29a3d;font-family:"Jost","Avenir Next",Futura,"Century Gothic","Helvetica Neue",system-ui,sans-serif;font-weight:500;letter-spacing:.22em;font-size:14px;opacity:0;animation:lv-sub 23s cubic-bezier(.4,0,.2,1) infinite}
.lv-fbord{fill:none;stroke:#e86c2f;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;opacity:0;animation:lv-fbord 23s cubic-bezier(.4,0,.2,1) infinite}
.lv-code{opacity:0;animation:lv-code 23s cubic-bezier(.4,0,.2,1) infinite}
@keyframes lv-core{
0%{opacity:0;transform:scale(.5)} 1.3%{opacity:.75;transform:scale(1.05)}
3.5%{opacity:.55;transform:scale(.95)} 5.2%{opacity:.35;transform:scale(.92)}
22%{opacity:.55;transform:scale(1)} 37%{opacity:.24;transform:scale(.88)}
53%{opacity:.55;transform:scale(1)} 68%{opacity:.24;transform:scale(.88)}
83%{opacity:.55;transform:scale(1)} 99%{opacity:.35;transform:scale(.92)} 100%{opacity:0}
}
@keyframes lv-flame{
0%,6.5%{opacity:0;transform:scale(.4)} 8.7%{opacity:.9;transform:scale(1)}
10.4%{opacity:1;transform:scale(1.05)} 12%{opacity:1;transform:scale(1)}
99%{opacity:1;transform:scale(1)} 100%{opacity:0;transform:scale(.4)}
}
@keyframes lv-fbord{
0%,11.7%{opacity:0} 14.3%{opacity:1} 99%{opacity:1} 100%{opacity:0}
}
@keyframes lv-code{
0%,15.2%{opacity:0} 17.4%{opacity:1} 99%{opacity:1} 100%{opacity:0}
}
@keyframes lv-cruc{
0%,18.3%{stroke-dashoffset:36;opacity:0} 18.8%{opacity:.8}
22.6%{stroke-dashoffset:0;opacity:.8} 99%{stroke-dashoffset:0;opacity:.8}
100%{stroke-dashoffset:36;opacity:0}
}
@keyframes lv-sub{
0%,19.6%{opacity:0} 23.9%{opacity:.85} 99%{opacity:.85} 100%{opacity:0}
}
@keyframes lv-word{
0%,20.4%{opacity:0} 25.2%{opacity:1} 99%{opacity:1} 100%{opacity:0}
}
@media (prefers-reduced-motion:reduce){
.lv-flame,.lv-fbord,.lv-core,.lv-cruc,.lv-word,.lv-sub,.lv-code{animation:none}
.lv-flame{opacity:1} .lv-fbord{opacity:1} .lv-code{opacity:1}
.lv-core{opacity:.35;transform:scale(.95);transform-origin:50% 100%;transform-box:fill-box}
.lv-cruc{opacity:.8;stroke-dashoffset:0} .lv-word{opacity:1} .lv-sub{opacity:.85}
}
]]></style>
</defs>
<path class="lv-cruc" d="M 92 66 L 128 66"/>
<path class="lv-flame" d="M 110 64 C 92 57 92 36 110 22 C 128 36 128 57 110 64 Z"/>
<path class="lv-fbord" d="M 110 64 C 92 57 92 36 110 22 C 128 36 128 57 110 64 Z"/>
<path class="lv-core" d="M 110 58 C 100 52 100 40 110 32 C 120 40 120 52 110 58 Z"/>
<g class="lv-code" fill="none" stroke="#c25a1a" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<polyline points="106,42 103,46 106,50"/>
<line x1="112" y1="43" x2="109" y2="49"/>
<polyline points="115,42 118,46 115,50"/>
</g>
<text class="lv-word" x="113" y="93" text-anchor="middle">lian</text>
<text class="lv-sub" x="112" y="110" text-anchor="middle">build</text>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -1,3 +1,5 @@
#!/bin/bash
pnpm run dev
SLIDES_PATH=${1:-slides.md}
pnpm run dev $SLIDES_PATH

View file

@ -0,0 +1,576 @@
---
theme: default
title: Working Group — Ontología y Reflection
titleTemplate: '%s - OntoRef'
layout: cover
keywords: Ontología,Reflection,Rust,Nickel,WorkingGroup,RustLasPalmas
download: true
exportFilename: ontoref-working-group
monaco: true
remoteAssets: true
selectable: true
record: true
colorSchema: dark
lineNumbers: false
themeConfig:
primary: '#f74c00'
logoHeader: '/ferris.svg'
fonts:
mono: 'Victor Mono'
drawings:
enabled: true
persist: false
presenterOnly: false
syncAll: true
scripts:
- setup/image-overlay.ts
class: 'justify-center flex flex-cols'
---
<h1 class="font-medium text-3xl leading-tight">Working Group<br><span class="text-orange-400">Ontología y Reflection en tus proyectos</span></h1>
<div class="flex justify-center mt-6 text-gray-400 text-lg italic">
Sesión de presentación · Rust-LasPalmas
</div>
<div class="flex justify-center mt-8">
<img class="w-40" src="/jesusperez_w.svg">
</div>
<style scoped>
h1, h2, p { z-index: 10; }
</style>
<Footer />
<!--
Esto no es una charla. Es la sesión donde decidís si entráis a un grupo de trabajo cerrado.
Saludo breve, agradecer la asistencia, y avisar: en 60 minutos saldrán con la decisión tomada.
No hay seguimiento por correo, no hay "ya os mandaré info". Hoy entráis o no entráis.
-->
---
layout: section
---
# Tres preguntas, antes de nada
<!--
Pasar al modo participativo desde el primer momento.
No es retórica — pedir respuesta de verdad. Si nadie levanta la mano, esperar.
El silencio funciona aquí.
-->
---
layout: default
---
# Levantad la mano si esto os ha pasado
<div class="mt-8 space-y-6">
<v-click>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-2xl mt-1">1.</span>
<div class="text-gray-200 text-lg">
Postmortem donde la decisión que se violó <em>estaba escrita</em> en una ADR — pero nadie la recordaba.
</div>
</div>
</v-click>
<v-click>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-2xl mt-1">2.</span>
<div class="text-gray-200 text-lg">
Onboarding de alguien nuevo (o tú mismo, después de tres meses) re-descubriendo <em>por qué</em> el código está así.
</div>
</div>
</v-click>
<v-click>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-2xl mt-1">3.</span>
<div class="text-gray-200 text-lg">
Diagrama de arquitectura que <em>mintió</em> tres meses después de pintarlo.
</div>
</div>
</v-click>
</div>
<!--
Click por click. Pausa entre cada uno.
Si la sala levanta la mano en los tres, ya tienes audiencia. Si solo en uno, ese es el dolor a destacar.
Reconocer en voz alta: "ya, lo conocemos todos."
-->
---
layout: default
---
# El patrón
<div class="mt-6 grid grid-cols-2 gap-8">
<div>
**Para el código exigimos**
<div class="mt-4 space-y-3">
<div class="flex items-start gap-2">
<span class="text-green-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300">Tipos estrictos</div>
</div>
<div class="flex items-start gap-2">
<span class="text-green-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300">Tests que validan invariantes</div>
</div>
<div class="flex items-start gap-2">
<span class="text-green-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300">Compilador que rechaza incoherencias</div>
</div>
<div class="flex items-start gap-2">
<span class="text-green-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300">Errores antes de llegar a producción</div>
</div>
</div>
</div>
<div>
<v-click>
**Para las decisiones aceptamos**
<div class="mt-4 space-y-3">
<div class="flex items-start gap-2">
<span class="text-red-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300">Markdown libre</div>
</div>
<div class="flex items-start gap-2">
<span class="text-red-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300">Diagramas que envejecen</div>
</div>
<div class="flex items-start gap-2">
<span class="text-red-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300">Convención y disciplina humana</div>
</div>
<div class="flex items-start gap-2">
<span class="text-red-400 font-bold text-sm mt-1"></span>
<div class="text-gray-300">Errores que aparecen en el postmortem</div>
</div>
</div>
</v-click>
</div>
</div>
<!--
Esta es LA slide. La incoherencia entre las dos columnas es el insight completo.
"Rust nos da tipos hasta el borde del binario. Después, texto libre."
Aquí no se vende solución todavía.
-->
---
layout: section
---
# ¿Y si las tratáramos igual?
<div class="mt-8 text-gray-400 text-xl italic">
Tipos. Contratos. Validación al guardar, no en el postmortem.
</div>
<!--
Pausa larga aquí.
La pregunta queda en el aire. La siguiente slide es la respuesta práctica.
-->
---
layout: default
---
# Lo que vamos a ver ahora
<div class="mt-8 space-y-5 text-gray-300 text-lg">
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-xl mt-1"></span>
<div>Un proyecto vacío</div>
</div>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-xl mt-1"></span>
<div>Un axioma · una tensión · una decisión</div>
</div>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-xl mt-1"></span>
<div>Un cambio que viola la decisión</div>
</div>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-xl mt-1"></span>
<div>El typecheck rechazándolo</div>
</div>
</div>
<div class="mt-12 text-center text-gray-500 text-sm italic">
8 líneas de código. Sin frameworks. Sin instalar nada raro.
</div>
<!--
Slide-anchor para la mini-demo. Aquí salgo de las slides y voy a la terminal.
~10 minutos de demo en vivo.
La frase final ("8 líneas de código") es un compromiso público que la demo tiene que sostener.
-->
---
layout: cover
class: 'justify-center flex flex-cols'
---
<h1 class="font-medium text-3xl text-orange-400">Demo en vivo</h1>
<div class="flex justify-center mt-6 text-gray-400 text-lg italic">
El terminal y un editor. Nada más.
</div>
<!--
Salir de slidev. Terminal en pantalla compartida.
Pre-cargado:
- directorio /tmp/demo-wg/ vacío
- dos buffers preparados pero no escritos
- ontoref CLI instalada y verificada
- nickel typecheck verificado
Plan B si algo falla: tener un asciinema grabado que reproducir.
Volver a las slides cuando suene la frase: "Esto que acabáis de ver es lo más pequeño que se puede tener."
-->
---
layout: default
---
# Lo que acabáis de ver
<div class="mt-6 grid grid-cols-2 gap-8">
<div>
**Lo construido en 8 minutos**
<div class="mt-4 space-y-3 text-gray-300">
<div class="flex items-start gap-2">
<span class="text-orange-400 font-bold text-sm mt-1">·</span>
<div>Un <code>core.ncl</code> con un axioma y una tensión</div>
</div>
<div class="flex items-start gap-2">
<span class="text-orange-400 font-bold text-sm mt-1">·</span>
<div>Una ADR como contrato vivo</div>
</div>
<div class="flex items-start gap-2">
<span class="text-orange-400 font-bold text-sm mt-1">·</span>
<div>Un cambio rechazado al guardar</div>
</div>
</div>
</div>
<div>
<v-click>
**Lo que esto se vuelve con tiempo**
<div class="mt-4 text-gray-300 text-sm">
Un proyecto real, después de meses:
</div>
<div class="mt-3 font-mono text-xs text-gray-400 bg-gray-900 p-3 rounded">
.ontology/<br>
├── core.ncl <span class="text-gray-600">(20 nodos, 22 aristas)</span><br>
├── state.ncl <span class="text-gray-600">(5 dimensiones FSM)</span><br>
├── gate.ncl <span class="text-gray-600">(membranas)</span><br>
└── manifest.ncl<br>
adrs/ <span class="text-gray-600">(20 decisiones vivas)</span><br>
reflection/modes/ <span class="text-gray-600">(rutinas tipadas)</span>
</div>
<div class="mt-3 text-gray-500 text-sm italic">
La estructura es la misma. Solo crece el contenido.
</div>
</v-click>
</div>
</div>
<!--
Click reveal: lo que tienen ahora vs destino realista.
NO es "tienes que llegar aquí". Es "esto crece a esto cuando le dedicas tiempo".
Frase clave: "lo de antes y esto es el mismo schema. Lo importante es empezar."
-->
---
layout: section
---
# La propuesta
<div class="mt-6 text-gray-400 text-xl">
Working group cerrado. Cuatro o cinco personas.
</div>
<!--
Cambio de tono. Ahora es la oferta concreta.
-->
---
layout: default
---
# Cómo funciona
<div class="mt-4 space-y-4 text-gray-300">
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-sm mt-1 w-32 flex-none">Formato</span>
<div>Presencial · coworking en Las Palmas</div>
</div>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-sm mt-1 w-32 flex-none">Tamaño</span>
<div>45 personas · cohorte cerrada</div>
</div>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-sm mt-1 w-32 flex-none">Sesiones</span>
<div>4 sesiones de ~2 horas · una cada 2 semanas</div>
</div>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-sm mt-1 w-32 flex-none">Entre sesiones</span>
<div>~1 hora de iteración sobre vuestro proyecto</div>
</div>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-sm mt-1 w-32 flex-none">Coste</span>
<div>Vuestro tiempo. Coworking compartido.</div>
</div>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-sm mt-1 w-32 flex-none">Después</span>
<div>Si funciona, abrimos un siguiente ciclo. No hay programa eterno.</div>
</div>
</div>
<!--
Honestidad sobre el compromiso real.
"Cohorte cerrada" — no se incorpora gente a mitad. Razón: continuidad y lenguaje común.
"4 sesiones" — no más. Si necesitan más, abren otro ciclo después.
-->
---
layout: default
---
# Qué os lleváis
<div class="mt-8 space-y-5 text-gray-300 text-lg">
<div class="flex items-start gap-3">
<span class="text-green-400 font-bold text-xl mt-1"></span>
<div>Un <code>.ontology/</code> mínimo funcionando para un proyecto vuestro</div>
</div>
<div class="flex items-start gap-3">
<span class="text-green-400 font-bold text-xl mt-1"></span>
<div>Una ADR formalizada como constraint que protege ese proyecto</div>
</div>
<div class="flex items-start gap-3">
<span class="text-green-400 font-bold text-xl mt-1"></span>
<div>Una <code>reflection mode</code> propia para una rutina vuestra</div>
</div>
<div class="flex items-start gap-3">
<span class="text-green-400 font-bold text-xl mt-1"></span>
<div>Capacidad de seguir solos, sin depender del grupo</div>
</div>
</div>
<div class="mt-12 text-gray-500 italic text-center">
No salís con un curso. Salís con código vuestro vivo.
</div>
<!--
Outcome material. Cuatro cosas concretas, ninguna abstracta.
La frase del cierre es la promesa.
-->
---
layout: default
---
# Qué esto NO es
<div class="mt-8 space-y-4 text-gray-300">
<div class="flex items-start gap-3">
<span class="text-red-400 font-bold text-xl mt-1"></span>
<div>Un curso de Nickel desde cero</div>
</div>
<div class="flex items-start gap-3">
<span class="text-red-400 font-bold text-xl mt-1"></span>
<div>Una venta de OntoRef como producto</div>
</div>
<div class="flex items-start gap-3">
<span class="text-red-400 font-bold text-xl mt-1"></span>
<div>Soporte 24/7 entre sesiones</div>
</div>
<div class="flex items-start gap-3">
<span class="text-red-400 font-bold text-xl mt-1"></span>
<div>Asistencia opcional · si fallas dos sesiones, sales del ciclo</div>
</div>
<div class="flex items-start gap-3">
<span class="text-red-400 font-bold text-xl mt-1"></span>
<div>Una comunidad amplia · esto es trabajo concentrado, no networking</div>
</div>
</div>
<!--
Gestionar expectativas con honestidad.
La línea de "fallas dos sesiones, sales del ciclo" parece dura — es deliberada.
La continuidad es lo que define el grupo. Sin ella, no hay valor.
Mejor decirlo aquí que en sesión 3 cuando alguien falló dos veces.
-->
---
layout: default
---
# Lo que necesito de cada uno antes de la primera sesión
<div class="mt-6 space-y-4 text-gray-300">
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-sm mt-1 w-8 flex-none">1.</span>
<div>Un proyecto vuestro · Rust o no, lo que tengáis vivo</div>
</div>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-sm mt-1 w-8 flex-none">2.</span>
<div>Nickel y la CLI de OntoRef instaladas (os paso la guía)</div>
</div>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-sm mt-1 w-8 flex-none">3.</span>
<div>Una decisión arquitectónica de ese proyecto que recordéis · cualquiera</div>
</div>
<div class="flex items-start gap-3">
<span class="text-orange-400 font-bold text-sm mt-1 w-8 flex-none">4.</span>
<div>Disposición a romper esa decisión en vivo y ver qué pasa</div>
</div>
</div>
<div class="mt-10 text-gray-500 italic">
Si traéis esto, en sesión 1 ya estáis modelando.
</div>
<!--
La preparación entre la decisión-de-entrar y la sesión 1 es lo que filtra a quien va en serio.
Lista corta y concreta. Sin requisitos masivos.
-->
---
layout: cover
class: 'justify-center flex flex-cols'
---
<h1 class="font-medium text-3xl text-orange-400">¿Quién entra?</h1>
<div class="flex justify-center mt-6 text-gray-300 text-lg">
Levantad la mano. No por correo después.
</div>
<div class="flex justify-center mt-12 text-gray-500 italic text-base">
Mejor tres comprometidos que cinco difusos.
</div>
<!--
Esta es la slide del momento de decisión.
NO seguir presentando después de esto. NO añadir "ya os pensáis si queréis".
Pausa. Mirar a la sala. Esperar manos.
Tomar nombres y mails ahí mismo. Crear el grupo de Telegram/Signal antes de salir.
-->
---
layout: default
---
# Si no entras hoy
<div class="mt-8 space-y-4 text-gray-300 text-lg">
<div class="flex items-start gap-3">
<span class="text-gray-500 font-bold text-sm mt-1">·</span>
<div>El siguiente ciclo se abre cuando éste termine — sin fecha fijada todavía.</div>
</div>
<div class="flex items-start gap-3">
<span class="text-gray-500 font-bold text-sm mt-1">·</span>
<div>Si quieres aviso del próximo, déjalo dicho hoy.</div>
</div>
<div class="flex items-start gap-3">
<span class="text-gray-500 font-bold text-sm mt-1">·</span>
<div>El material del approach está abierto en Github · podéis explorar solos.</div>
</div>
</div>
<div class="mt-12 text-center text-gray-400 italic">
Gracias por venir.
</div>
<Footer />
<!--
Slide para no dejar a nadie con la sensación de "rechazado".
Quien no entró hoy tiene puerta abierta para el siguiente ciclo.
Y el approach está públicamente disponible — la sesión les sirvió aunque no entren al WG.
-->

View file

@ -0,0 +1,33 @@
# Working Group OntoRef — Material de Apoyo
Material para la sesión de presentación + arranque del Working Group local
(Rust-LasPalmas) sobre Ontología y Reflection.
## Estructura
```text
work-group-ore/
├── README.md Este archivo
├── setup.md Guía de instalación para participantes
└── demo/
├── README.md Cómo correr las demos
├── script.md Guion timing-by-timing de la mini-demo
├── option-a-owner/ Demo principal: owner por servicio
├── option-b-layers/ Demo alternativa: capas sin acople
└── recording/ Plan B — asciinema pre-grabada
```
## Slides asociadas
`../presentation/work_group_info.md` — sesión de presentación en Slidev.
## Flujo recomendado
1. **Antes de la sesión presentación**: ensayar la demo en vivo con
`demo/option-a-owner/` siguiendo `demo/script.md`. Grabar la versión asciinema
como Plan B (`demo/recording/record.sh`).
2. **Durante la sesión presentación**: usar las slides + cambiar a terminal en
la slide-anchor "Demo en vivo".
3. **Tras la sesión presentación**: enviar `setup.md` a quienes se apuntaron.
4. **Sesión 0 del WG**: cada participante llega con setup completo y un
proyecto suyo identificado.

View file

@ -0,0 +1,59 @@
# Mini-demo — Working Group OntoRef
Dos opciones de mini-demo (~8-10 min) para la sesión de presentación. La
demo se ejecuta en vivo desde una terminal cuando las slides llegan a la
slide-anchor "Demo en vivo".
## Cuál usar
| Opción | Recomendada para | Complejidad |
|---|---|---|
| **A — Owner por servicio** | Audiencia mixta de developers (cualquier stack) | Baja, ~12 líneas |
| **B — Capas sin acople** | Audiencia rustacean / backend con experiencia | Media, ~30 líneas |
**Recomendación**: usar la opción A en la primera sesión. La opción B queda
guardada por si en alguna repetición del WG la audiencia exige más
sofisticación.
## Estructura de cada opción
Cada opción tiene tres archivos `.ncl` que representan los tres pasos de la
demo:
```text
option-X/
├── README.md Descripción de la opción
├── core.start.ncl Paso 1: archivo mínimo sin contrato
├── core.with-contract.ncl Paso 2: contrato añadido, nickel export pasa
└── core.broken.ncl Paso 3: violación, nickel export falla
```
El presentador no copia estos archivos durante la demo — los va escribiendo
en vivo. Los archivos sirven como:
- Referencia de lo que tiene que terminar escribiendo
- Plan B si se queda en blanco (`cp core.with-contract.ncl /tmp/demo-wg/core.ncl`)
- Material de grabación asciinema para Plan B audiovisual
## Ejecutar manualmente para ensayar
```bash
cd /tmp && rm -rf demo-wg && mkdir demo-wg && cd demo-wg
# Paso 1
cp /Users/Akasha/Development/ontoref/assets/work-group-ore/demo/option-a-owner/core.start.ncl ./core.ncl
nickel export core.ncl # exporta JSON
# Paso 2
cp /Users/Akasha/Development/ontoref/assets/work-group-ore/demo/option-a-owner/core.with-contract.ncl ./core.ncl
nickel export core.ncl # exporta JSON
# Paso 3
cp /Users/Akasha/Development/ontoref/assets/work-group-ore/demo/option-a-owner/core.broken.ncl ./core.ncl
nickel export core.ncl # falla con missing definition for 'owner'
```
## Plan B audiovisual
Ver `recording/README.md` para la grabación con asciinema y cómo reproducirla
si la demo en vivo falla.

View file

@ -0,0 +1,38 @@
# Opción A — Owner por servicio (recomendada)
Demo principal de la sesión de presentación. Modela una decisión universal
("cada servicio tiene un owner asignado") como contrato Nickel y muestra
cómo el typecheck rechaza una violación de la misma.
## Por qué esta opción
- **Reconocible al instante** — cualquier developer ha vivido el postmortem
donde nadie sabe quién mantiene el servicio
- **Modelable en pocas líneas** — el contrato es un record con dos campos
- **Error de typecheck legible**`missing field 'owner'` no necesita
explicación
## Archivos
| Archivo | Paso de la demo | Resultado de `nickel export` |
|---|---|---|
| `core.start.ncl` | Paso 2 — sin contrato | ✓ exporta JSON (pero no valida nada) |
| `core.with-contract.ncl` | Paso 3 — contrato aplicado | ✓ exporta JSON (estructura validada) |
| `core.broken.ncl` | Paso 4 — violación | ✗ error: `missing definition for 'owner'` |
> **Por qué `nickel export` y no `nickel typecheck`**: los contratos
> `| Array Service` se ejecutan durante la evaluación, no durante el chequeo
> de tipos. `nickel typecheck core.broken.ncl` pasa silenciosamente porque
> solo verifica tipos estáticos. `nickel export` evalúa el archivo entero y
> aplica los contratos — eso es lo que activa el error.
## Verificación rápida (ensayo)
```bash
cd /Users/Akasha/Development/ontoref/assets/work-group-ore/demo/option-a-owner
nickel export core.start.ncl # exporta JSON
nickel export core.with-contract.ncl # exporta JSON
nickel export core.broken.ncl # falla con error legible — esperado
```
Ver `../script.md` para el guion completo de la demo.

View file

@ -0,0 +1,15 @@
let Service = {
name | String,
owner | String,
}
in
{
axiom = "Cada servicio en producción tiene un owner identificable",
services | Array Service = [
{ name = "api", owner = "backend" },
{ name = "frontend", owner = "frontend" },
{ name = "metrics" },
],
}

View file

@ -0,0 +1,8 @@
{
axiom = "Cada servicio en producción tiene un owner identificable",
services = [
{ name = "api", owner = "backend" },
{ name = "frontend", owner = "frontend" },
],
}

View file

@ -0,0 +1,14 @@
let Service = {
name | String,
owner | String,
}
in
{
axiom = "Cada servicio en producción tiene un owner identificable",
services | Array Service = [
{ name = "api", owner = "backend" },
{ name = "frontend", owner = "frontend" },
],
}

View file

@ -0,0 +1,68 @@
# Opción B — Capas sin acople (alternativa rustacean)
Demo alternativa, más sofisticada. Modela la regla de arquitectura hexagonal
"el dominio no depende de infraestructura, la aplicación no depende de
infraestructura" como contrato Nickel custom, y muestra el typecheck
rechazando una dependencia ilegal.
## Cuándo usar esta en vez de A
- Audiencia con experiencia previa en arquitectura limpia / hexagonal
- Cohorte que ya pasó por el WG y vuelve para repetir con tema más denso
- Sesión más larga (15 min en vez de 8) donde da tiempo a explicar las capas
**Para la primera sesión con audiencia mixta: usa la opción A.**
## Archivos
| Archivo | Paso de la demo | Resultado de `nickel export` |
|---|---|---|
| `core.start.ncl` | Paso 2 — sin contrato custom | ✓ exporta JSON (estructura básica) |
| `core.with-contract.ncl` | Paso 3 — contrato `ValidLayerDependency` aplicado | ✓ exporta JSON (todas las dependencias son legales) |
| `core.broken.ncl` | Paso 4 — dependencia ilegal | ✗ error: `Layer violation: user (Domain) cannot depend on db_pool (Infrastructure)` |
> **Composición de contratos**: los dos contratos (`Dependency` para la forma
> del record y `ValidLayerDependency` para la regla de capas) se aplican
> encadenados con `| Array Dependency | Array ValidLayerDependency`. No se
> mergean con `&` porque uno es record-shape y el otro es custom contract —
> el `|` los aplica secuencialmente al mismo valor.
## Concepto modelado
Tres capas:
- `'Domain` — lógica pura, no debe conocer infraestructura
- `'Application` — orquesta dominio, no debe conocer infraestructura
- `'Infrastructure` — adaptadores, puede depender de cualquier capa
Reglas codificadas en `allowed_dependency`:
| Source | Targets permitidos |
|---|---|
| `'Domain` | `'Domain` solamente |
| `'Application` | `'Domain` y `'Application` |
| `'Infrastructure` | cualquiera |
## Verificación rápida (ensayo)
```bash
cd /Users/Akasha/Development/ontoref/assets/work-group-ore/demo/option-b-layers
nickel export core.start.ncl # exporta JSON
nickel export core.with-contract.ncl # exporta JSON
nickel export core.broken.ncl # falla — Layer violation: user (Domain) cannot depend on db_pool (Infrastructure)
```
## Adaptación del guion
El guion en `../script.md` está escrito para opción A. Para opción B, los
cambios:
- **Paso 2**: tecleas la lista de dependencias sin contrato custom
- **Paso 3**: tecleas el `let allowed_dependency = ...` y el
`ValidLayerDependency` con `std.contract.custom`
- **Paso 4**: añades una dependencia `'Domain -> 'Infrastructure` y el typecheck
rechaza con el mensaje del custom contract
- **Paso 5**: arreglarlo significa o cambiar la capa del módulo origen, o
refactorizar para introducir un puerto/adapter
Tiempo: ~12-15 min en vez de 8-10.

View file

@ -0,0 +1,45 @@
let Layer = [| 'Domain, 'Application, 'Infrastructure |]
in
let layer_to_string = fun l =>
l |> match {
'Domain => "Domain",
'Application => "Application",
'Infrastructure => "Infrastructure",
}
in
let allowed_dependency = fun source target =>
(source == 'Domain && target == 'Domain)
|| (source == 'Application && (target == 'Domain || target == 'Application))
|| (source == 'Infrastructure)
in
let Dependency = {
from_module | String,
from_layer | Layer,
to_module | String,
to_layer | Layer,
}
in
let ValidLayerDependency = std.contract.custom (fun label =>
fun value =>
if allowed_dependency value.from_layer value.to_layer then
'Ok value
else
'Error {
message = "Layer violation: %{value.from_module} (%{layer_to_string value.from_layer}) cannot depend on %{value.to_module} (%{layer_to_string value.to_layer})",
}
)
in
{
axiom = "El dominio no depende de infraestructura. La aplicación tampoco.",
dependencies | Array Dependency | Array ValidLayerDependency = [
{ from_module = "user_service", from_layer = 'Application, to_module = "user", to_layer = 'Domain },
{ from_module = "user_repository", from_layer = 'Infrastructure, to_module = "user", to_layer = 'Domain },
{ from_module = "user", from_layer = 'Domain, to_module = "db_pool", to_layer = 'Infrastructure },
],
}

View file

@ -0,0 +1,8 @@
{
axiom = "El dominio no depende de infraestructura. La aplicación tampoco.",
dependencies = [
{ from_module = "user_service", from_layer = 'Application, to_module = "user", to_layer = 'Domain },
{ from_module = "user_repository", from_layer = 'Infrastructure, to_module = "user", to_layer = 'Domain },
],
}

View file

@ -0,0 +1,44 @@
let Layer = [| 'Domain, 'Application, 'Infrastructure |]
in
let layer_to_string = fun l =>
l |> match {
'Domain => "Domain",
'Application => "Application",
'Infrastructure => "Infrastructure",
}
in
let allowed_dependency = fun source target =>
(source == 'Domain && target == 'Domain)
|| (source == 'Application && (target == 'Domain || target == 'Application))
|| (source == 'Infrastructure)
in
let Dependency = {
from_module | String,
from_layer | Layer,
to_module | String,
to_layer | Layer,
}
in
let ValidLayerDependency = std.contract.custom (fun label =>
fun value =>
if allowed_dependency value.from_layer value.to_layer then
'Ok value
else
'Error {
message = "Layer violation: %{value.from_module} (%{layer_to_string value.from_layer}) cannot depend on %{value.to_module} (%{layer_to_string value.to_layer})",
}
)
in
{
axiom = "El dominio no depende de infraestructura. La aplicación tampoco.",
dependencies | Array Dependency | Array ValidLayerDependency = [
{ from_module = "user_service", from_layer = 'Application, to_module = "user", to_layer = 'Domain },
{ from_module = "user_repository", from_layer = 'Infrastructure, to_module = "user", to_layer = 'Domain },
],
}

View file

@ -0,0 +1,50 @@
# Plan B Audiovisual — Asciinema pre-grabada
Si la demo en vivo falla durante la sesión (cuelgue de editor, problema con
nickel, conexión de red rara), se reproduce esta grabación. La audiencia
sigue viendo el approach funcionando — no se pierde el momento.
## Grabar la demo
Antes de la sesión, ejecutar:
```bash
cd /Users/Akasha/Development/ontoref/assets/work-group-ore/demo/recording
./record.sh
```
El script:
1. Crea un directorio temporal `/tmp/demo-wg-rec/`
2. Inicia `asciinema rec demo.cast`
3. Reproduce la demo paso a paso con timing visible
4. Genera `demo.cast` en este directorio
## Reproducir durante la sesión
Si la demo en vivo se atasca:
```bash
asciinema play /Users/Akasha/Development/ontoref/assets/work-group-ore/demo/recording/demo.cast
```
Para reproducir a velocidad acelerada:
```bash
asciinema play -s 2 demo.cast
```
## Requisitos
- `asciinema` instalado (`brew install asciinema` o `pip install asciinema`)
- `nickel` 1.16.0+ accesible en `$PATH`
## Verificar que la grabación funciona
Después de grabar, reproducir desde el principio:
```bash
asciinema play demo.cast
```
Si se ve la demo entera reproduciéndose sin saltos raros, está lista.

View file

@ -0,0 +1,107 @@
#!/usr/bin/env bash
# record.sh - Graba la demo "Owner por servicio" con asciinema.
# Genera demo.cast reproducible como Plan B durante la sesión presentación.
set -euo pipefail
DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_DIR="${DEMO_DIR}/../option-a-owner"
WORK_DIR="/tmp/demo-wg-rec"
CAST_FILE="${DEMO_DIR}/demo.cast"
if ! command -v asciinema >/dev/null 2>&1; then
echo "ERROR: asciinema no instalado. brew install asciinema" >&2
exit 1
fi
if ! command -v nickel >/dev/null 2>&1; then
echo "ERROR: nickel no instalado. brew install nickel-lang" >&2
exit 1
fi
cat <<EOF
Va a grabar la demo en:
$CAST_FILE
Pulsa enter para empezar la grabación. Después podrás reproducirla con:
asciinema play $CAST_FILE
EOF
read -r
# Reset working directory
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR"
# Pequeño script Python que reproduce la demo dentro de asciinema
PLAY_SCRIPT="$(mktemp)"
cat > "$PLAY_SCRIPT" <<'PLAY'
#!/usr/bin/env bash
set -e
CYAN='\033[1;36m'
YELLOW='\033[1;33m'
GREEN='\033[1;32m'
RED='\033[1;31m'
GRAY='\033[0;90m'
NC='\033[0m'
WORK_DIR="$1"
SOURCE_DIR="$2"
cd "$WORK_DIR"
step() { echo -e "\n${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; echo -e "${YELLOW} $1${NC}"; echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"; sleep 2; }
say() { echo -e "${GRAY}# $1${NC}"; sleep 2; }
cmd() { echo -e "${CYAN}\$ $1${NC}"; sleep 1; eval "$1"; sleep 2; }
step "Paso 1 — Punto de partida"
say "Esto es lo que cualquiera tiene antes de empezar."
cmd "pwd"
cmd "ls"
step "Paso 2 — Escribir core.ncl mínimo"
say "Defino lo que quiero que sea cierto. Sin contrato todavía."
cmd "cat $SOURCE_DIR/core.start.ncl"
cp "$SOURCE_DIR/core.start.ncl" core.ncl
cmd "nickel export core.ncl"
say "Exporta. Pero no valida nada todavía — la estructura es lo único garantizado."
step "Paso 3 — Añadir el contrato"
say "Defino qué es un Servicio. Esto es la ADR como código, no como markdown."
cmd "cat $SOURCE_DIR/core.with-contract.ncl"
cp "$SOURCE_DIR/core.with-contract.ncl" core.ncl
cmd "nickel export core.ncl"
say "Sigue pasando. Los servicios cumplen el contrato."
step "Paso 4 — El rompimiento"
say "Viernes por la tarde, prisa. Servicio nuevo de metrics, owner mañana."
cmd "cat $SOURCE_DIR/core.broken.ncl"
cp "$SOURCE_DIR/core.broken.ncl" core.ncl
echo -e "${CYAN}\$ nickel export core.ncl${NC}"
sleep 1
nickel export core.ncl 2>&1 || true
sleep 4
say "El contrato no me dejó. No es un linter post-hoc — es validación al guardar."
say "Si esto fuera un PR, el CI falla antes de pasar a review."
step "Paso 5 — Arreglo y cierre"
cp "$SOURCE_DIR/core.with-contract.ncl" core.ncl
cmd "nickel export core.ncl"
cmd "wc -l core.ncl"
say "Doce líneas. Nickel solo. Sin daemon, sin frameworks."
say "Esto se hace en cinco minutos en cualquier proyecto vuestro."
sleep 3
PLAY
chmod +x "$PLAY_SCRIPT"
# Run inside asciinema
asciinema rec --command "$PLAY_SCRIPT '$WORK_DIR' '$SOURCE_DIR'" --title "OntoRef WG — mini-demo" "$CAST_FILE"
rm -f "$PLAY_SCRIPT"
rm -rf "$WORK_DIR"
echo ""
echo "Grabación completa: $CAST_FILE"
echo "Para reproducir:"
echo " asciinema play $CAST_FILE"

View file

@ -0,0 +1,235 @@
# Guion mini-demo — Owner por servicio (Opción A)
Tiempo total: 8-10 minutos. Lo que hay que teclear y lo que hay que decir,
paso a paso. Para la opción B, el ritmo es similar pero con más densidad
técnica — ver `option-b-layers/README.md`.
## Pre-requisitos en la máquina del presentador
- Terminal con tipografía visible desde la última fila del coworking
- Editor con resaltado de sintaxis para Nickel (idealmente) o cualquier editor
- Pre-cargado en una pestaña: el `core.ncl` final (`with-contract.ncl`) por si
hay bloqueo
- `/tmp/demo-wg/` vacío antes de empezar
## Pre-paso (cero, no contar como demo)
Limpiar y posicionarse:
```bash
rm -rf /tmp/demo-wg && mkdir /tmp/demo-wg && cd /tmp/demo-wg
clear
```
---
## Paso 1 — directorio vacío (~30s)
**Teclear:**
```bash
pwd
ls
```
**Decir:**
> Esto es lo que cualquiera de vosotros tiene antes de empezar. Sin framework,
> sin scaffold, sin instalar nada. Vamos a montar el esqueleto del approach
> con Nickel solo.
---
## Paso 2 — escribir `core.ncl` mínimo (~2 min)
**Abrir editor:**
```bash
$EDITOR core.ncl
```
**Escribir** (despacio, comentando mientras se escribe):
```nickel
{
axiom = "Cada servicio en producción tiene un owner identificable",
services = [
{ name = "api", owner = "backend" },
{ name = "frontend", owner = "frontend" },
],
}
```
**Decir mientras tecleas:**
> Voy a escribir lo que quiero que sea cierto en mi proyecto: que cada
> servicio tenga un dueño identificable. Lo que pongo aquí es texto, todavía
> no es contrato. Solo dice que la palabra `axiom` y la palabra `services`
> existen.
**Validar:**
```bash
nickel export core.ncl
```
**Esperar a ver:** el JSON evaluado, sin errores.
**Decir:**
> Vale, esto pasa. Pero ojo: aquí no he validado nada todavía. Es como tener
> un YAML con sintaxis correcta. La estructura es lo único que se garantiza.
> Si mañana añado un servicio sin owner, esto sigue exportando sin quejarse.
> Vamos a arreglar eso.
---
## Paso 3 — añadir contrato (~3 min)
**Editar para que quede así:**
```nickel
let Service = {
name | String,
owner | String,
} in
{
axiom = "Cada servicio en producción tiene un owner identificable",
services | Array Service = [
{ name = "api", owner = "backend" },
{ name = "frontend", owner = "frontend" },
],
}
```
**Decir mientras tecleas:**
> Esto es el contrato. Defino qué es un Servicio: tiene un nombre y un owner,
> ambos strings. Y le aplico el contrato a la lista — todos los elementos
> tienen que cumplir la forma. Esto, en un proyecto real, es lo que en
> markdown llamaríais una ADR. La diferencia es que aquí la ADR es código.
**Validar:**
```bash
nickel export core.ncl
```
**Esperar a ver:** el JSON evaluado, sin errores.
**Decir:**
> Sigue pasando. Los servicios cumplen el contrato.
---
## Paso 4 — el rompimiento (~2 min)
**Decir antes de tocar el código:**
> Vale. Ahora viernes por la tarde, hay prisa, hay que meter un servicio nuevo
> de métricas. Owner todavía no asignado, ya lo arreglo el lunes.
**Editar para añadir el servicio sin owner:**
```nickel
services | Array Service = [
{ name = "api", owner = "backend" },
{ name = "frontend", owner = "frontend" },
{ name = "metrics" },
],
```
**Validar:**
```bash
nickel export core.ncl
```
**Esperar a ver:** error que menciona `missing definition for 'owner'` y
señala la línea del contrato y el record que viola.
**LEER EL ERROR EN VOZ ALTA, despacio.**
**Decir:**
> El contrato no me dejó. No es un linter post-hoc, no es un test que tarda
> cinco minutos en CI. Es la validación del archivo, ahora mismo, en mi
> editor. Si esto fuera un PR, el CI falla antes de pasar a review. Tres
> meses después, cuando metrics se rompa, no va a ser que nadie sepa quién
> lo mantiene.
**Pausa larga.** Dejar que la sala procese.
---
## Paso 5 — arreglar y cierre (~1.5 min)
**Editar para añadir owner:**
```nickel
{ name = "metrics", owner = "platform" },
```
**Validar:**
```bash
nickel export core.ncl
```
**Esperar a ver:** el JSON con el servicio nuevo incluido.
**Decir:**
> Y arreglarlo es trivial. Pongo el owner, pasa. La fricción está en la
> decisión, no en escribir el código.
**Mostrar tamaño:**
```bash
wc -l core.ncl
```
**Esperar a ver:** ~12 líneas.
**Decir:**
> Doce líneas. Esto es Nickel solo. Sin daemon, sin CLI mía, sin proyecto
> enorme. Lo de antes lo construyes en cinco minutos en cualquier proyecto
> vuestro.
---
## Volver a slides
Cierra el editor (no la terminal — quédate en `/tmp/demo-wg`). Cambia a
slidev. La siguiente slide hace el contraste con un `.ontology/` ya crecido.
## Plan B si algo se cuelga
| Síntoma | Acción |
|---|---|
| Editor crashea al escribir | `cp` el archivo del paso correspondiente: `cp /Users/Akasha/Development/ontoref/assets/work-group-ore/demo/option-a-owner/core.with-contract.ncl ./core.ncl` |
| `nickel` no responde | Plan B audiovisual: `asciinema play /Users/Akasha/Development/ontoref/assets/work-group-ore/demo/recording/demo.cast` |
| Te quedas en blanco mid-demo | Pausa, di "voy a copiar la versión completa para no perder tiempo", `cp core.with-contract.ncl ./core.ncl`, sigue desde paso 4 |
| Pregunta inesperada que para la demo | Apunta la pregunta en pizarra, di "lo retomamos después de la demo", sigue |
> **Nota técnica**: usamos `nickel export` (no `nickel typecheck`) porque los
> contratos `| Array Service` y `std.contract.custom` se evalúan en runtime,
> no durante el chequeo de tipos estáticos. `nickel export` evalúa la
> expresión a JSON y dispara los contratos en el camino — si alguno falla, el
> error sale por `stderr`. Es el comando que mejor refleja "qué pasa cuando
> tu CI valida el archivo".
## Errores comunes durante la demo
- **No esperar a que la sala lea el error**: la pausa larga después del fallo
del typecheck es donde aterriza el insight. Cuenta hasta cinco mentalmente.
- **Hablar más rápido cuando uno está nervioso**: el tempo de la demo es lento
a propósito. Si te oyes acelerando, respira.
- **Disculparse por la simpleza**: NO lo hagas. La simpleza ES el argumento.
Si dices "esto es muy simple, en producción es más complicado", pierdes la
fuerza del ejemplo.

View file

@ -0,0 +1,125 @@
# Setup — Working Group OntoRef
Lo que tenéis que tener instalado y preparado **antes** de la sesión 1 del
working group. Tiempo estimado: 20-30 minutos si todo va bien, 1 hora si toca
debuggear instalación.
## Requisitos previos
- macOS o Linux (ambos probados; Windows no soportado en este ciclo)
- Acceso a terminal y editor de texto cualquiera (vim, neovim, vscode, zed,
helix, lo que uséis)
- Conexión a internet en la sesión (para clonar repos)
## 1. Instalar Nickel
Nickel es el lenguaje de contratos. Se instala como un binario único.
### macOS
```bash
brew install nickel-lang
```
Si no usáis brew:
```bash
curl -L https://github.com/tweag/nickel/releases/latest/download/nickel-x86_64-apple-darwin --output ~/.local/bin/nickel
chmod +x ~/.local/bin/nickel
```
### Linux
```bash
curl -L https://github.com/tweag/nickel/releases/latest/download/nickel-x86_64-unknown-linux-gnu --output ~/.local/bin/nickel
chmod +x ~/.local/bin/nickel
```
(Asegúrate de que `~/.local/bin` está en tu `$PATH`.)
### Verificar
```bash
nickel --version
```
Debe mostrar `nickel 1.16.0` o superior.
## 2. Instalar la CLI de OntoRef
OntoRef es la herramienta que opera sobre `.ontology/` y se construye con
`cargo`. Necesitáis tener Rust instalado (`rustup` si no lo tenéis).
```bash
git clone https://github.com/jesusperez-pro/ontoref.git ~/dev/ontoref
cd ~/dev/ontoref
cargo install --path . --bin ontoref
```
(El path del repo y la URL son orientativos — si Jesús os da uno distinto,
usad el que os pase.)
### Verificar
```bash
ontoref --version
ontoref describe --help
```
Si `ontoref describe --help` devuelve la lista de subcomandos, está listo.
## 3. Preparar lo que traéis a la sesión
### Un proyecto vuestro
Cualquier proyecto que tengáis vivo y conozcáis bien. **Rust o no, da igual.**
Lo que importa es que sepáis hablar de él. Si tenéis varios, traed UNO — el
que más os duela en términos de coherencia perdida.
### Una decisión arquitectónica recordada
Algo que decidisteis en su momento y que sigue vivo en el código:
- "Usamos PostgreSQL en vez de SQLite porque…"
- "Todas las APIs públicas tienen rate-limiting porque…"
- "El módulo de auth no importa de billing porque…"
- "Cualquier campo público de la API tiene que estar versionado porque…"
No necesita ser sofisticada. Necesita ser **vuestra** y **viva**.
### Disposición a romperla
Vamos a modelar esa decisión como contrato y luego vamos a intentar violarla
en vivo para ver el typecheck rechazándola. Si sois posesivos con vuestras
decisiones, el ejercicio se hace cuesta arriba.
## 4. Comprobaciones finales (24h antes)
Ejecutad esto en una terminal limpia:
```bash
nickel --version # debe responder
ontoref --version # debe responder
mkdir -p ~/tmp/wg-test && cd ~/tmp/wg-test
echo '{ x = 1 }' > test.ncl
nickel export test.ncl # debe imprimir {"x": 1}
```
Si los cuatro comandos pasan, estáis listos.
## Si algo falla
- **Antes de la sesión**: avisad por el grupo de Telegram. Es mejor 30 minutos
de troubleshooting compartido que perder 30 minutos de la sesión.
- **Durante la sesión**: pareja con quien sí tiene el setup hasta que se
resuelva. La sesión no espera, pero tampoco se deja a nadie atrás.
## Recursos opcionales (no necesarios para la sesión 1)
- [Nickel Documentation](https://nickel-lang.org/) — para hojearlo si queréis
contexto
- [OntoRef README](https://github.com/jesusperez-pro/ontoref) — la herramienta
por dentro
No hace falta leerlos antes. Lo que se necesita aprender, se aprende en
sesión.

View file

@ -28,6 +28,8 @@ uuid = { workspace = true, optional = true }
axum-server = { version = "0.7", features = ["tls-rustls"], optional = true }
rmcp = { version = "1", features = ["server", "transport-io", "transport-streamable-http-server"], optional = true }
schemars = { version = "1", optional = true }
async-graphql = { version = "=8.0.0-rc.5", optional = true }
async-graphql-axum = { version = "=8.0.0-rc.5", optional = true }
thiserror = { workspace = true }
dashmap = { workspace = true }
clap = { workspace = true }
@ -44,12 +46,13 @@ ontoref-ontology = { path = "../ontoref-ontology", features = ["derive"] }
libc = { workspace = true }
[features]
default = ["db", "nats", "ui", "mcp"]
default = ["db", "nats", "ui", "mcp", "graphql"]
db = ["stratum-db/remote"]
nats = ["dep:platform-nats"]
ui = ["dep:tera", "dep:toml", "dep:uuid"]
tls = ["ui", "dep:axum-server"]
mcp = ["ui", "dep:rmcp", "dep:schemars"]
graphql = ["dep:async-graphql", "dep:async-graphql-axum", "axum/ws"]
[dev-dependencies]
tokio-test = { workspace = true }

View file

@ -1,5 +1,7 @@
use std::net::IpAddr;
use std::path::PathBuf;
#[cfg(any(feature = "mcp", feature = "graphql"))]
use std::sync::atomic::AtomicBool;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
@ -92,6 +94,75 @@ fn is_uuid_v4(s: &str) -> bool {
.all(|(i, &c)| matches!(i, 8 | 13 | 18 | 23) || c.is_ascii_hexdigit())
}
/// Runtime on/off flags for optional services compiled into the binary.
///
/// Toggles are `AtomicBool` so middleware reads are a single load instruction
/// with no lock contention on the hot path.
pub struct ServiceFlags {
#[cfg(feature = "mcp")]
pub mcp: AtomicBool,
#[cfg(feature = "graphql")]
pub graphql: AtomicBool,
}
impl ServiceFlags {
pub fn new() -> Arc<Self> {
Arc::new(Self {
#[cfg(feature = "mcp")]
mcp: AtomicBool::new(true),
#[cfg(feature = "graphql")]
graphql: AtomicBool::new(true),
})
}
/// Returns `true` if the named service is enabled (or not compiled in).
pub fn is_enabled(&self, service: &str) -> bool {
match service {
#[cfg(feature = "mcp")]
"mcp" => self.mcp.load(Ordering::Relaxed),
#[cfg(feature = "graphql")]
"graphql" => self.graphql.load(Ordering::Relaxed),
_ => false,
}
}
/// Toggle a named service. Returns the new state, or `None` if the service
/// is unknown or not compiled in.
#[cfg_attr(
not(any(feature = "mcp", feature = "graphql")),
allow(unused_variables)
)]
pub fn set(&self, service: &str, enabled: bool) -> Option<bool> {
match service {
#[cfg(feature = "mcp")]
"mcp" => {
self.mcp.store(enabled, Ordering::Relaxed);
Some(enabled)
}
#[cfg(feature = "graphql")]
"graphql" => {
self.graphql.store(enabled, Ordering::Relaxed);
Some(enabled)
}
_ => None,
}
}
}
// Cannot be derived: AtomicBool::default() is false, but services start
// enabled.
#[allow(clippy::derivable_impls)]
impl Default for ServiceFlags {
fn default() -> Self {
Self {
#[cfg(feature = "mcp")]
mcp: AtomicBool::new(true),
#[cfg(feature = "graphql")]
graphql: AtomicBool::new(true),
}
}
}
/// Shared application state injected into handlers.
#[derive(Clone)]
pub struct AppState {
@ -143,9 +214,36 @@ pub struct AppState {
/// accepts this password and creates a "_daemon" session with Role::Admin.
#[cfg(feature = "ui")]
pub daemon_admin_hash: Option<String>,
/// Argon2id PHC hash of the GraphQL read token from
/// `config.graphql.read_token_hash`. `None` means GraphQL endpoints
/// require no dedicated token.
#[cfg(feature = "graphql")]
pub graphql_read_token_hash: Option<String>,
/// Runtime enable/disable flags for optional services (MCP, GraphQL).
/// Shared across all clones of AppState — toggling affects all connections.
pub service_flags: Arc<ServiceFlags>,
}
impl AppState {
/// Verify a raw token against `graphql_read_token_hash`.
///
/// Returns `true` when:
/// - no hash is configured (`None`) — open access, or
/// - the token matches the configured argon2id hash.
#[cfg(feature = "graphql")]
pub fn verify_graphql_token(&self, token: &str) -> bool {
let Some(ref hash) = self.graphql_read_token_hash else {
return true;
};
use crate::registry::{verify_keys_list, KeyEntry, Role};
let synthetic = vec![KeyEntry {
role: Role::Viewer,
hash: hash.clone(),
label: "graphql".into(),
}];
verify_keys_list(&synthetic, token).is_some()
}
fn touch_activity(&self) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
@ -367,6 +465,11 @@ pub fn router(state: AppState) -> axum::Router {
put(project_config_update),
);
// Service toggle endpoints — daemon admin protected.
let app = app
.route("/services", get(services_status))
.route("/services/{service}", put(service_toggle));
// Session endpoints — gated on ui feature (requires SessionStore).
#[cfg(feature = "ui")]
let app = app
@ -407,18 +510,54 @@ pub fn router(state: AppState) -> axum::Router {
};
// MCP streamable-HTTP endpoint — stateless per-request factory.
// Toggle middleware checks service_flags.mcp before forwarding.
#[cfg(feature = "mcp")]
let app = {
use rmcp::transport::streamable_http_server::{
session::local::LocalSessionManager, StreamableHttpServerConfig, StreamableHttpService,
};
let mcp_flags = Arc::clone(&state.service_flags);
let mcp_state = state.clone();
let mcp_svc = StreamableHttpService::new(
move || Ok(crate::mcp::OntoreServer::new(mcp_state.clone())),
std::sync::Arc::new(LocalSessionManager::default()),
StreamableHttpServerConfig::default(),
);
app.nest_service("/mcp", mcp_svc)
let mcp_router =
axum::Router::new()
.fallback_service(mcp_svc)
.layer(axum::middleware::from_fn(
move |req: axum::extract::Request, next: axum::middleware::Next| {
let flags = Arc::clone(&mcp_flags);
async move {
if !flags.mcp.load(Ordering::Relaxed) {
return StatusCode::SERVICE_UNAVAILABLE.into_response();
}
next.run(req).await
}
},
));
app.nest("/mcp", mcp_router)
};
// GraphQL endpoint — schema built once with AppState; GraphiQL at GET /graphql.
// Toggle middleware checks service_flags.graphql before forwarding.
#[cfg(feature = "graphql")]
let app = {
let gql_flags = Arc::clone(&state.service_flags);
let gql_router =
crate::graphql::graphql_router(&state).route_layer(axum::middleware::from_fn(
move |req: axum::extract::Request, next: axum::middleware::Next| {
let flags = Arc::clone(&gql_flags);
async move {
if !flags.graphql.load(Ordering::Relaxed) {
return StatusCode::SERVICE_UNAVAILABLE.into_response();
}
next.run(req).await
}
},
));
app.nest("/graphql", gql_router)
};
app
@ -3504,6 +3643,130 @@ impl IntoResponse for crate::error::DaemonError {
}
}
// ── Service toggles ──────────────────────────────────────────────────────────
#[derive(Serialize)]
struct ServiceEntry {
service: &'static str,
compiled: bool,
enabled: bool,
}
/// List all optional services with their compiled-in and runtime-enabled
/// status.
async fn services_status(State(state): State<AppState>) -> impl IntoResponse {
state.touch_activity();
let services = vec![
ServiceEntry {
service: "mcp",
#[cfg(feature = "mcp")]
compiled: true,
#[cfg(not(feature = "mcp"))]
compiled: false,
#[cfg(feature = "mcp")]
enabled: state.service_flags.mcp.load(Ordering::Relaxed),
#[cfg(not(feature = "mcp"))]
enabled: false,
},
ServiceEntry {
service: "graphql",
#[cfg(feature = "graphql")]
compiled: true,
#[cfg(not(feature = "graphql"))]
compiled: false,
#[cfg(feature = "graphql")]
enabled: state.service_flags.graphql.load(Ordering::Relaxed),
#[cfg(not(feature = "graphql"))]
enabled: false,
},
];
Json(services)
}
#[derive(Deserialize)]
struct ServiceToggleRequest {
// used in the #[cfg(feature = "ui")] handler; appears dead without that feature
#[cfg_attr(not(feature = "ui"), allow(dead_code))]
enabled: bool,
}
/// Enable or disable a named optional service at runtime. Daemon admin Bearer
/// required.
///
/// Gated on `ui` because daemon admin auth (`daemon_admin_hash`) requires the
/// session store compiled in with the `ui` feature. Without `ui`, the endpoint
/// returns 501 — no auth surface exists to protect the toggle.
#[cfg(feature = "ui")]
async fn service_toggle(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
Path(service): Path<String>,
Json(req): Json<ServiceToggleRequest>,
) -> impl IntoResponse {
state.touch_activity();
let Some(ref admin_hash) = state.daemon_admin_hash else {
return (
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"error": "daemon admin auth not configured"})),
)
.into_response();
};
let bearer = headers
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(str::trim)
.filter(|s| !s.is_empty());
let Some(token) = bearer else {
return (
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"error": "Authorization: Bearer <admin-token> required"})),
)
.into_response();
};
let key_entry = crate::registry::KeyEntry {
role: crate::registry::Role::Admin,
hash: admin_hash.clone(),
label: "daemon-admin".to_string(),
};
if crate::registry::verify_keys_list(std::slice::from_ref(&key_entry), token).is_none() {
return (
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({"error": "invalid admin credentials"})),
)
.into_response();
}
match state.service_flags.set(&service, req.enabled) {
Some(new_state) => {
tracing::info!(service = %service, enabled = new_state, "service toggled");
Json(serde_json::json!({"service": service, "enabled": new_state})).into_response()
}
None => (
StatusCode::NOT_FOUND,
Json(
serde_json::json!({"error": format!("unknown or not compiled service: {service}")}),
),
)
.into_response(),
}
}
#[cfg(not(feature = "ui"))]
async fn service_toggle(
State(_state): State<AppState>,
Path(_service): Path<String>,
Json(_req): Json<ServiceToggleRequest>,
) -> impl IntoResponse {
(
StatusCode::NOT_IMPLEMENTED,
Json(
serde_json::json!({"error": "service toggle requires the ui feature (daemon admin auth)"}),
),
)
}
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicU64;
@ -3575,6 +3838,9 @@ mod tests {
ncl_write_lock: Arc::new(crate::ui::ncl_write::NclWriteLock::new()),
#[cfg(feature = "ui")]
daemon_admin_hash: None,
#[cfg(feature = "graphql")]
graphql_read_token_hash: None,
service_flags: ServiceFlags::new(),
}
}

View file

@ -33,6 +33,9 @@ pub struct DaemonNclConfig {
#[cfg(feature = "db")]
#[serde(default)]
pub db: DbConfig,
#[cfg(feature = "graphql")]
#[serde(default)]
pub graphql: GraphqlConfig,
}
/// `ui` section — template and asset paths, optional TLS cert overrides.
@ -202,6 +205,19 @@ pub struct DaemonRuntimeConfig {
pub notification_ack_required: Vec<String>,
}
/// `graphql` section — GraphQL endpoint access control (feature-gated).
///
/// `read_token_hash` is an argon2id PHC string produced by
/// `ontoref-daemon --hash-password <token>`. Empty string disables auth.
#[cfg(feature = "graphql")]
#[derive(Debug, Deserialize, Default, ConfigFields)]
#[config_section(id = "graphql", ncl_file = ".ontoref/config.ncl")]
pub struct GraphqlConfig {
/// Argon2id PHC hash of the read token. Empty = no auth required.
#[serde(default)]
pub read_token_hash: String,
}
/// `db` section — SurrealDB connection (feature-gated).
#[cfg(feature = "db")]
#[derive(Debug, Deserialize, Default, ConfigFields)]

File diff suppressed because it is too large Load diff

View file

@ -18,6 +18,8 @@ pub mod config;
pub mod config_coherence;
pub mod error;
pub mod federation;
#[cfg(feature = "graphql")]
pub mod graphql;
#[cfg(feature = "mcp")]
pub mod mcp;
#[cfg(feature = "nats")]

View file

@ -868,6 +868,17 @@ async fn main() {
}
};
// Load GraphQL read token hash from config (feature-gated).
#[cfg(feature = "graphql")]
let graphql_read_token_hash: Option<String> = loaded_ncl_config.as_ref().and_then(|c| {
let h = c.graphql.read_token_hash.trim();
if h.is_empty() {
None
} else {
Some(h.to_owned())
}
});
// Capture display values before they are moved into AppState.
#[cfg(feature = "ui")]
let project_root_str = project_root.display().to_string();
@ -915,6 +926,9 @@ async fn main() {
ncl_write_lock: Arc::new(ontoref_daemon::ui::ncl_write::NclWriteLock::new()),
#[cfg(feature = "ui")]
daemon_admin_hash: daemon_admin_hash.clone(),
#[cfg(feature = "graphql")]
graphql_read_token_hash: graphql_read_token_hash.clone(),
service_flags: ontoref_daemon::api::ServiceFlags::new(),
}
}
#[cfg(not(feature = "nats"))]
@ -949,6 +963,9 @@ async fn main() {
ncl_write_lock: Arc::new(ontoref_daemon::ui::ncl_write::NclWriteLock::new()),
#[cfg(feature = "ui")]
daemon_admin_hash,
#[cfg(feature = "graphql")]
graphql_read_token_hash,
service_flags: ontoref_daemon::api::ServiceFlags::new(),
}
}
};
@ -1036,6 +1053,10 @@ async fn main() {
return;
}
// Capture before state is moved into the router.
#[cfg(feature = "graphql")]
let graphql_auth_protected = state.graphql_read_token_hash.is_some();
let app = api::router(state).layer(TraceLayer::new_for_http().make_span_with(
|req: &axum::http::Request<_>| {
// Health-check endpoints are polled frequently — log at TRACE to
@ -1072,6 +1093,51 @@ async fn main() {
info!(addr = %addr, "listening");
// ── Active feature log ───────────────────────────────────────────────
{
#[allow(unused_mut)]
let mut features: Vec<&str> = Vec::new();
#[cfg(feature = "db")]
{
features.push("db");
}
#[cfg(feature = "nats")]
{
features.push("nats");
}
#[cfg(feature = "ui")]
{
features.push("ui");
}
#[cfg(feature = "mcp")]
{
features.push("mcp");
}
#[cfg(feature = "graphql")]
{
features.push("graphql");
}
#[cfg(feature = "tls")]
{
features.push("tls");
}
info!(features = ?features, "active features");
}
#[cfg(feature = "graphql")]
{
let auth_status = if graphql_auth_protected {
"token-protected"
} else {
"open (no token configured)"
};
info!(
endpoint = "/graphql",
auth = auth_status,
"GraphQL available"
);
}
#[cfg(feature = "ui")]
if let Some((ref tdir, ref public)) = ui_startup {
#[cfg(feature = "tls")]

View file

@ -306,6 +306,7 @@ impl OntoreServer {
.with_async_tool::<ConfigCoherenceTool>()
.with_async_tool::<ConfigQuickrefTool>()
.with_async_tool::<ConfigUpdateTool>()
.with_async_tool::<VaultStatusTool>()
}
fn project_ctx(&self, slug: Option<&str>) -> ProjectCtx {
@ -346,9 +347,66 @@ impl OntoreServer {
}
}
// ── MCP tool catalog (ADR-015)
// ─────────────────────────────────────────────────
/// All MCP tools registered via `#[ontoref_derive::onto_mcp_tool(...)]`,
/// sorted by tool name. Reads the linker-built inventory at runtime — no
/// allocation of catalog state, no daemon-state dependency.
pub fn catalog() -> Vec<&'static ontoref_ontology::McpToolEntry> {
let mut tools: Vec<&'static ontoref_ontology::McpToolEntry> =
inventory::iter::<ontoref_ontology::McpToolEntry>().collect();
tools.sort_by_key(|t| t.name);
tools
}
/// Render a single catalog entry into the JSON shape returned by the
/// `ontoref_help` tool. `constraint` is mapped to `required: bool` and an
/// optional `default` field; non-empty `kind` is surfaced as a type hint.
fn render_tool_for_help(t: &ontoref_ontology::McpToolEntry) -> serde_json::Value {
let params: Vec<serde_json::Value> = t
.params
.iter()
.map(|p| {
let mut entry = serde_json::Map::new();
entry.insert("name".into(), serde_json::Value::String(p.name.into()));
if !p.kind.is_empty() {
entry.insert("kind".into(), serde_json::Value::String(p.kind.into()));
}
entry.insert(
"required".into(),
serde_json::Value::Bool(p.constraint == "required"),
);
if let Some(default) = p.constraint.strip_prefix("default=") {
entry.insert("default".into(), serde_json::Value::String(default.into()));
}
if !p.description.is_empty() {
entry.insert(
"description".into(),
serde_json::Value::String(p.description.into()),
);
}
serde_json::Value::Object(entry)
})
.collect();
serde_json::json!({
"name": t.name,
"description": t.description,
"category": t.category,
"params": params,
})
}
// ── Tool: list_projects
// ─────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_list_projects",
description = "List all available projects. In single-project mode returns the current \
project name.",
category = "discovery"
)]
struct ListProjectsTool;
impl ToolBase for ListProjectsTool {
@ -390,6 +448,13 @@ impl AsyncTool<OntoreServer> for ListProjectsTool {
// ── Tool: search
// ────────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_search",
description = "Free-text search across nodes, ADRs, and modes. Returns kind+id for use with \
ontoref_get.",
category = "discovery",
params = "query:string:required:Free-text search query; project:string:optional:Project slug"
)]
struct SearchTool;
impl ToolBase for SearchTool {
@ -435,6 +500,12 @@ impl AsyncTool<OntoreServer> for SearchTool {
// ── Tool: describe_project
// ──────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_describe",
description = "Architecture overview: README summary and manifest.",
category = "discovery",
params = "project:string:optional:Project slug"
)]
struct DescribeProjectTool;
impl ToolBase for DescribeProjectTool {
@ -496,6 +567,12 @@ impl AsyncTool<OntoreServer> for DescribeProjectTool {
// ── Tool: list_adrs
// ─────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_list_adrs",
description = "List all ADRs with id, title, status, constraint counts.",
category = "discovery",
params = "project:string:optional:Project slug"
)]
struct ListAdrsTool;
impl ToolBase for ListAdrsTool {
@ -580,6 +657,12 @@ impl AsyncTool<OntoreServer> for ListAdrsTool {
// ── Tool: get_adr
// ───────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_get_adr",
description = "Full ADR by id or partial stem (e.g. adr-001).",
category = "ontology",
params = "id:string:required:ADR id or partial stem; project:string:optional:Project slug"
)]
struct GetAdrTool;
impl ToolBase for GetAdrTool {
@ -642,6 +725,12 @@ impl AsyncTool<OntoreServer> for GetAdrTool {
// ── Tool: list_ontology_extensions
// ──────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_list_ontology_extensions",
description = "List extra .ontology/*.ncl files beyond core/state/gate.",
category = "discovery",
params = "project:string:optional:Project slug"
)]
struct ListOntologyExtensionsTool;
impl ToolBase for ListOntologyExtensionsTool {
@ -705,6 +794,12 @@ impl AsyncTool<OntoreServer> for ListOntologyExtensionsTool {
// ── Tool: get_ontology_extension
// ────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_get_ontology_extension",
description = "Export a project-defined .ontology extension by stem (e.g. career, personal).",
category = "ontology",
params = "id:string:required:Extension stem; project:string:optional:Project slug"
)]
struct GetOntologyExtensionTool;
impl ToolBase for GetOntologyExtensionTool {
@ -771,6 +866,12 @@ impl AsyncTool<OntoreServer> for GetOntologyExtensionTool {
// ── Tool: list_modes
// ────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_list_modes",
description = "List all reflection modes with id, trigger, step count.",
category = "discovery",
params = "project:string:optional:Project slug"
)]
struct ListModesTool;
impl ToolBase for ListModesTool {
@ -842,6 +943,12 @@ impl AsyncTool<OntoreServer> for ListModesTool {
// ── Tool: get_mode
// ──────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_get_mode",
description = "Full reflection mode including all steps and preconditions.",
category = "ontology",
params = "id:string:required:Mode id; project:string:optional:Project slug"
)]
struct GetModeTool;
impl ToolBase for GetModeTool {
@ -904,6 +1011,13 @@ impl AsyncTool<OntoreServer> for GetModeTool {
// ── Tool: get_backlog
// ───────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_backlog_list",
description = "List backlog items, optionally filtered by status.",
category = "knowledge",
params = "project:string:optional:Project slug; \
status:string:optional:Open|InProgress|Done|Cancelled"
)]
struct GetBacklogTool;
impl ToolBase for GetBacklogTool {
@ -972,6 +1086,12 @@ impl AsyncTool<OntoreServer> for GetBacklogTool {
// ── Tool: get_node
// ──────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_get_node",
description = "Full ontology node by id substring match.",
category = "ontology",
params = "id:string:required:Node id substring; project:string:optional:Project slug"
)]
struct GetNodeTool;
impl ToolBase for GetNodeTool {
@ -1036,6 +1156,13 @@ impl AsyncTool<OntoreServer> for GetNodeTool {
// ── Tool: ontoref_get (unified dispatcher) ───────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_get",
description = "Retrieve full details of any item by kind+id (from ontoref_search results).",
category = "ontology",
params = "id:string:required:Item id from ontoref_search; kind:string:required:node|adr|mode; \
project:string:optional:Project slug"
)]
struct GetTool;
impl ToolBase for GetTool {
@ -1088,6 +1215,12 @@ impl AsyncTool<OntoreServer> for GetTool {
// ── Tool: ontoref_help
// ────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_help",
description = "List all available ontoref MCP tools with descriptions, parameters, and the \
current active project.",
category = "discovery"
)]
struct HelpTool;
impl ToolBase for HelpTool {
@ -1126,104 +1259,8 @@ impl AsyncTool<OntoreServer> for HelpTool {
.and_then(|g| g.clone());
let available = service.available_projects();
let tools = serde_json::json!([
{ "name": "ontoref_help", "description": "This help. Lists all tools, parameters, and current project.", "params": [] },
{ "name": "ontoref_list_projects", "description": "List all registered projects.", "params": [] },
{ "name": "ontoref_set_project", "description": "Set the default project for this session.",
"params": [{"name": "project", "required": true, "description": "Project slug from ontoref_list_projects"}] },
{ "name": "ontoref_status", "description": "Full dashboard: actors, notifications, backlog stats, ADR/mode counts, manifest.",
"params": [{"name": "project", "required": false}] },
{ "name": "ontoref_search", "description": "Free-text search across nodes, ADRs, and modes. Returns kind+id for use with ontoref_get.",
"params": [{"name": "query", "required": true}, {"name": "project", "required": false}] },
{ "name": "ontoref_get", "description": "Retrieve full details of any item by kind+id (from ontoref_search results).",
"params": [{"name": "id", "required": true}, {"name": "kind", "required": true, "values": ["node", "adr", "mode"]}, {"name": "project", "required": false}] },
{ "name": "ontoref_describe", "description": "Architecture overview: README summary and manifest.",
"params": [{"name": "project", "required": false}] },
{ "name": "ontoref_list_adrs", "description": "List all ADRs with id, title, status, constraint counts.",
"params": [{"name": "project", "required": false}] },
{ "name": "ontoref_get_adr", "description": "Full ADR by id or partial stem (e.g. adr-001).",
"params": [{"name": "id", "required": true}, {"name": "project", "required": false}] },
{ "name": "ontoref_list_ontology_extensions", "description": "List extra .ontology/*.ncl files beyond core/state/gate.",
"params": [{"name": "project", "required": false}] },
{ "name": "ontoref_get_ontology_extension", "description": "Export a project-defined .ontology extension by stem (e.g. career, personal).",
"params": [{"name": "id", "required": true}, {"name": "project", "required": false}] },
{ "name": "ontoref_list_modes", "description": "List all reflection modes with id, trigger, step count.",
"params": [{"name": "project", "required": false}] },
{ "name": "ontoref_get_mode", "description": "Full reflection mode including all steps and preconditions.",
"params": [{"name": "id", "required": true}, {"name": "project", "required": false}] },
{ "name": "ontoref_get_node", "description": "Full ontology node by id substring match.",
"params": [{"name": "id", "required": true}, {"name": "project", "required": false}] },
{ "name": "ontoref_backlog_list", "description": "List backlog items, optionally filtered by status.",
"params": [{"name": "project", "required": false}, {"name": "status", "required": false, "values": ["Open", "InProgress", "Done", "Cancelled"]}] },
{ "name": "ontoref_backlog", "description": "Mutate backlog: add a new item or update an item's status.",
"params": [
{"name": "operation", "required": true, "values": ["add", "update_status"]},
{"name": "project", "required": false},
{"name": "id", "required": false, "note": "required for update_status"},
{"name": "status", "required": false, "note": "required for update_status", "values": ["Open", "InProgress", "Done", "Cancelled"]},
{"name": "title", "required": false, "note": "required for add"},
{"name": "kind", "required": false, "values": ["Feature", "Bug", "Chore", "Research"]},
{"name": "priority", "required": false, "values": ["Critical", "High", "Medium", "Low"]},
{"name": "detail", "required": false}
] },
{ "name": "ontoref_constraints", "description": "All hard and soft architectural constraints extracted from all ADRs.",
"params": [{"name": "project", "required": false}] },
{ "name": "ontoref_qa_list", "description": "List Q&A entries from reflection/qa.ncl. Optionally filter by question substring.",
"params": [{"name": "project", "required": false}, {"name": "filter", "required": false}] },
{ "name": "ontoref_qa_add", "description": "Add a Q&A entry to reflection/qa.ncl (persisted to disk). Use to record architectural knowledge.",
"params": [
{"name": "question", "required": true},
{"name": "answer", "required": false},
{"name": "actor", "required": false, "default": "agent"},
{"name": "tags", "required": false, "note": "array of strings"},
{"name": "related", "required": false, "note": "array of node/ADR ids"},
{"name": "project", "required": false}
] },
{ "name": "ontoref_action_list", "description": "List quick actions from .ontoref/config.ncl quick_actions array.",
"params": [{"name": "project", "required": false}] },
{ "name": "ontoref_action_add", "description": "Create a new reflection mode file in reflection/modes/<id>.ncl.",
"params": [
{"name": "id", "required": true, "note": "short slug, e.g. gen-docs"},
{"name": "label", "required": true},
{"name": "trigger", "required": true},
{"name": "steps", "required": true, "note": "array of step strings"},
{"name": "icon", "required": false, "values": ["book-open", "refresh", "code", "bolt"]},
{"name": "category", "required": false, "values": ["docs", "sync", "analysis", "test"]},
{"name": "actors", "required": false, "note": "array: developer | agent | ci"},
{"name": "project", "required": false}
] },
{ "name": "ontoref_validate_adrs", "description": "Run typed constraint checks for all ADRs. Returns pass/fail per constraint with detail.",
"params": [{"name": "project", "required": false}] },
{ "name": "ontoref_validate", "description": "Run the full project validation suite: ADR constraints, content assets, connections, gate consistency.",
"params": [{"name": "project", "required": false}] },
{ "name": "ontoref_impact", "description": "BFS impact graph from a node: what else is affected if this node changes.",
"params": [
{"name": "node_id", "required": true},
{"name": "depth", "required": false, "default": 2},
{"name": "include_external", "required": false, "note": "traverse cross-project connections"},
{"name": "project", "required": false}
] },
{ "name": "ontoref_guides", "description": "Complete project guide: identity, axioms, practices, constraints, gate state, modes, actor policy, content assets, connections. Canonical entry point for cold-start context.",
"params": [{"name": "project", "required": false}, {"name": "actor", "required": false, "values": ["developer", "agent", "ci", "admin"]}] },
{ "name": "ontoref_bookmark_list", "description": "List saved search bookmarks for the project.",
"params": [{"name": "project", "required": false}, {"name": "filter", "required": false}] },
{ "name": "ontoref_bookmark_add", "description": "Save a search bookmark (node_id + title + optional tags).",
"params": [
{"name": "node_id", "required": true},
{"name": "title", "required": true},
{"name": "kind", "required": false, "values": ["node", "adr", "mode"]},
{"name": "tags", "required": false, "note": "array of strings"},
{"name": "project", "required": false}
] },
{ "name": "ontoref_api_catalog", "description": "Annotated daemon API surface: all HTTP routes with method, path, auth, actors, params, tags.",
"params": [
{"name": "actor", "required": false, "values": ["developer", "agent", "ci", "admin"]},
{"name": "tag", "required": false, "note": "e.g. ontology, projects, search"},
{"name": "auth", "required": false, "values": ["none", "viewer", "admin"]}
] },
{ "name": "ontoref_file_versions", "description": "Per-file reload counters for ontology files. Counter increments on each daemon reload of that file.",
"params": [{"name": "project", "required": false}] },
]);
let tools: Vec<serde_json::Value> =
catalog().iter().map(|t| render_tool_for_help(t)).collect();
Ok(serde_json::json!({
"current_project": current_project,
@ -1236,6 +1273,12 @@ impl AsyncTool<OntoreServer> for HelpTool {
// ── Tool: set_project ────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_set_project",
description = "Set the default project for this session.",
category = "discovery",
params = "project:string:required:Project slug from ontoref_list_projects"
)]
struct SetProjectTool;
impl ToolBase for SetProjectTool {
@ -1297,6 +1340,13 @@ impl AsyncTool<OntoreServer> for SetProjectTool {
// ── Tool: project_status ─────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_status",
description = "Full dashboard: actors, notifications, backlog stats, ADR/mode counts, \
manifest.",
category = "discovery",
params = "project:string:optional:Project slug"
)]
struct ProjectStatusTool;
impl ToolBase for ProjectStatusTool {
@ -1416,6 +1466,51 @@ impl AsyncTool<OntoreServer> for ProjectStatusTool {
serde_json::Value::Null
};
// Extract structured registry topology from manifest so MCP consumers
// don't need to navigate the raw NCL JSON themselves.
let registry = {
let rp = manifest.get("registry_provides");
let participant = rp
.and_then(|v| v.get("participant"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_owned();
let registries = rp.and_then(|v| v.get("registries"));
let default_id = registries
.and_then(|v| v.get("default"))
.and_then(|v| v.as_str())
.unwrap_or("primary");
let entry = registries
.and_then(|v| v.get("registries"))
.and_then(|v| v.as_array())
.and_then(|arr| {
arr.iter()
.find(|e| e.get("id").and_then(|i| i.as_str()) == Some(default_id))
})
.cloned()
.unwrap_or(serde_json::Value::Null);
let namespaces = entry
.get("namespaces")
.and_then(|n| n.get("own"))
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let endpoint = entry
.get("endpoint")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_owned();
if participant.is_empty() {
serde_json::Value::Null
} else {
serde_json::json!({
"participant": participant,
"endpoint": endpoint,
"namespaces": namespaces,
})
}
};
Ok(serde_json::json!({
"project": ctx.root.file_name().and_then(|n| n.to_str()).unwrap_or("unknown"),
"root": ctx.root.display().to_string(),
@ -1424,6 +1519,7 @@ impl AsyncTool<OntoreServer> for ProjectStatusTool {
"backlog": backlog_stats,
"adrs": adr_count,
"modes": mode_count,
"registry": registry,
"manifest": manifest,
}))
}
@ -1432,6 +1528,18 @@ impl AsyncTool<OntoreServer> for ProjectStatusTool {
// ── Tool: backlog
// ─────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_backlog",
description = "Mutate backlog: add a new item or update an item's status.",
category = "knowledge",
params = "operation:string:required:add|update_status; project:string:optional:Project slug; \
id:string:optional:required for update_status; \
status:string:optional:Open|InProgress|Done|Cancelled (required for update_status); \
title:string:optional:required for add; \
kind:string:optional:Feature|Bug|Chore|Research; \
priority:string:optional:Critical|High|Medium|Low; detail:string:optional:Free-text \
detail"
)]
struct BacklogOpTool;
impl ToolBase for BacklogOpTool {
@ -1607,6 +1715,12 @@ fn collect_constraints(
// ── Tool: get_constraints
// ───────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_constraints",
description = "All hard and soft architectural constraints extracted from all ADRs.",
category = "validation",
params = "project:string:optional:Project slug"
)]
struct GetConstraintsTool;
impl ToolBase for GetConstraintsTool {
@ -1678,6 +1792,14 @@ impl AsyncTool<OntoreServer> for GetConstraintsTool {
// ── Tool: qa_list
// ────────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_qa_list",
description = "List Q&A entries from reflection/qa.ncl. Optionally filter by question \
substring.",
category = "knowledge",
params = "project:string:optional:Project slug; filter:string:optional:Question substring \
filter"
)]
struct QaListTool;
impl ToolBase for QaListTool {
@ -1745,6 +1867,15 @@ impl AsyncTool<OntoreServer> for QaListTool {
// ── Tool: qa_add
// ─────────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_qa_add",
description = "Add a Q&A entry to reflection/qa.ncl (persisted to disk). Use to record \
architectural knowledge.",
category = "knowledge",
params = "question:string:required:Question text; answer:string:optional:Answer text; \
actor:string:optional:default=agent; tags:array:optional:Tag strings; \
related:array:optional:Related node or ADR ids; project:string:optional:Project slug"
)]
struct QaAddTool;
impl ToolBase for QaAddTool {
@ -1822,6 +1953,12 @@ impl AsyncTool<OntoreServer> for QaAddTool {
// ── Tool: action_list
// ────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_action_list",
description = "List quick actions from .ontoref/config.ncl quick_actions array.",
category = "knowledge",
params = "project:string:optional:Project slug"
)]
struct ActionListTool;
impl ToolBase for ActionListTool {
@ -1879,6 +2016,16 @@ impl AsyncTool<OntoreServer> for ActionListTool {
// ── Tool: action_add
// ─────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_action_add",
description = "Create a new reflection mode file in reflection/modes/<id>.ncl.",
category = "knowledge",
params = "id:string:required:Mode id slug; label:string:required:Display label; \
trigger:string:required:Trigger description; steps:array:required:Step strings; \
icon:string:optional:book-open|refresh|code|bolt; \
category:string:optional:docs|sync|analysis|test; \
actors:array:optional:developer|agent|ci; project:string:optional:Project slug"
)]
struct ActionAddTool;
impl ToolBase for ActionAddTool {
@ -1980,6 +2127,13 @@ impl AsyncTool<OntoreServer> for ActionAddTool {
// ── Tool: validate_adrs
// ──────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_validate_adrs",
description = "Run typed constraint checks for all ADRs. Returns pass/fail per constraint \
with detail.",
category = "validation",
params = "project:string:optional:Project slug"
)]
struct ValidateAdrsTool;
impl ToolBase for ValidateAdrsTool {
@ -2049,6 +2203,14 @@ struct ImpactInput {
include_external: Option<bool>,
}
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_impact",
description = "BFS impact graph from a node: what else is affected if this node changes.",
category = "ontology",
params = "node_id:string:required:Source node id; depth:u32:optional:default=2; \
include_external:bool:optional:Traverse cross-project edges; \
project:string:optional:Project slug"
)]
struct ImpactTool;
impl ToolBase for ImpactTool {
@ -2106,6 +2268,13 @@ impl AsyncTool<OntoreServer> for ImpactTool {
// ── Tool: validate_project
// ────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_validate",
description = "Run the full project validation suite: ADR constraints, content assets, \
connections, gate consistency.",
category = "validation",
params = "project:string:optional:Project slug"
)]
struct ValidateProjectTool;
impl ToolBase for ValidateProjectTool {
@ -2173,6 +2342,14 @@ impl AsyncTool<OntoreServer> for ValidateProjectTool {
// ── Tool: guides
// ──────────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_guides",
description = "Complete project guide: identity, axioms, practices, constraints, gate state, \
modes, actor policy, content assets, connections. Canonical entry point for \
cold-start context.",
category = "discovery",
params = "project:string:optional:Project slug; actor:string:optional:developer|agent|ci|admin"
)]
struct GuidesTool;
impl ToolBase for GuidesTool {
@ -2233,6 +2410,14 @@ impl AsyncTool<OntoreServer> for GuidesTool {
// ── Tool: api_catalog
// ────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_api_catalog",
description = "Annotated daemon API surface: all HTTP routes with method, path, auth, actors, \
params, tags.",
category = "discovery",
params = "actor:string:optional:developer|agent|ci|admin; tag:string:optional:Semantic tag \
filter; auth:string:optional:none|viewer|admin"
)]
struct ApiCatalogTool;
impl ToolBase for ApiCatalogTool {
@ -2305,6 +2490,13 @@ impl AsyncTool<OntoreServer> for ApiCatalogTool {
// ── Tool: file_versions
// ──────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_file_versions",
description = "Per-file reload counters for ontology files. Counter increments on each daemon \
reload of that file.",
category = "discovery",
params = "project:string:optional:Project slug"
)]
struct FileVersionsTool;
impl ToolBase for FileVersionsTool {
@ -2433,6 +2625,12 @@ impl ServerHandler for OntoreServer {
// ── Tool: bookmark_list
// ─────────────────────────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_bookmark_list",
description = "List saved search bookmarks for the project.",
category = "knowledge",
params = "project:string:optional:Project slug; filter:string:optional:Substring filter"
)]
struct BookmarkListTool;
impl ToolBase for BookmarkListTool {
@ -2507,6 +2705,14 @@ impl AsyncTool<OntoreServer> for BookmarkListTool {
// ── Tool: bookmark_add
// ────────────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_bookmark_add",
description = "Save a search bookmark (node_id + title + optional tags).",
category = "knowledge",
params = "node_id:string:required:Node id; title:string:required:Bookmark title; \
kind:string:optional:node|adr|mode; tags:array:optional:Tag strings; \
project:string:optional:Project slug"
)]
struct BookmarkAddTool;
impl ToolBase for BookmarkAddTool {
@ -2588,6 +2794,13 @@ impl AsyncTool<OntoreServer> for BookmarkAddTool {
// ── Tool: project_config (read full export or single section)
// ─────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_project_config",
description = "Read a project's config surface. Returns the full NCL export (merged with \
active overrides) for all sections, or a single section when section is given.",
category = "config",
params = "project:string:optional:Project slug; section:string:optional:Section id"
)]
struct ProjectConfigTool;
impl ToolBase for ProjectConfigTool {
@ -2658,6 +2871,13 @@ impl AsyncTool<OntoreServer> for ProjectConfigTool {
// ── Tool: config_coherence
// ──────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_config_coherence",
description = "Run the multi-consumer coherence check for a project's config surface. Reports \
unclaimed NCL fields and consumer fields missing from the NCL export.",
category = "config",
params = "project:string:optional:Project slug; section:string:optional:Section id"
)]
struct ConfigCoherenceTool;
impl ToolBase for ConfigCoherenceTool {
@ -2725,6 +2945,14 @@ impl AsyncTool<OntoreServer> for ConfigCoherenceTool {
// ── Tool: config_quickref
// ───────────────────────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_config_quickref",
description = "Generate living config documentation for a project. Combines current NCL \
values, manifest rationales, per-section meta records, override history, and \
coherence status.",
category = "config",
params = "project:string:optional:Project slug; section:string:optional:Section id"
)]
struct ConfigQuickrefTool;
impl ToolBase for ConfigQuickrefTool {
@ -2790,6 +3018,16 @@ impl AsyncTool<OntoreServer> for ConfigQuickrefTool {
// ── Tool: config_update (override layer mutation)
// ────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_config_update",
description = "Mutate a config section via the override layer. Generates a \
{section}.overrides.ncl file merged after the original NCL. dry_run defaults \
to true.",
category = "config",
params = "project:string:required:Project slug; section:string:required:Section id; \
values:json:required:JSON object with field updates; \
dry_run:bool:optional:default=true; reason:string:optional:Audit reason"
)]
struct ConfigUpdateTool;
impl ToolBase for ConfigUpdateTool {
@ -2901,3 +3139,235 @@ pub async fn serve_stdio(state: AppState) -> anyhow::Result<()> {
.map_err(|e| anyhow::anyhow!("MCP wait error: {e}"))?;
Ok(())
}
// ── Tool: vault_status (ADR-017 / ADR-019) ──────────────────────────────────
#[ontoref_derive::onto_mcp_tool(
name = "ontoref_vault_status",
description = "Read-only credential vault state: vault_id, recipient count, recipient groups \
+ path rules, last logged operation, master_key resolution. No decryption.",
category = "credentials",
params = "project:string:optional:Project slug (defaults to current project)"
)]
struct VaultStatusTool;
impl ToolBase for VaultStatusTool {
type Parameter = ProjectParam;
type Output = serde_json::Value;
type Error = ToolError;
fn name() -> Cow<'static, str> {
"ontoref_vault_status".into()
}
fn description() -> Option<Cow<'static, str>> {
Some(
"Structured credential vault state for the active project (ADR-017 + ADR-019). \
Returns: vault_id, vault_dir, backend, registry_endpoint, master_key (path or \
unresolved-error), access_sops_present (bool), recipient_count, recipient_groups \
(groupkey list), recipient_rules (pathgroups), actor_key_bindings, last_operation \
from logs/access.jsonl. NEVER decrypts credentials."
.into(),
)
}
fn output_schema() -> Option<Arc<JsonObject>> {
None
}
}
impl AsyncTool<OntoreServer> for VaultStatusTool {
async fn invoke(
service: &OntoreServer,
param: ProjectParam,
) -> Result<serde_json::Value, ToolError> {
debug!(tool = "vault_status", project = ?param.project);
let ctx = service.project_ctx(param.project.as_deref());
let project_ncl = ctx.root.join(".ontoref").join("project.ncl");
if !project_ncl.exists() {
return Ok(serde_json::json!({
"ok": false,
"reason": format!("no .ontoref/project.ncl at {}", ctx.root.display()),
}));
}
// Evaluate project.ncl via the cache (consistent with other tools).
let project_json: serde_json::Value = ctx
.cache
.export(&project_ncl, ctx.import_path.as_deref())
.await
.map(|(v, _)| v)
.map_err(|e| ToolError(format!("project.ncl evaluation failed: {e}")))?;
let sops = match project_json.get("sops") {
Some(s) if !s.is_null() => s,
_ => {
return Ok(serde_json::json!({
"ok": false,
"reason": "project.ncl has no sops block — vault not configured",
}));
}
};
let enabled = sops
.get("enabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !enabled {
return Ok(serde_json::json!({
"ok": false,
"reason": "sops.enabled = false in project.ncl",
}));
}
let slug = project_json
.get("slug")
.and_then(|v| v.as_str())
.unwrap_or("");
let vault_id = sops
.get("vault_id")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.unwrap_or(slug)
.to_string();
let vault_backend = sops
.get("vault_backend")
.and_then(|v| v.as_str())
.unwrap_or("restic")
.to_string();
let registry_endpoint = sops
.get("registry_endpoint")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let actor_key_bindings = sops
.get("actor_key_bindings")
.cloned()
.unwrap_or(serde_json::json!({}));
let recipient_groups = sops
.get("recipient_groups")
.cloned()
.unwrap_or(serde_json::json!({}));
let recipient_rules = sops
.get("recipient_rules")
.cloned()
.unwrap_or(serde_json::json!([]));
// master_key path: project override → global config.
let master_key = sops
.get("master_key_path")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(String::from)
.or_else(|| {
let home = std::env::var_os("HOME").map(std::path::PathBuf::from)?;
let global = home.join(".config").join("ontoref").join("config.ncl");
if !global.exists() {
return None;
}
let out = std::process::Command::new("nickel")
.arg("export")
.arg(&global)
.output()
.ok()?;
if !out.status.success() {
return None;
}
let v: serde_json::Value = serde_json::from_slice(&out.stdout).ok()?;
v.get("vault")
.and_then(|x| x.get("master_key_path"))
.and_then(|s| s.as_str())
.filter(|s| !s.is_empty())
.map(String::from)
});
let home = std::env::var_os("HOME")
.map(std::path::PathBuf::from)
.ok_or_else(|| ToolError("HOME not resolvable".into()))?;
let vault_dir = home
.join(".config")
.join("ontoref")
.join("vaults")
.join(&vault_id);
let access_sops = vault_dir.join("access.sops.yaml");
let logs_jsonl = vault_dir.join("logs").join("access.jsonl");
// Recipient count from access.sops.yaml's plaintext sops.age metadata.
// The file format places one '- recipient: age1...' entry per recipient
// in the sops metadata block; counting matching lines avoids needing a
// YAML parser dependency in the daemon.
let recipient_count: u64 = if access_sops.exists() {
std::fs::read_to_string(&access_sops)
.map(|s| {
s.lines()
.filter(|l| l.trim_start().starts_with("- recipient: age1"))
.count() as u64
})
.unwrap_or(0)
} else {
0
};
// Last operation from access.jsonl.
let last_op = if logs_jsonl.exists() {
std::fs::read_to_string(&logs_jsonl)
.ok()
.and_then(|s| s.lines().last().map(String::from))
.and_then(|line| serde_json::from_str::<serde_json::Value>(&line).ok())
} else {
None
};
Ok(serde_json::json!({
"ok": true,
"vault_id": vault_id,
"vault_dir": vault_dir.display().to_string(),
"backend": vault_backend,
"registry_endpoint": registry_endpoint,
"master_key": master_key,
"access_sops_present": access_sops.exists(),
"recipient_count": recipient_count,
"recipient_groups": recipient_groups,
"recipient_rules": recipient_rules,
"actor_key_bindings": actor_key_bindings,
"last_operation": last_op,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catalog_contains_known_tools_and_unique_names() {
let entries = catalog();
assert!(
entries.len() >= 30,
"expected at least 30 MCP tools in inventory, found {}",
entries.len()
);
let names: Vec<&str> = entries.iter().map(|t| t.name).collect();
assert!(names.contains(&"ontoref_help"));
assert!(names.contains(&"ontoref_guides"));
assert!(names.contains(&"ontoref_status"));
assert!(names.contains(&"ontoref_api_catalog"));
let mut sorted_unique = names.clone();
sorted_unique.sort_unstable();
sorted_unique.dedup();
assert_eq!(
sorted_unique.len(),
names.len(),
"duplicate tool names in inventory: {names:?}"
);
for name in &names {
assert!(
name.starts_with("ontoref_"),
"tool name '{name}' does not start with 'ontoref_'"
);
}
}
}

View file

@ -442,9 +442,25 @@ impl ProjectRegistry {
/// Resolve nickel_import_paths entries into a colon-separated
/// NICKEL_IMPORT_PATH string. Relative paths are resolved against the project
/// root.
/// root. The user-global schemas directory `~/.config/ontoref/schemas/` is
/// always appended so files importing `ontoref-project.ncl` (the canonical
/// project schema) resolve correctly even when the project's local
/// nickel_import_paths does not include it.
fn global_schemas_path() -> Option<String> {
let home = std::env::var_os("HOME")?;
let p = Path::new(&home)
.join(".config")
.join("ontoref")
.join("schemas");
p.is_dir().then(|| p.display().to_string())
}
fn resolve_import_path(paths: &[String], root: &Path) -> Option<String> {
let joined = paths
resolve_import_path_inner(paths, root, global_schemas_path().as_deref())
}
fn resolve_import_path_inner(paths: &[String], root: &Path, extra: Option<&str>) -> Option<String> {
let mut entries: Vec<String> = paths
.iter()
.map(|p| {
let candidate = Path::new(p);
@ -454,8 +470,13 @@ fn resolve_import_path(paths: &[String], root: &Path) -> Option<String> {
root.join(candidate).display().to_string()
}
})
.collect::<Vec<_>>()
.join(":");
.collect();
if let Some(s) = extra {
if !entries.iter().any(|e| e == s) {
entries.push(s.to_string());
}
}
let joined = entries.join(":");
if joined.is_empty() {
None
} else {
@ -758,28 +779,55 @@ mod tests {
#[test]
fn empty_paths_returns_none() {
assert_eq!(resolve_import_path(&[], Path::new("/root")), None);
assert_eq!(
resolve_import_path_inner(&[], Path::new("/root"), None),
None
);
}
#[test]
fn absolute_path_returned_as_is() {
let result = resolve_import_path(&["/abs/path".to_string()], Path::new("/root"));
let result =
resolve_import_path_inner(&["/abs/path".to_string()], Path::new("/root"), None);
assert_eq!(result, Some("/abs/path".to_string()));
}
#[test]
fn relative_path_joined_with_root() {
let result = resolve_import_path(&["lib".to_string()], Path::new("/root"));
let result = resolve_import_path_inner(&["lib".to_string()], Path::new("/root"), None);
assert_eq!(result, Some("/root/lib".to_string()));
}
#[test]
fn multiple_paths_colon_separated() {
let result =
resolve_import_path(&["/abs".to_string(), "rel".to_string()], Path::new("/root"));
let result = resolve_import_path_inner(
&["/abs".to_string(), "rel".to_string()],
Path::new("/root"),
None,
);
assert_eq!(result, Some("/abs:/root/rel".to_string()));
}
#[test]
fn extra_global_schema_appended() {
let result = resolve_import_path_inner(
&["/abs".to_string()],
Path::new("/root"),
Some("/global/schemas"),
);
assert_eq!(result, Some("/abs:/global/schemas".to_string()));
}
#[test]
fn extra_global_schema_not_duplicated() {
let result = resolve_import_path_inner(
&["/global/schemas".to_string()],
Path::new("/root"),
Some("/global/schemas"),
);
assert_eq!(result, Some("/global/schemas".to_string()));
}
// ── with_primary slug collision ───────────────────────────────────────
fn minimal_ctx(slug: &str) -> Arc<ProjectContext> {

View file

@ -247,6 +247,141 @@ impl Default for DomainCtx {
}
}
/// Extract registry participant, namespaces, and endpoint from manifest JSON.
/// Returns empty strings/vec when `registry_provides` is absent.
/// Pure projection of `.ontoref/project.ncl::sops` into vault basics.
/// Returns `Some((vault_id, vault_mode))` when sops is enabled, `None`
/// otherwise. vault_mode: "declarative" if `recipient_rules` is a non-empty
/// array, "legacy" otherwise.
fn vault_basics_from_json(project_json: Option<&serde_json::Value>) -> Option<(String, String)> {
let pj = project_json?;
let sops = pj.get("sops")?;
let enabled = sops
.get("enabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !enabled {
return None;
}
let slug = pj.get("slug").and_then(|v| v.as_str()).unwrap_or("");
let vault_id = sops
.get("vault_id")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.unwrap_or(slug)
.to_string();
let has_rules = sops
.get("recipient_rules")
.and_then(|v| v.as_array())
.map(|a| !a.is_empty())
.unwrap_or(false);
let mode = if has_rules { "declarative" } else { "legacy" }.to_string();
Some((vault_id, mode))
}
/// Extract vault state for the UI from a project.ncl JSON
/// (.ontoref/project.ncl::sops). Returns (has_vault, vault_id, vault_mode,
/// recipient_count, last_op_summary). recipient_count is parsed from
/// access.sops.yaml metadata when the file exists locally.
fn extract_vault_ctx(
project_json: Option<&serde_json::Value>,
) -> (bool, String, String, i32, String) {
let Some((vault_id, mode)) = vault_basics_from_json(project_json) else {
return (false, String::new(), String::new(), 0, String::new());
};
// Local files — best-effort, no failure on absence.
let home = std::env::var_os("HOME").map(std::path::PathBuf::from);
let (recipient_count, last_op_summary) = if let Some(home) = home {
let vault_dir = home
.join(".config")
.join("ontoref")
.join("vaults")
.join(&vault_id);
let access_sops = vault_dir.join("access.sops.yaml");
let count: i32 = if access_sops.exists() {
std::fs::read_to_string(&access_sops)
.map(|s| {
// sops emits both YAML (`- recipient: age1...`) and JSON
// (`"recipient": "age1..."`). Match the substring that
// both share — any line containing `recipient` followed
// by an `age1` token.
s.lines()
.filter(|l| l.contains("recipient") && l.contains("age1"))
.count() as i32
})
.unwrap_or(0)
} else {
0
};
let logs = vault_dir.join("logs").join("access.jsonl");
let last = if logs.exists() {
std::fs::read_to_string(&logs)
.ok()
.and_then(|s| s.lines().last().map(String::from))
.and_then(|line| serde_json::from_str::<serde_json::Value>(&line).ok())
.map(|v| {
let op = v.get("op").and_then(|x| x.as_str()).unwrap_or("?");
let actor = v.get("actor").and_then(|x| x.as_str()).unwrap_or("?");
let ts = v.get("ts").and_then(|x| x.as_str()).unwrap_or("");
format!("{op} by {actor} @ {ts}")
})
.unwrap_or_default()
} else {
String::new()
};
(count, last)
} else {
(0, String::new())
};
(true, vault_id, mode, recipient_count, last_op_summary)
}
fn extract_registry_ctx(
manifest_json: Option<&serde_json::Value>,
) -> (String, Vec<String>, String) {
let Some(mj) = manifest_json else {
return (String::new(), Vec::new(), String::new());
};
let rp = mj.get("registry_provides");
let participant = rp
.and_then(|v| v.get("participant"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_owned();
if participant.is_empty() {
return (String::new(), Vec::new(), String::new());
}
let registries = rp.and_then(|v| v.get("registries"));
let default_id = registries
.and_then(|v| v.get("default"))
.and_then(|v| v.as_str())
.unwrap_or("primary");
let entry = registries
.and_then(|v| v.get("registries"))
.and_then(|v| v.as_array())
.and_then(|arr| {
arr.iter()
.find(|e| e.get("id").and_then(|i| i.as_str()) == Some(default_id))
});
let namespaces = entry
.and_then(|e| e.get("namespaces"))
.and_then(|n| n.get("own"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default();
let endpoint = entry
.and_then(|e| e.get("endpoint"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_owned();
(participant, namespaces, endpoint)
}
/// Resolve domain_origin and domain_provides context from a manifest JSON
/// value.
async fn resolve_domain_ctx(
@ -328,8 +463,8 @@ async fn resolve_domain_ctx(
}
/// Insert logo and MCP metadata into a Tera context.
/// Logos are loaded from `.ontoref/config.ncl`; MCP availability is
/// compile-time.
/// Logos are loaded from `.ontoref/config.ncl`; service flags reflect runtime
/// state from `AppState::service_flags`.
pub(crate) async fn insert_brand_ctx(
ctx: &mut Context,
root: &std::path::Path,
@ -337,11 +472,12 @@ pub(crate) async fn insert_brand_ctx(
import_path: Option<&str>,
base_url: &str,
registry: &Arc<crate::registry::ProjectRegistry>,
flags: &crate::api::ServiceFlags,
) {
let (logo, logo_dark) = load_logos(root, cache, import_path, base_url).await;
ctx.insert("logo", &logo);
ctx.insert("logo_dark", &logo_dark);
insert_mcp_ctx(ctx);
insert_mcp_ctx(ctx, flags);
// Expose repo + docs URLs from card for file link generation in templates.
let (repo_url, docs_url) = if let Some(json) = load_config_json(root, cache, import_path).await
{
@ -382,16 +518,20 @@ pub(crate) async fn insert_brand_ctx(
.unwrap_or(false);
ctx.insert("has_provisioning", &is_provisioning);
// Domain ontology layer — manifest.domain_origin and manifest.domain_provides.
// Surfaces the two-layer ontology distinction in the UI header and sidebar.
// Domain ontology layer + registry topology — both read from manifest.ncl.
let manifest_path = root.join(".ontology").join("manifest.ncl");
let domain = if manifest_path.exists() {
let manifest_json: Option<serde_json::Value> = if manifest_path.exists() {
match cache.export(&manifest_path, import_path).await {
Ok((mj, _)) => resolve_domain_ctx(&mj, registry).await,
Err(_) => DomainCtx::default(),
Ok((mj, _)) => Some(mj),
Err(_) => None,
}
} else {
DomainCtx::default()
None
};
let domain = match &manifest_json {
Some(mj) => resolve_domain_ctx(mj, registry).await,
None => DomainCtx::default(),
};
ctx.insert("has_domain_origin", &domain.has_origin);
ctx.insert("domain_origin_id", &domain.origin_id);
@ -404,14 +544,50 @@ pub(crate) async fn insert_brand_ctx(
ctx.insert("domain_provides_name", &domain.provides_name);
ctx.insert("domain_provides_kind", &domain.provides_kind);
ctx.insert("domain_impl_projects", &domain.impl_projects);
// Registry topology — manifest.registry_provides (default registry entry)
let (registry_participant, registry_namespaces, registry_endpoint) =
extract_registry_ctx(manifest_json.as_ref());
ctx.insert("has_registry", &!registry_participant.is_empty());
ctx.insert("registry_participant", &registry_participant);
ctx.insert("registry_namespaces", &registry_namespaces);
ctx.insert("registry_endpoint", &registry_endpoint);
// Vault state — project.ncl::sops (ADR-017 + ADR-019)
let project_ncl_path = root.join(".ontoref").join("project.ncl");
let project_json: Option<serde_json::Value> = if project_ncl_path.exists() {
match cache.export(&project_ncl_path, import_path).await {
Ok((pj, _)) => Some(pj),
Err(_) => None,
}
} else {
None
};
let (has_vault, vault_id, vault_mode, vault_recipients, vault_last_op) =
extract_vault_ctx(project_json.as_ref());
ctx.insert("has_vault", &has_vault);
ctx.insert("vault_id", &vault_id);
ctx.insert("vault_mode", &vault_mode);
ctx.insert("vault_recipients", &vault_recipients);
ctx.insert("vault_last_op", &vault_last_op);
}
/// Insert MCP metadata and daemon version into a Tera context.
pub(crate) fn insert_mcp_ctx(ctx: &mut Context) {
/// Insert MCP and GraphQL runtime state and daemon version into a Tera context.
pub(crate) fn insert_mcp_ctx(ctx: &mut Context, flags: &crate::api::ServiceFlags) {
ctx.insert("daemon_version", env!("CARGO_PKG_VERSION"));
#[cfg(feature = "graphql")]
ctx.insert(
"graphql_active",
&flags.graphql.load(std::sync::atomic::Ordering::Relaxed),
);
#[cfg(not(feature = "graphql"))]
ctx.insert("graphql_active", &false);
#[cfg(feature = "mcp")]
{
ctx.insert("mcp_active", &true);
ctx.insert(
"mcp_active",
&flags.mcp.load(std::sync::atomic::Ordering::Relaxed),
);
ctx.insert(
"mcp_tools",
&[
@ -495,6 +671,7 @@ pub async fn dashboard(State(state): State<AppState>) -> Result<Html<String>, Ui
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
&state.service_flags,
)
.await;
@ -527,6 +704,7 @@ pub async fn graph(State(state): State<AppState>) -> Result<Html<String>, UiErro
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
&state.service_flags,
)
.await;
@ -587,6 +765,7 @@ pub async fn sessions(State(state): State<AppState>) -> Result<Html<String>, UiE
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
&state.service_flags,
)
.await;
@ -636,6 +815,7 @@ pub async fn notifications_page(State(state): State<AppState>) -> Result<Html<St
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
&state.service_flags,
)
.await;
@ -663,6 +843,7 @@ pub async fn search_page(State(state): State<AppState>) -> Result<Html<String>,
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
&state.service_flags,
)
.await;
render(tera, "pages/search.html", &ctx).await
@ -694,6 +875,7 @@ pub async fn search_page_mp(
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
render(tera, "pages/search.html", &ctx).await
@ -764,6 +946,7 @@ pub async fn modes(State(state): State<AppState>) -> Result<Html<String>, UiErro
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
&state.service_flags,
)
.await;
@ -854,30 +1037,46 @@ pub async fn project_picker(State(state): State<AppState>) -> Result<Html<String
(vec![], 0)
};
// Manifest — layers, operational_modes, default_mode, repo_kind
// Manifest — layers, operational_modes, default_mode, repo_kind, registry
let manifest_path = proj.root.join(".ontology").join("manifest.ncl");
let (layers, op_modes, default_mode, repo_kind) = if manifest_path.exists() {
match proj
.cache
let manifest_json: Option<serde_json::Value> = if manifest_path.exists() {
proj.cache
.export(&manifest_path, proj.import_path.as_deref())
.await
{
Ok((json, _)) => {
.ok()
.map(|(j, _)| j)
} else {
None
};
let (layers, op_modes, default_mode, repo_kind) = match &manifest_json {
Some(json) => {
let layers: Vec<serde_json::Value> = json
.get("layers")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().map(|l| serde_json::json!({
.map(|arr| {
arr.iter()
.map(|l| {
serde_json::json!({
"id": l.get("id").and_then(|v| v.as_str()).unwrap_or(""),
"description": l.get("description").and_then(|v| v.as_str()).unwrap_or(""),
})).collect())
})
})
.collect()
})
.unwrap_or_default();
let op_modes: Vec<serde_json::Value> = json
.get("operational_modes")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().map(|m| serde_json::json!({
.map(|arr| {
arr.iter()
.map(|m| {
serde_json::json!({
"id": m.get("id").and_then(|v| v.as_str()).unwrap_or(""),
"description": m.get("description").and_then(|v| v.as_str()).unwrap_or(""),
})).collect())
})
})
.collect()
})
.unwrap_or_default();
let default_mode = json
.get("default_mode")
@ -891,11 +1090,24 @@ pub async fn project_picker(State(state): State<AppState>) -> Result<Html<String
.to_string();
(layers, op_modes, default_mode, repo_kind)
}
Err(_) => (vec![], vec![], String::new(), String::new()),
}
} else {
(vec![], vec![], String::new(), String::new())
None => (vec![], vec![], String::new(), String::new()),
};
let (registry_participant, registry_namespaces, registry_endpoint) =
extract_registry_ctx(manifest_json.as_ref());
// Vault — read project.ncl::sops (ADR-017 + ADR-019)
let project_ncl_path = proj.root.join(".ontoref").join("project.ncl");
let project_json: Option<serde_json::Value> = if project_ncl_path.exists() {
proj.cache
.export(&project_ncl_path, proj.import_path.as_deref())
.await
.ok()
.map(|(j, _)| j)
} else {
None
};
let (has_vault, vault_id, vault_mode, vault_recipients, vault_last_op) =
extract_vault_ctx(project_json.as_ref());
// card — loaded from `.ontoref/config.ncl` `card` field (which imports
// ../card.ncl)
@ -953,6 +1165,14 @@ pub async fn project_picker(State(state): State<AppState>) -> Result<Html<String
"showcase": showcase,
"generated": generated,
"opmode": opmode,
"registry_participant": registry_participant,
"registry_namespaces": registry_namespaces,
"registry_endpoint": registry_endpoint,
"has_vault": has_vault,
"vault_id": vault_id,
"vault_mode": vault_mode,
"vault_recipients": vault_recipients,
"vault_last_op": vault_last_op,
}));
}
@ -960,7 +1180,7 @@ pub async fn project_picker(State(state): State<AppState>) -> Result<Html<String
ctx.insert("projects", &projects);
ctx.insert("base_url", "/ui");
ctx.insert("hide_project_nav", &true);
insert_mcp_ctx(&mut ctx);
insert_mcp_ctx(&mut ctx, &state.service_flags);
render(tera, "pages/project_picker.html", &ctx).await
}
@ -1228,6 +1448,7 @@ pub async fn dashboard_mp(
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
@ -1347,6 +1568,7 @@ pub async fn api_catalog_page_mp(
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
@ -1460,6 +1682,7 @@ pub async fn graph_mp(
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
@ -1512,6 +1735,7 @@ pub async fn sessions_mp(
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
@ -1597,6 +1821,7 @@ pub async fn notifications_mp(
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
@ -2563,6 +2788,7 @@ pub async fn backlog_page(State(state): State<AppState>) -> Result<Html<String>,
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
&state.service_flags,
)
.await;
render(tera, "pages/backlog.html", &ctx).await
@ -2599,6 +2825,7 @@ pub async fn backlog_page_mp(
proj.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
render(tera, "pages/backlog.html", &ctx).await
@ -3140,10 +3367,48 @@ pub async fn manage_page(State(state): State<AppState>) -> Result<Html<String>,
ctx.insert("hide_project_nav", &true);
ctx.insert("error", &Option::<String>::None);
ctx.insert("daemon_admin_enabled", &state.daemon_admin_hash.is_some());
insert_mcp_ctx(&mut ctx);
insert_mcp_ctx(&mut ctx, &state.service_flags);
render(tera, "pages/manage.html", &ctx).await
}
/// HTMX endpoint — toggle a runtime service (mcp/graphql) on or off.
/// AdminGuard ensures only authenticated daemon admins can call this.
pub async fn service_toggle_ui(
State(state): State<AppState>,
_guard: super::auth::AdminGuard,
axum::extract::Path(service): axum::extract::Path<String>,
) -> impl axum::response::IntoResponse {
use std::sync::atomic::Ordering;
use axum::http::StatusCode;
let new_state = state.service_flags.set(&service, {
// Toggle: read current, flip it.
match service.as_str() {
#[cfg(feature = "mcp")]
"mcp" => !state.service_flags.mcp.load(Ordering::Relaxed),
#[cfg(feature = "graphql")]
"graphql" => !state.service_flags.graphql.load(Ordering::Relaxed),
_ => return (StatusCode::NOT_FOUND, "unknown service").into_response(),
}
});
let Some(enabled) = new_state else {
return (StatusCode::NOT_FOUND, "unknown or not compiled service").into_response();
};
tracing::info!(service = %service, enabled = enabled, "service toggled via UI");
// Return an HTMX-friendly fragment: just the badge so the button can swap it.
let badge_class = if enabled {
"badge badge-success badge-sm"
} else {
"badge badge-error badge-sm"
};
let label = if enabled { "enabled" } else { "disabled" };
axum::response::Html(format!(r#"<span class="{badge_class}">{label}</span>"#)).into_response()
}
pub async fn manage_logout(
State(state): State<AppState>,
request: axum::extract::Request,
@ -3348,6 +3613,7 @@ pub async fn modes_mp(
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
@ -3439,6 +3705,7 @@ pub async fn compose_page_mp(
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
@ -3648,6 +3915,7 @@ pub async fn actions_page(State(state): State<AppState>) -> Result<Html<String>,
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
&state.service_flags,
)
.await;
render(tera, "pages/actions.html", &ctx).await
@ -3679,6 +3947,7 @@ pub async fn actions_page_mp(
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
render(tera, "pages/actions.html", &ctx).await
@ -3705,6 +3974,7 @@ pub async fn qa_page(State(state): State<AppState>) -> Result<Html<String>, UiEr
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
&state.service_flags,
)
.await;
render(tera, "pages/qa.html", &ctx).await
@ -3736,6 +4006,7 @@ pub async fn qa_page_mp(
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
render(tera, "pages/qa.html", &ctx).await
@ -4230,6 +4501,7 @@ pub async fn config_page_mp(
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
@ -4323,6 +4595,7 @@ pub async fn adrs_page_mp(
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
@ -4403,6 +4676,7 @@ pub async fn personal_page_mp(
import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
@ -4479,6 +4753,7 @@ pub async fn career_page_mp(
import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
@ -4547,8 +4822,92 @@ pub async fn provisioning_page_mp(
import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
render(tera, "pages/provisioning.html", &ctx).await
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn vault_basics_returns_none_when_no_project_json() {
assert!(vault_basics_from_json(None).is_none());
}
#[test]
fn vault_basics_returns_none_when_sops_absent() {
let j = json!({ "slug": "demo" });
assert!(vault_basics_from_json(Some(&j)).is_none());
}
#[test]
fn vault_basics_returns_none_when_sops_disabled() {
let j = json!({ "slug": "demo", "sops": { "enabled": false } });
assert!(vault_basics_from_json(Some(&j)).is_none());
}
#[test]
fn vault_basics_legacy_mode_no_rules() {
let j = json!({
"slug": "demo",
"sops": { "enabled": true, "vault_id": "demo-vault" }
});
let (id, mode) = vault_basics_from_json(Some(&j)).expect("vault");
assert_eq!(id, "demo-vault");
assert_eq!(mode, "legacy");
}
#[test]
fn vault_basics_legacy_mode_empty_rules_array() {
let j = json!({
"slug": "demo",
"sops": { "enabled": true, "recipient_rules": [] }
});
let (id, mode) = vault_basics_from_json(Some(&j)).expect("vault");
assert_eq!(id, "demo");
assert_eq!(mode, "legacy");
}
#[test]
fn vault_basics_declarative_mode_with_rules() {
let j = json!({
"slug": "libre-wuji",
"sops": {
"enabled": true,
"vault_id": "libre-wuji",
"recipient_rules": [
{ "path": "access\\.sops\\.yaml$", "groups": ["admin"] }
]
}
});
let (id, mode) = vault_basics_from_json(Some(&j)).expect("vault");
assert_eq!(id, "libre-wuji");
assert_eq!(mode, "declarative");
}
#[test]
fn vault_basics_falls_back_to_slug_when_vault_id_missing() {
let j = json!({
"slug": "fallback-slug",
"sops": { "enabled": true }
});
let (id, _mode) = vault_basics_from_json(Some(&j)).expect("vault");
assert_eq!(id, "fallback-slug");
}
#[test]
fn vault_basics_falls_back_to_slug_when_vault_id_empty_string() {
let j = json!({
"slug": "fallback-slug",
"sops": { "enabled": true, "vault_id": "" }
});
let (id, _mode) = vault_basics_from_json(Some(&j)).expect("vault");
assert_eq!(id, "fallback-slug");
}
}

View file

@ -22,7 +22,7 @@ pub async fn login_page(
ctx.insert("base_url", &base_url);
ctx.insert("hide_project_nav", &true);
ctx.insert("current_role", "");
insert_mcp_ctx(&mut ctx);
insert_mcp_ctx(&mut ctx, &state.service_flags);
if let Some(proj) = state.registry.get(&slug) {
insert_brand_ctx(
&mut ctx,
@ -31,6 +31,7 @@ pub async fn login_page(
proj.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
}
@ -78,7 +79,7 @@ pub async fn login_submit(
tctx.insert("base_url", &base_url);
tctx.insert("hide_project_nav", &true);
tctx.insert("current_role", "");
insert_mcp_ctx(&mut tctx);
insert_mcp_ctx(&mut tctx, &state.service_flags);
insert_brand_ctx(
&mut tctx,
&ctx.root,
@ -86,6 +87,7 @@ pub async fn login_submit(
ctx.import_path.as_deref(),
&base_url,
&state.registry,
&state.service_flags,
)
.await;
match render(tera, "pages/login.html", &tctx).await {
@ -104,7 +106,7 @@ pub async fn manage_login_page(State(state): State<AppState>) -> Result<Html<Str
ctx.insert("daemon_admin_enabled", &state.daemon_admin_hash.is_some());
ctx.insert("hide_project_nav", &true);
ctx.insert("current_role", "");
insert_mcp_ctx(&mut ctx);
insert_mcp_ctx(&mut ctx, &state.service_flags);
render(tera, "pages/manage_login.html", &ctx).await
}
@ -145,7 +147,7 @@ pub async fn manage_login_submit(
tctx.insert("daemon_admin_enabled", &true);
tctx.insert("hide_project_nav", &true);
tctx.insert("current_role", "");
insert_mcp_ctx(&mut tctx);
insert_mcp_ctx(&mut tctx, &state.service_flags);
match render(tera, "pages/manage_login.html", &tctx).await {
Ok(html) => html.into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),

View file

@ -56,6 +56,10 @@ fn multi_router(state: AppState) -> axum::Router {
.route("/manage", get(handlers::manage_page_guarded))
.route("/manage/add", post(handlers::manage_add_guarded))
.route("/manage/remove", post(handlers::manage_remove_guarded))
.route(
"/manage/services/{service}/toggle",
post(handlers::service_toggle_ui),
)
.route(
"/manage/login",
get(login::manage_login_page).post(login::manage_login_submit),

View file

@ -505,8 +505,20 @@
<span class="inline-block w-1.5 h-1.5 rounded-full bg-success ml-1"></span>
</span>
</li>
<li class="divider my-0.5"></li>
{% endif %}
{% if graphql_active %}
<li>
<a href="/graphql" target="_blank" class="gap-1.5 text-base-content/60">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
GraphQL
<span class="inline-block w-1.5 h-1.5 rounded-full bg-success ml-1"></span>
</a>
</li>
{% endif %}
<li class="divider my-0.5"></li>
{% if slug and current_role == "viewer" %}
<li class="px-2 py-0.5 text-xs text-base-content/40 font-mono">{{ slug }} · viewer</li>
{% endif %}
@ -1244,6 +1256,32 @@
</div>
</div>
{% endif %}
{% if graphql_active %}
<a
href="/graphql"
target="_blank"
class="btn btn-xs btn-ghost gap-1"
title="GraphQL endpoint active"
>
<svg
class="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
<span class="text-xs font-mono">GraphQL</span>
<span
class="inline-block w-1.5 h-1.5 rounded-full bg-success"
></span>
</a>
{% endif %}
<!-- Manage gear: only on main picker page (no project) -->
{% if not slug %}
<a
@ -1382,6 +1420,45 @@
</div>
</div>
{% if (has_registry or has_vault) and slug %}
<div class="border-b border-base-content/8 bg-base-200/50 px-4 py-1 hidden sm:flex items-center gap-2 text-[11px] font-mono text-base-content/40 overflow-x-auto">
{% if has_registry %}
<svg class="w-3 h-3 flex-shrink-0 text-accent/50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/>
</svg>
<span class="text-base-content/30">registry</span>
<span class="text-accent/70 font-semibold">{{ registry_participant }}</span>
{% if registry_namespaces | length > 0 %}
<span class="text-base-content/20">·</span>
{% for ns in registry_namespaces %}
<span class="text-base-content/50">{{ ns }}</span>
{% if not loop.last %}<span class="text-base-content/20">·</span>{% endif %}
{% endfor %}
{% endif %}
{% if registry_endpoint %}
<span class="text-base-content/20">·</span>
<span class="text-base-content/30 truncate max-w-xs" title="{{ registry_endpoint }}">{{ registry_endpoint }}</span>
{% endif %}
{% endif %}
{% if has_vault %}
{% if has_registry %}<span class="text-base-content/20">·</span>{% endif %}
<span class="text-base-content/30">vault</span>
<span class="text-accent/70 font-semibold" title="vault id">{{ vault_id }}</span>
<span class="text-base-content/20">·</span>
<span class="px-1.5 py-0.5 rounded {% if vault_mode == 'declarative' %}bg-success/15 text-success{% else %}bg-warning/15 text-warning{% endif %} text-[10px] font-semibold uppercase tracking-wide" title="vault mode (declarative = recipient_rules; legacy = SOPS_AGE_RECIPIENTS)">{{ vault_mode }}</span>
{% if vault_recipients > 0 %}
<span class="text-base-content/20">·</span>
<span class="text-base-content/40" title="recipients in access.sops.yaml">{{ vault_recipients }} recip</span>
{% endif %}
{% if vault_last_op %}
<span class="text-base-content/20">·</span>
<span class="text-base-content/30 truncate max-w-xs" title="last audit log entry">{{ vault_last_op }}</span>
{% endif %}
{% endif %}
</div>
{% endif %}
<main class="{% block main_class %}container mx-auto px-4 py-6 max-w-7xl{% endblock main_class %}">
{% block content %}{% endblock content %}
</main>

View file

@ -21,6 +21,52 @@
{% if error %}<span>{{ error }}</span>{% endif %}
</div>
<!-- Runtime Services -->
<div class="card bg-base-200 border border-base-content/10 mb-6">
<div class="card-body p-5">
<h2 class="card-title text-sm font-semibold">Runtime Services</h2>
<p class="text-xs text-base-content/40 -mt-1">Toggle optional services without restarting the daemon.</p>
<div class="flex flex-wrap gap-3 mt-3">
{% if mcp_active is defined %}
<div class="flex items-center gap-2 bg-base-300 rounded-lg px-3 py-2">
<span class="text-xs font-mono font-semibold">MCP</span>
<span id="mcp-badge" class="{% if mcp_active %}badge badge-success badge-sm{% else %}badge badge-error badge-sm{% endif %}">
{% if mcp_active %}enabled{% else %}disabled{% endif %}
</span>
<button
class="btn btn-xs btn-ghost ml-1"
hx-post="/ui/manage/services/mcp/toggle"
hx-target="#mcp-badge"
hx-swap="outerHTML"
title="Toggle MCP">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
</button>
</div>
{% endif %}
{% if graphql_active is defined %}
<div class="flex items-center gap-2 bg-base-300 rounded-lg px-3 py-2">
<span class="text-xs font-mono font-semibold">GraphQL</span>
<span id="graphql-badge" class="{% if graphql_active %}badge badge-success badge-sm{% else %}badge badge-error badge-sm{% endif %}">
{% if graphql_active %}enabled{% else %}disabled{% endif %}
</span>
<button
class="btn btn-xs btn-ghost ml-1"
hx-post="/ui/manage/services/graphql/toggle"
hx-target="#graphql-badge"
hx-swap="outerHTML"
title="Toggle GraphQL">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
</button>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Registered Projects -->
<div class="mb-6" id="manage-projects-section">
{% include "partials/manage_projects_section.html" %}

View file

@ -89,6 +89,16 @@
{% if p.repo_kind %}
<span class="badge badge-outline badge-xs flex-shrink-0 text-base-content/40">{{ p.repo_kind }}</span>
{% endif %}
{% if p.registry_participant %}
<span class="badge badge-outline badge-xs flex-shrink-0 font-mono text-accent/70" title="Registry participant: {{ p.registry_participant }}">
⟳ {{ p.registry_participant }}
</span>
{% endif %}
{% if p.has_vault %}
<span class="badge badge-outline badge-xs flex-shrink-0 font-mono {% if p.vault_mode == 'declarative' %}text-success{% else %}text-warning{% endif %}" title="Vault {{ p.vault_id }} · {{ p.vault_mode }} mode · {{ p.vault_recipients }} recipients">
⛁ {{ p.vault_id }}{% if p.vault_recipients > 0 %} · {{ p.vault_recipients }}{% endif %}
</span>
{% endif %}
</div>
<!-- Quick-access shortcut icons -->
<div class="flex items-center gap-0.5 flex-shrink-0">
@ -205,6 +215,51 @@
hx-trigger="revealed"
hx-swap="innerHTML"></div>
{% if p.registry_participant %}
<details class="collapse collapse-arrow bg-base-300/40 rounded-lg">
<summary class="collapse-title text-xs font-semibold py-2 min-h-0 px-3 cursor-pointer">
Registry · <span class="font-mono font-normal text-accent/80">{{ p.registry_participant }}</span>
</summary>
<div class="collapse-content px-3 pb-3">
{% if p.registry_endpoint %}
<p class="text-xs font-medium text-base-content/40 uppercase tracking-wider mb-1">Endpoint</p>
<p class="font-mono text-xs text-base-content/60 mb-2 break-all">{{ p.registry_endpoint }}</p>
{% endif %}
{% if p.registry_namespaces %}
<p class="text-xs font-medium text-base-content/40 uppercase tracking-wider mb-1">Namespaces</p>
<div class="space-y-0.5">
{% for ns in p.registry_namespaces %}
<p class="font-mono text-xs text-base-content/70">{{ ns }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</details>
{% endif %}
{% if p.has_vault %}
<details class="collapse collapse-arrow bg-base-300/40 rounded-lg">
<summary class="collapse-title text-xs font-semibold py-2 min-h-0 px-3 cursor-pointer">
Vault · <span class="font-mono font-normal {% if p.vault_mode == 'declarative' %}text-success{% else %}text-warning{% endif %}">{{ p.vault_id }}</span>
<span class="ml-2 px-1.5 py-0.5 rounded text-[10px] font-semibold uppercase tracking-wide {% if p.vault_mode == 'declarative' %}bg-success/15 text-success{% else %}bg-warning/15 text-warning{% endif %}">{{ p.vault_mode }}</span>
</summary>
<div class="collapse-content px-3 pb-3">
<p class="text-xs font-medium text-base-content/40 uppercase tracking-wider mb-1">Mode</p>
<p class="font-mono text-xs text-base-content/70 mb-2">
{% if p.vault_mode == 'declarative' %}per-file recipient routing (ADR-019){% else %}legacy uniform recipients (ADR-017){% endif %}
</p>
{% if p.vault_recipients > 0 %}
<p class="text-xs font-medium text-base-content/40 uppercase tracking-wider mb-1">Recipients in access.sops.yaml</p>
<p class="font-mono text-xs text-base-content/70 mb-2">{{ p.vault_recipients }}</p>
{% endif %}
{% if p.vault_last_op %}
<p class="text-xs font-medium text-base-content/40 uppercase tracking-wider mb-1">Last audit entry</p>
<p class="font-mono text-xs text-base-content/70 break-all">{{ p.vault_last_op }}</p>
{% endif %}
</div>
</details>
{% endif %}
{% if p.layers or p.op_modes %}
<details class="collapse collapse-arrow bg-base-300/40 rounded-lg">
<summary class="collapse-title text-xs font-semibold py-2 min-h-0 px-3 cursor-pointer">

View file

@ -266,6 +266,143 @@ fn emit_onto_api(attr: OntoApiAttr, item: proc_macro2::TokenStream) -> proc_macr
}
}
// ── #[onto_mcp_tool(...)]
// ──────────────────────────────────────────────────
/// Attribute macro for MCP tool unit-structs in ontoref-daemon.
///
/// Registers the tool in the MCP catalog at link time via
/// `inventory::submit!(McpToolEntry{...})`. The annotated item is emitted
/// unchanged — `ToolBase` and `AsyncTool` impls below the struct continue to
/// own the executable behaviour (ADR-015).
///
/// # Required keys
/// - `name = "ontoref_xxx"` — must equal `ToolBase::name()` for the same struct
/// - `description = "..."` — one-line agent-facing description
///
/// # Optional keys
/// - `category = "discovery"` — semantic grouping (discovery | ontology |
/// knowledge | validation | config). Empty by default.
/// - `params = "name:type:constraint:desc; ..."` — same grammar as
/// `#[onto_api(params = ...)]`; semicolon-separated, four-field entries.
///
/// # Example
/// ```ignore
/// #[onto_mcp_tool(
/// name = "ontoref_search",
/// description = "Free-text search across nodes, ADRs, and modes.",
/// category = "discovery",
/// params = "query:string:required:Search term; project:string:optional:Project slug",
/// )]
/// struct SearchTool;
/// ```
#[proc_macro_attribute]
pub fn onto_mcp_tool(args: TokenStream, input: TokenStream) -> TokenStream {
match expand_onto_mcp_tool(args, input) {
Ok(ts) => ts.into(),
Err(err) => err.to_compile_error().into(),
}
}
fn expand_onto_mcp_tool(
args: TokenStream,
input: TokenStream,
) -> syn::Result<proc_macro2::TokenStream> {
let item = proc_macro2::TokenStream::from(input);
let kv_args = syn::parse::Parser::parse(
Punctuated::<MetaNameValue, Token![,]>::parse_terminated,
args,
)?;
let mut name: Option<String> = None;
let mut description: Option<String> = None;
let mut category = String::new();
let mut params_raw: Option<String> = None;
for kv in &kv_args {
let key = kv
.path
.get_ident()
.ok_or_else(|| syn::Error::new_spanned(&kv.path, "expected identifier"))?
.to_string();
let val = lit_str(&kv.value)
.ok_or_else(|| syn::Error::new_spanned(&kv.value, "expected string literal"))?;
match key.as_str() {
"name" => name = Some(val),
"description" => description = Some(val),
"category" => category = val,
"params" => params_raw = Some(val),
other => {
return Err(syn::Error::new_spanned(
&kv.path,
format!(
"unknown onto_mcp_tool key: {other}; expected name, description, \
category, params"
),
))
}
}
}
let name = name.ok_or_else(|| {
syn::Error::new(
Span::call_site(),
"#[onto_mcp_tool] requires name = \"ontoref_xxx\"",
)
})?;
let description = description.ok_or_else(|| {
syn::Error::new(
Span::call_site(),
"#[onto_mcp_tool] requires description = \"...\"",
)
})?;
let params = parse_params(params_raw.as_deref().unwrap_or(""))?;
let name_lit = LitStr::new(&name, Span::call_site());
let desc_lit = LitStr::new(&description, Span::call_site());
let category_lit = LitStr::new(&category, Span::call_site());
let param_exprs: Vec<_> = params
.iter()
.map(|p| {
let n = LitStr::new(&p.name, Span::call_site());
let k = LitStr::new(&p.kind, Span::call_site());
let c = LitStr::new(&p.constraint, Span::call_site());
let d = LitStr::new(&p.description, Span::call_site());
quote! {
::ontoref_ontology::ApiParam { name: #n, kind: #k, constraint: #c, description: #d }
}
})
.collect();
let unique = name
.bytes()
.fold(5381u64, |h, b| h.wrapping_mul(33).wrapping_add(b as u64));
let static_ident =
syn::Ident::new(&format!("__ONTOREF_MCP_TOOL_{unique:x}"), Span::call_site());
Ok(quote! {
::inventory::submit! {
::ontoref_ontology::McpToolEntry {
name: #name_lit,
description: #desc_lit,
category: #category_lit,
params: &[#(#param_exprs),*],
source_file: file!(),
}
}
#[doc(hidden)]
#[allow(non_upper_case_globals, dead_code)]
static #static_ident: () = ();
#item
})
}
// ── Attribute parsing
// ─────────────────────────────────────────────────────────

View file

@ -18,16 +18,21 @@ pub mod contrib;
#[cfg(feature = "derive")]
pub mod api;
#[cfg(feature = "derive")]
pub mod mcp;
#[cfg(feature = "derive")]
pub use api::{ApiParam, ApiRouteEntry};
#[cfg(feature = "derive")]
pub use contrib::{ConfigFieldsEntry, NodeContribution, TestCoverage};
pub use error::OntologyError;
#[cfg(feature = "derive")]
pub use mcp::McpToolEntry;
pub use ontology::{Core, Gate, Ontology, State};
// Re-export the proc-macro crate so consumers only need to depend on
// `ontoref-ontology` with the `derive` feature — no separate ontoref-derive dep.
#[cfg(feature = "derive")]
pub use ontoref_derive::{onto_validates, ConfigFields, OntologyNode};
pub use ontoref_derive::{onto_mcp_tool, onto_validates, ConfigFields, OntologyNode};
pub use types::{
AbstractionLevel, CoreConfig, Coupling, Dimension, DimensionState, Duration, Edge, EdgeType,
GateConfig, Horizon, Membrane, Node, OpeningCondition, Permeability, Pole, Protocol,

View file

@ -0,0 +1,24 @@
use crate::api::ApiParam;
/// Static metadata for an MCP tool exposed by ontoref-daemon.
///
/// Registered at link time via `inventory::submit!` — generated by
/// `#[onto_mcp_tool(...)]` proc-macro attribute on each tool struct.
/// Collected via `inventory::iter::<McpToolEntry>()` (ADR-015).
///
/// `params` reuses [`ApiParam`] — both API routes and MCP tools are protocol
/// surfaces and share the same `name:type:constraint:description` shape.
#[derive(serde::Serialize, Clone)]
pub struct McpToolEntry {
pub name: &'static str,
pub description: &'static str,
/// Semantic grouping (e.g. "discovery", "ontology", "knowledge",
/// "validation", "config"). Used by future tiering work; safe to leave
/// empty.
pub category: &'static str,
pub params: &'static [ApiParam],
/// Source file path captured via `file!()` at the macro call site.
pub source_file: &'static str,
}
inventory::collect!(McpToolEntry);

View file

@ -175,8 +175,416 @@ export def "backlog show" [id: string] {
}
}
# ─── install (Mixed/platform only) ───────────────────────────────────────────
def detect-platform []: nothing -> string {
if (do { ^which docker } | complete | get exit_code) == 0 { "docker" }
else if (do { ^which podman } | complete | get exit_code) == 0 { "podman" }
else if (do { ^which kubectl } | complete | get exit_code) == 0 { "kubernetes" }
else { "" }
}
export def "install" [
--mode: string = "solo" # Deployment mode: solo|multi-user|cicd|enterprise
--platform: string = "" # Container platform: docker|podman|kubernetes (auto-detected)
--dry-run # Print what would be run without executing
]: nothing -> nothing {
let root = (project-root)
let installer_bin = [$root, "platform", "target", "release", "provisioning-installer"] | path join
let installer_src = [$root, "platform", "installer"] | path join
let just_available = (do { ^which just } | complete | get exit_code) == 0
let resolved_platform = if ($platform | is-not-empty) { $platform } else { detect-platform }
print $"Platform install mode=($mode) platform=(if ($resolved_platform | is-empty) { '?' } else { $resolved_platform })"
print ""
if ($resolved_platform | is-empty) {
print "No supported container platform found (docker, podman, kubectl)."
print "Install one and retry."
return
}
if ($installer_bin | path exists) {
print $"installer binary: ($installer_bin)"
if $dry_run {
print $"[dry-run] would run: ($installer_bin) --headless --mode ($mode) --platform ($resolved_platform) --yes"
return
}
do { ^$installer_bin --headless --mode $mode --platform $resolved_platform --yes } | complete | ignore
return
}
if ($installer_src | path exists) {
print "installer binary not built — building from source..."
if $dry_run {
print $"[dry-run] would run: cargo build --release --manifest-path ($installer_src)/Cargo.toml"
return
}
let r = do { ^cargo build --release --manifest-path $"($installer_src)/Cargo.toml" } | complete
if $r.exit_code != 0 {
error make { msg: $"installer build failed:\n($r.stderr)" }
}
do { ^$installer_bin --headless --mode $mode --platform $resolved_platform --yes } | complete | ignore
return
}
if $just_available {
print "No installer binary or source found — delegating to: just install"
if $dry_run {
print $"[dry-run] would run: just install mode=($mode)"
return
}
let r = do { ^just install $"mode=($mode)" } | complete
if $r.exit_code != 0 {
error make { msg: $"just install failed:\n($r.stderr)" }
}
return
}
print "installer not found and 'just' is not available."
print $"Build the installer first: cargo build --release --manifest-path ($installer_src)/Cargo.toml"
}
# ─── ops (ADR-037: NATS ops contract) ────────────────────────────────────────
def workspace-id []: nothing -> string {
let card = load-card
if ($card | is-empty) { "unknown" } else { $card.id }
}
def nats-available []: nothing -> bool {
(do { ^which nats } | complete | get exit_code) == 0
}
def keeper-cli-available []: nothing -> bool {
(do { ^which keeper-cli } | complete | get exit_code) == 0
}
def rad-available []: nothing -> bool {
(do { ^which rad } | complete | get exit_code) == 0
}
export def "ops list" []: nothing -> nothing {
let ws = workspace-id
if not (nats-available) {
print "nats CLI not found — install nats-io/natscli"
return
}
let stream = $"OPS_PENDING_(($ws | str upcase))"
let r = do { ^nats stream info $stream --json } | complete
if $r.exit_code != 0 {
print $"Stream ($stream) not found or NATS unreachable: ($r.stderr | str trim)"
return
}
let info = $r.stdout | from json
print $"stream: ($stream)"
print $"messages: ($info.state.messages)"
print $"first_seq: ($info.state.first_seq)"
print $"last_seq: ($info.state.last_seq)"
}
export def "ops describe" [id: string]: nothing -> nothing {
let ws = workspace-id
if not (nats-available) { print "nats CLI not found"; return }
let subject = $"ops.pending.($ws).>"
let r = do { ^nats req $subject "" --count 50 --json } | complete
if $r.exit_code != 0 { print $"NATS request failed: ($r.stderr | str trim)"; return }
let msgs = $r.stdout | lines | each { |l| $l | from json | get data? | default {} }
let found = $msgs | where { |m| ($m.jti? | default "") == $id }
if ($found | is-empty) { print $"Op not found: ($id)"; return }
let op = $found | first
print $"jti: ($op.jti)"
print $"op_type: ($op.op_type? | default 'unknown')"
print $"target: ($op.target? | default 'unknown')"
print $"expected_version: ($op.expected_state_version? | default 'none')"
print $"scopes: ($op.scopes? | default [] | str join ', ')"
print $"issued_at: ($op.iat? | default 'unknown')"
}
export def "ops sign" [id: string]: nothing -> nothing {
if not (keeper-cli-available) { print "keeper-cli not found — build from platform/crates/ops-keeper"; return }
let ws = workspace-id
let r = do { ^keeper-cli --workspace $ws sign $id } | complete
if $r.exit_code != 0 {
error make { msg: $"keeper-cli sign failed: ($r.stderr | str trim)" }
}
print $r.stdout
}
export def "ops history" [--workspace: string = ""]: nothing -> nothing {
let ws = if ($workspace | is-empty) { workspace-id } else { $workspace }
let repo_path = $env.HOME | path join ".local/share/radicle/repos" $"state-($ws)"
if not ($repo_path | path exists) {
print $"Radicle state repo not found at ($repo_path)"
print "Run: rad clone rad:<rid> state-($ws)"
return
}
let r = do { ^git -C $repo_path log --oneline --no-decorate -20 } | complete
if $r.exit_code != 0 { print $"git log failed: ($r.stderr | str trim)"; return }
$r.stdout | lines | each { |l|
let parts = $l | split row " " | enumerate
let hash = $parts | where index == 0 | get item.0
let msg = $parts | skip 1 | get item | str join " "
{ commit: $hash, message: $msg }
}
}
# ─── playbook (extensions/playbooks/) ────────────────────────────────────────
def playbooks-root []: nothing -> string {
let root = project-root
[$root, "..", "..", "provisioning", "extensions", "playbooks"] | path join | path expand
}
export def "playbook list" []: nothing -> nothing {
let root = playbooks-root
if not ($root | path exists) { print "No extensions/playbooks/ directory found."; return }
^ls $root | where type == dir | get name | each { |d|
let playbook_path = [$d, "playbook.ncl"] | path join
let has_run = ([$d, "run.nu"] | path join | path exists)
{ name: ($d | path basename), has_run: $has_run, has_playbook: ($playbook_path | path exists) }
}
}
export def "playbook describe" [name: string]: nothing -> nothing {
let root = playbooks-root
let playbook_path = [$root, $name, "playbook.ncl"] | path join
if not ($playbook_path | path exists) {
print $"Playbook not found: ($name)"
print $"Expected at: ($playbook_path)"
return
}
let r = do { ^nickel export $playbook_path } | complete
if $r.exit_code != 0 { print $"Failed to load playbook: ($r.stderr | str trim)"; return }
let p = $r.stdout | from json
print $"name: ($p.name)"
print $"description: ($p.description)"
print $"version: ($p.version)"
print ""
print "preconditions:"
$p.preconditions? | default [] | each { |c| print $" - ($c)" }
print ""
print "steps:"
$p.steps? | default [] | enumerate | each { |s|
print $" ($s.index + 1). ($s.item.id): ($s.item.description)"
}
if ($p.rollback_strategy? | default "none") != "none" {
print $"\nrollback: ($p.rollback_strategy)"
}
}
export def "playbook run" [name: string, --dry-run]: nothing -> nothing {
let root = playbooks-root
let run_path = [$root, $name, "run.nu"] | path join
if not ($run_path | path exists) {
print $"run.nu not found for playbook: ($name)"
return
}
let ws = workspace-id
if $dry_run {
let dry_path = [$root, $name, "tests", "dry_run.nu"] | path join
if ($dry_path | path exists) {
let r = do { ^nu $dry_path --workspace $ws } | complete
if $r.exit_code != 0 { error make { msg: $"dry-run failed: ($r.stderr | str trim)" } }
print $r.stdout
} else {
print $"[dry-run] would execute: nu ($run_path) --workspace ($ws)"
print "No dry_run.nu found — simulating step listing only."
playbook describe $name
}
return
}
let r = do { ^nu $run_path --workspace $ws } | complete
if $r.exit_code != 0 {
error make { msg: $"playbook run failed: ($r.stderr | str trim)" }
}
print $r.stdout
}
export def "playbook history" []: nothing -> nothing {
let ws = workspace-id
let repo_path = $env.HOME | path join ".local/share/radicle/repos" $"state-($ws)"
if not ($repo_path | path exists) {
print $"Radicle state repo not found at ($repo_path)"
return
}
let r = do { ^git -C $repo_path log --oneline --no-decorate --grep="playbook:" -20 } | complete
if $r.exit_code != 0 { print $"git log failed: ($r.stderr | str trim)"; return }
if ($r.stdout | is-empty) { print "No playbook audit entries found."; return }
$r.stdout | lines | each { |l|
let parts = $l | split row " " | enumerate
let hash = $parts | where index == 0 | get item.0
let msg = $parts | skip 1 | get item | str join " "
{ commit: $hash, message: $msg }
}
}
# ─── keeper (ADR-037 dual-mode signing) ──────────────────────────────────────
export def "keeper status" []: nothing -> nothing {
let ws = workspace-id
if not (keeper-cli-available) { print "keeper-cli not found"; return }
let r = do { ^keeper-cli --workspace $ws status } | complete
if $r.exit_code != 0 {
print $"keeper-cli unavailable: ($r.stderr | str trim)"
return
}
print $r.stdout
}
export def "keeper policy" []: nothing -> nothing {
let ws = workspace-id
let policy_repo = $env.HOME | path join ".local/share/radicle/repos" $"policy-($ws)"
if not ($policy_repo | path exists) {
print $"Policy repo not cloned locally: policy-($ws)"
print "Run: rad clone rad:<rid> policy-($ws)"
return
}
let policy_path = [$policy_repo, "policy.ncl"] | path join
if not ($policy_path | path exists) { print "policy.ncl not found in policy repo"; return }
let r = do { ^nickel export $policy_path } | complete
if $r.exit_code != 0 { print $"Failed to load policy: ($r.stderr | str trim)"; return }
let p = $r.stdout | from json
print $"workspace: ($p.workspace)"
print $"version: ($p.version)"
print ""
print "auto_sign rules:"
$p.auto_sign? | default [] | each { |rule|
print $" op_type=($rule.op_type) target=($rule.target? | default '*') scope=($rule.scope? | default 'any')"
}
print ""
print "require_manual rules:"
$p.require_manual? | default [] | each { |rule|
print $" op_type=($rule.op_type) target=($rule.target? | default '*')"
}
}
export def "keeper switch" [mode: string]: nothing -> nothing {
let valid_modes = ["auto", "operator-only"]
if not ($valid_modes | any { |m| $m == $mode }) {
error make { msg: $"Invalid mode '($mode)'. Valid modes: ($valid_modes | str join ', ')" }
}
let playbook = if $mode == "auto" { "switch_to_vm_ops" } else { "switch_to_operator_only" }
print $"Delegating to playbook: ($playbook)"
playbook run $playbook
}
# ─── governance (ADR-038 Radicle delegation) ─────────────────────────────────
export def "governance delegations" []: nothing -> nothing {
let ws = workspace-id
let repos = ["policy", "desired", "state"] | each { |r| $"($r)-($ws)" }
$repos | each { |repo_name|
let r = do { ^rad id --repo $repo_name --json } | complete
if $r.exit_code != 0 {
{ repo: $repo_name, error: ($r.stderr | str trim), delegates: [] }
} else {
let info = $r.stdout | from json
{ repo: $repo_name, threshold: ($info.threshold? | default 1), delegates: ($info.delegates? | default []) }
}
}
}
export def "governance signers" []: nothing -> nothing {
let ws = workspace-id
if not (nats-available) { print "nats CLI not found"; return }
let r = do { ^nats auth account ls --json } | complete
if $r.exit_code != 0 {
print $"NATS auth query failed: ($r.stderr | str trim)"
return
}
let accounts = $r.stdout | from json
let ops_accounts = $accounts | where { |a| ($a.name? | default "") | str contains $ws }
if ($ops_accounts | is-empty) { print $"No NATS accounts found for workspace: ($ws)"; return }
$ops_accounts | each { |a|
{ account: $a.name, subject: ($a.subject? | default ""), signers: ($a.signing_keys? | default []) }
}
}
# ─── help + main ──────────────────────────────────────────────────────────────
# ─── registry (i sub-domain) ──────────────────────────────────────────────────
const DEFAULT_REGISTRY = "reg.librecloud.online"
def _find-capabilities-ncl []: nothing -> string {
let ws_path = ($env | get -o PROVISIONING_WORKSPACE_PATH | default "")
if ($ws_path | is-not-empty) {
let candidates = (do { glob $"($ws_path)/infra/*/capabilities.ncl" } | default [])
if ($candidates | is-not-empty) { return ($candidates | first) }
}
mut dir = $env.PWD
for _ in 0..6 {
let cur = $dir # immutable copy for closure capture in glob
let candidates = (do { glob $"($cur)/infra/*/capabilities.ncl" } | default [])
if ($candidates | is-not-empty) { return ($candidates | first) }
let parent = ($dir | path dirname)
if $parent == $dir { break }
$dir = $parent
}
""
}
export def "i resolve-registry" []: nothing -> string {
let from_env = ($env | get -o PROVISIONING_REGISTRY | default "")
if ($from_env | is-not-empty) { return $from_env }
let caps = (_find-capabilities-ncl)
if ($caps | is-not-empty) {
let ip = ($env.NICKEL_IMPORT_PATH? | default ($env.PROVISIONING? | default ""))
let ncl = (do { ^nickel export --import-path $ip $caps } | complete)
if $ncl.exit_code == 0 {
let data = ($ncl.stdout | from json)
let reg_default = ($data | get -o provides.registries.default | default "")
let registries = ($data | get -o provides.registries.registries | default [])
if ($reg_default | is-not-empty) and ($registries | is-not-empty) {
let entry = ($registries | where { |e| $e.id == $reg_default } | first | default null)
if ($entry != null) and ($entry.endpoint? | default "" | is-not-empty) {
return $entry.endpoint
}
}
}
}
$DEFAULT_REGISTRY
}
export def "i list" [--live]: nothing -> nothing {
let reg = (i resolve-registry)
if not $live {
print $"registry : ($reg)"
print "Use --live to query the live catalog."
return
}
let url = $"https://($reg)/v2/_catalog"
let body = try { http get $url } catch {
print $"Registry catalog unavailable at ($url)"
return
}
let repos = ($body | get -o repositories | default [])
let domains = ($repos | where { |r| $r | str starts-with "domains/" } | each { |r|
let parts = ($r | str replace "domains/" "" | split row "/")
{ kind: "domain", participant: ($parts | first), id: ($parts | skip 1 | str join "/"), ref: $"($reg)/($r)" }
})
let modes = ($repos | where { |r| $r | str starts-with "modes/" } | each { |r|
let parts = ($r | str replace "modes/" "" | split row "/")
{ kind: "mode", participant: ($parts | first), id: ($parts | skip 1 | str join "/"), ref: $"($reg)/($r)" }
})
let all = ($domains | append $modes)
if ($all | is-empty) {
print $"No domains/ or modes/ artifacts found in ($reg)"
return
}
print $"registry: ($reg) artifacts: ($all | length)"
print ""
$all | each { |a|
print $" ($a.kind) ($a.participant)/($a.id) → ($a.ref)"
}
null
}
def show-help [] {
print "Provisioning — ontoref domain extension"
print ""
@ -197,11 +605,42 @@ def show-help [] {
print "COMMANDS (Mixed/platform only)"
print " backlog [--priority] Backlog items filtered by High|Medium|Low"
print " backlog show <id> Full detail of a backlog item"
print " install [--mode] [--platform] Install provisioning platform services"
print ""
print "COMMANDS (DevWorkspace — ops contract, ADR-037)"
print " ops list Pending ops queue depth for this workspace"
print " ops describe <id> Full op detail: JWT claims, scopes, expected_version"
print " ops sign <id> Operator signs a pending op via keeper-cli"
print " ops history [<workspace>] Applied ops from <workspace>-state Radicle ledger"
print ""
print "COMMANDS (DevWorkspace — playbooks)"
print " playbook list Available playbooks for this workspace"
print " playbook describe <name> Steps, params, preconditions of a playbook"
print " playbook run <name> Execute a playbook (use --dry-run first)"
print " playbook history Past playbook executions from audit ledger"
print ""
print "COMMANDS (DevWorkspace — keeper daemon, ADR-037)"
print " keeper status Keeper mode (auto|operator-only|down) + policy version"
print " keeper policy Active keeper policy from policy-<workspace> Radicle repo"
print " keeper switch <mode> Switch keeper mode (delegates to switch_to_<mode> playbook)"
print ""
print "COMMANDS (DevWorkspace — governance, ADR-038)"
print " governance delegations Radicle delegation sets for policy/desired/state repos"
print " governance signers Active NATS JWT signers and M-of-N quorum status"
print ""
print "COMMANDS (all repo_kinds — registry)"
print " i resolve-registry Resolved OCI registry endpoint (env → capabilities.ncl → default)"
print " i list Show resolved registry endpoint"
print " i list --live Query _catalog and list live domains/ and modes/ artifacts"
}
def main [
...args: string
--priority: string = ""
--mode: string = "solo"
--platform: string = ""
--dry-run
--live
] {
if ($args | is-empty) or ($args | first) == "help" { show-help; return }
let sub = ($args | str join " ")
@ -212,9 +651,28 @@ def main [
"gates" => { gates }
"card" => { card }
"capabilities" => { capabilities }
"install" => { if $dry_run { install --mode $mode --platform $platform --dry-run } else { install --mode $mode --platform $platform } }
"backlog" => { backlog --priority $priority }
"ops list" => { ops list }
"ops history" => { ops history }
"keeper status" => { keeper status }
"keeper policy" => { keeper policy }
"governance delegations" => { governance delegations }
"governance signers" => { governance signers }
"playbook list" => { playbook list }
"playbook history" => { playbook history }
_ if ($sub | str starts-with "backlog show ") => { backlog show ($args | get 2) }
_ if ($sub | str starts-with "validate ") => { validate ($args | skip 1 | str join " ") }
_ if ($sub | str starts-with "ops describe ") => { ops describe ($args | get 2) }
_ if ($sub | str starts-with "ops sign ") => { ops sign ($args | get 2) }
_ if ($sub | str starts-with "playbook describe ") => { playbook describe ($args | get 2) }
_ if ($sub | str starts-with "playbook run ") => {
if $dry_run { playbook run ($args | get 2) --dry-run } else { playbook run ($args | get 2) }
}
_ if ($sub | str starts-with "keeper switch ") => { keeper switch ($args | get 2) }
_ if ($sub | str starts-with "ops history ") => { ops history --workspace ($args | get 2) }
"i resolve-registry" => { i resolve-registry | print }
"i list" => { if $live { i list --live } else { i list } }
_ => { print $"Unknown command: ($sub)"; show-help }
}
}

View file

@ -18,6 +18,33 @@ s.Domain & {
{ id = "capabilities", description = "Platform capabilities from manifest" },
{ id = "backlog [--priority]", description = "Backlog items filtered by High|Medium|Low (platform only)" },
{ id = "backlog show <id>", description = "Full detail of a backlog item" },
{ id = "install [--mode] [--platform]", description = "Install provisioning platform services (platform only)" },
# ── Ops contract (adr-037) — pending queue, signed commands, audit ──────
{ id = "ops list", description = "Pending and recent ops for the workspace (NATS ops.pending and ops.cmd subjects)" },
{ id = "ops describe <id>", description = "Full detail of a single op including JWT claims, scopes, expected_state_version" },
{ id = "ops sign <id>", description = "Operator signs a pending op via keeper-cli (requires authorized signer key)" },
{ id = "ops history [--workspace]", description = "Audit log of applied ops from <workspace>-state Radicle ledger" },
# ── Playbooks — executable operational artifacts ────────────────────────
{ id = "playbook list", description = "Available playbooks for this workspace's repo_kind" },
{ id = "playbook describe <name>", description = "Full definition (steps, params, preconditions) of a playbook" },
{ id = "playbook run <name>", description = "Execute a playbook (use --dry-run first to simulate without side effects)" },
{ id = "playbook history", description = "Past playbook executions, params, outcomes from audit ledger" },
# ── Keeper (signing daemon) status and control ──────────────────────────
{ id = "keeper status", description = "Current keeper mode (auto | operator-only | down) and policy version" },
{ id = "keeper policy", description = "Show active keeper policy for this workspace from policy-<workspace> Radicle repo" },
{ id = "keeper switch <mode>", description = "Switch keeper mode (delegates to switch_to_<mode> playbook)" },
# ── Governance (adr-038) — Radicle delegation queries ────────────────────
{ id = "governance delegations", description = "Current Radicle delegation set for the workspace's policy/desired/state repos" },
{ id = "governance signers", description = "Active signers for ops and policy (M-of-N quorum status)" },
# ── Registry (OCI / zot) ─────────────────────────────────────────────────
{ id = "i resolve-registry", description = "Resolved OCI registry endpoint: PROVISIONING_REGISTRY env → capabilities.ncl → constant fallback" },
{ id = "i list", description = "Show resolved registry endpoint without querying live catalog" },
{ id = "i list --live", description = "Query _catalog on the resolved registry; list live domains/<participant>/<id> and modes/<participant>/<id>" },
],
pages = [

View file

@ -52,7 +52,8 @@ def main [] {
mkdir $bin_dir
# ── 1. Binary → ontoref-daemon.bin ────────────────────────────────────────
let bin_src = $"($repo_root)/target/release/ontoref-daemon"
let target_dir = (cd $repo_root; ^cargo metadata --format-version 1 --no-deps | from json | get target_directory)
let bin_src = $"($target_dir)/release/ontoref-daemon"
let bin_dest = $"($bin_dir)/ontoref-daemon.bin"
if not ($bin_src | path exists) {

View file

@ -16,9 +16,24 @@ let LogLevel = [| 'trace, 'debug, 'info, 'warn, 'error |] in
let Rotation = [| 'daily, 'hourly, 'never |] in
let Actor = [| 'developer, 'agent, 'ci, 'admin |] in
let Severity = [| 'Hard, 'Soft |] in
let VaultBackend = [| 'restic, 'kopia |] in
let KeyRole = [| 'admin, 'viewer |] in
let KeyEntry = { role | KeyRole, hash | String } in
let ActorKeyBindings = { _ | String } in
let RecipientGroup = { _ | Array String } in
let RecipientRule = { path | String, groups | Array String } in
let SopsConfig = {
enabled | Bool | default = false,
vault_id | String | optional,
vault_backend | VaultBackend | default = 'restic,
registry_endpoint | String | optional,
age_key_paths | Array String | default = [],
master_key_path | String | optional,
actor_key_bindings | ActorKeyBindings | default = {},
recipient_groups | RecipientGroup | default = {},
recipient_rules | Array RecipientRule | default = [],
} in
let ProjectEntry = {
slug | String,
root | String,
@ -26,6 +41,7 @@ let ProjectEntry = {
keys | Array KeyEntry | default = [],
remote_url | String | default = "",
push_only | Bool | default = false,
sops | SopsConfig | optional,
} in
{
@ -75,6 +91,31 @@ let ProjectEntry = {
path | String = "",
},
# Default vault settings (ADR-017). Per-project override in <project>/.ontoref/project.ncl::sops.
vault = {
# Path to the master age private key (.kage) used to open project vaults.
# Empty here = each project must declare sops.master_key_path explicitly.
master_key_path | String = "",
# Default vault backend tool. Switching requires re-initializing local repos.
backend | VaultBackend = 'restic,
# Cosign keypair for signing/verifying src-vault OCI artifacts.
# Empty paths = cosign operations error explicitly. No env-var fallback.
cosign = {
key_path | String = "", # cosign.key — used by `secrets push` to sign
pub_path | String = "", # cosign.pub — used by `secrets sync` and verify
# Upload signature metadata to Rekor (sigstore public transparency log).
# Default false (private self-hosted model). Set true only if you publish
# artifacts that need globally verifiable, tamper-evident audit trail.
# Verification side propagates: false → cosign verify uses --insecure-ignore-tlog.
tlog | Bool = false,
# Cosign 2+ requires a --signing-config without Rekor URLs when tlog=false.
# Generate once with:
# curl https://raw.githubusercontent.com/sigstore/root-signing/refs/heads/main/targets/signing_config.v0.2.json \
# | jq 'del(.rekorTlogUrls) | del(.rekorTlogConfig)' > <path>
signing_config_path | String = "",
},
},
ui = {
# Resolved from platform data dir when empty:
# macOS: ~/Library/Application Support/ontoref/templates

View file

@ -9,6 +9,45 @@ let KeyEntry = {
label | String | default = "",
} in
# Maps ONTOREF_ACTOR value → role name declared in scopes/<role>.ncl.
# Example: { developer = "developer", ci = "cdci", ontoref = "ontoref" }
let actor_key_bindings_type = { _ | String } in
let VaultBackend = [| 'restic, 'kopia |] in
# Per-file recipient routing (optional). When `recipient_rules` is declared,
# bootstrap generates <vault_dir>/.sops.yaml from these definitions and sops
# encrypts each file with the union of recipient groups matching its path.
# Use case: multi-tenant projects where different clients/agents/teams
# decrypt different credential files within the same vault.
let recipient_group_type = { _ | Array String } in # group → list of age1... pubkeys
let recipient_rule_type = {
path | String, # path_regex relative to vault dir
groups | Array String, # union of these recipient_groups
} in
let SopsConfig = {
enabled | Bool | default = false,
# Slug used for ~/.config/ontoref/vaults/<vault_id>/. Defaults to project slug when absent.
vault_id | String | optional,
# Restic or kopia. Switching requires re-initializing the local repo.
vault_backend | VaultBackend | default = 'restic,
# ZOT registry endpoint where src-vault/<vault_id>:latest is pushed/pulled.
registry_endpoint | String | optional,
# Paths to age public key files for each declared recipient (relative or absolute).
age_key_paths | Array String | default = [],
# Path to the master age private key (.kage). Overrides the global path in config.ncl.
# Never store this inside the vault directory or the project root.
master_key_path | String | optional,
# ONTOREF_ACTOR → role name binding. Used by credential resolution to select the correct
# sops-encrypted file in scopes/<role>.ncl before calling oras.
actor_key_bindings | actor_key_bindings_type | default = {},
# Per-file recipient routing — empty = use SOPS_AGE_RECIPIENTS env (legacy mode).
recipient_groups | recipient_group_type | default = {},
recipient_rules | Array recipient_rule_type | default = [],
} in
let ProjectEntry = {
slug | String,
# Absolute local path to the project root. Empty string for push_only projects.
@ -21,6 +60,8 @@ let ProjectEntry = {
remote_url | String | default = "",
# When true: no local file watch, no NCL import. Project pushes ontology via POST /sync.
push_only | Bool | default = false,
# Registry credential vault configuration. Absent = no registry credentials declared.
sops | SopsConfig | optional,
} in
{

View file

@ -0,0 +1,78 @@
# Integration Templates (ADR-015 / federated OCI artifacts)
Three starting templates for integration. Pick the role you want to play; you
can be both a producer and a consumer.
## Pick the template that matches what you want to do
| Template | Role | Outcome |
|---|---|---|
| `domain-producer/` | Define a typed contract for structured data | Push `domains/<participant>/<id>:<version>` to ZOT |
| `mode-producer/` | Author an orchestration mode that consumes domains | Push `modes/<participant>/<id>:<version>` to ZOT |
| `mode-consumer/` | Adopt someone else's published mode | Cabling file in `infra/<ws>/integrations/` |
## Layout
```text
domain-producer/
contract.ncl.template ← schema (Nickel types) for the structured data
example.json.template ← reference instance matching the contract
manifest.ncl.template ← DomainArtifact descriptor
mode-producer/
provisioning.ncl.template ← IntegrationMode declaration
domains.lock.ncl.template ← pinned versions of consumed domains
manifest.ncl.template ← ModeArtifact descriptor
mode-consumer/
cabling.ncl.template ← binds mode params to workspace values
```
## Workflow
### Producer (one-time per artifact)
```bash
# 1. Copy template to your catalog dir
cp -r install/resources/templates/integration/domain-producer/ catalog/domains/my-domain/
mv catalog/domains/my-domain/contract.ncl.template catalog/domains/my-domain/contract.ncl
mv catalog/domains/my-domain/example.json.template catalog/domains/my-domain/example.json
mv catalog/domains/my-domain/manifest.ncl.template catalog/domains/my-domain/manifest.ncl
# 2. Edit the files: replace <placeholders>, define your types
# 3. Publish
ore secrets bootstrap # one-time, vault setup
prvng integration domain publish catalog/domains/my-domain <participant>
prvng integration domain verify <participant>/my-domain 0.1.0
```
### Consumer (one-time per mode you adopt)
```bash
# 1. Subscribe — pulls the mode + its domain dependencies, scaffolds cabling
prvng integration subscribe <mode-id> \
--mode-file infra/modes/<mode-id>.ncl \
--workspace-dir .
# 2. Edit the generated cabling file
$EDITOR infra/<ws>/integrations/<mode-id>.ncl
# 3. Validate that all bindings resolve
prvng integration validate <mode-id> --workspace-dir .
# 4. Invoke the mode
prvng integration invoke <mode-id> --binary <name>
```
## Without templates: minimal viable
The artifact format is documented in `provisioning/schemas/lib/integration/oci_artifact_format.ncl`.
You can write `contract.ncl`, `manifest.ncl`, `provisioning.ncl` from scratch following
the type definitions there. Templates only save copy-paste time.
## See also
- `reflection/qa.ncl::integration-*` — FAQ with diagrams
- `provisioning/schemas/lib/integration/oci_artifact_format.ncl` — types
- `prvng integration --help` — command surface

View file

@ -0,0 +1,32 @@
# <my-domain> contract v0.1.0
# Domain contract: typed shape for <describe what kind of data>.
# Consumers receive the schema via OCI pull and bind values via cabling.
# Helper enums and sub-records.
let _<TypeName> = [| 'value_one, 'value_two |] in
let _<NestedRecord> = {
field_a | String,
field_b | _<TypeName>,
} in
# Top-level context type — this is what consumers receive at the binding point.
# Convention: name it <DomainName>Context.
let _<MyDomain>Context = {
schema_version | String | default = "0.1.0",
workspace | String, # provided by the consumer
mode_id | String, # provided by the mode invoking this binding
# ── Replace below with your domain-specific fields ──
endpoint | String
| doc "Description of what this field means.",
options | _<NestedRecord> | optional
| doc "Optional structured config.",
flags | Array String | default = [],
} in
{
<TypeName> = _<TypeName>,
<NestedRecord> = _<NestedRecord>,
<MyDomain>Context = _<MyDomain>Context,
}

View file

@ -0,0 +1,11 @@
{
"schema_version": "0.1.0",
"workspace": "<example-workspace>",
"mode_id": "<example-mode-using-this-domain>",
"endpoint": "https://example.com/api",
"options": {
"field_a": "sample",
"field_b": "value_one"
},
"flags": []
}

View file

@ -0,0 +1,27 @@
let oci = import "schemas/lib/integration/oci_artifact_format.ncl" in
{
artifact = {
media_type = "application/vnd.ontoref.domain.v1",
id = "<my-domain-id>",
version = "0.1.0",
description = "<one-line summary of what this domain represents>",
layers = [
{
media_type = "application/vnd.ontoref.domain.contract.v1",
description = "contract.ncl — typed schema for <my-domain-id>",
required = true,
},
{
media_type = "application/vnd.ontoref.domain.example.v1",
description = "example.json — reference instance",
required = false,
},
],
# ADR-017 G2 — declare which RegistryEntry credentials this artifact depends
# on. Used by `ore secrets close` impact analysis to identify which artifacts
# are affected when credentials change. Empty if the domain does not consume
# registry credentials at runtime.
uses_registry = "primary",
} | oci.DomainArtifact,
}

Some files were not shown because too many files have changed in this diff Show more