feat: domain extension system, VCS abstraction, personal/provisioning domains, web subpages
Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
Nickel Type Check / Nickel Type Checking (push) Has been cancelled

Domain extension system (ADR-012): bash-layer dispatch activates repo_kind-conditional CLI
  domains. install.nu copies domains/ tree; short_alias wrappers generated (personal, prov).
  ore help and describe capabilities domain-aware.

  personal domain (PersonalOntology): career skills/talks/publications/positioning, CFP
  pipeline (Watching→Delivered), opportunities lifecycle, content pipeline, Sessionize
  integration. Daemon pages: /career, /personal.

  provisioning domain (DevWorkspace/Mixed): FSM state, next transitions, connections graph,
  gates, workspace card, capabilities, backlog. Daemon page: /provisioning.

  VCS abstraction layer (ADR-013): reflection/modules/vcs.nu — uniform jj/git API via
  filesystem detection (.jj/ vs .git/). opmode.nu and git-event.nu migrated off ^git.
  reflection/bin/jjw.nu — jj + ontoref + Radicle agent workspace lifecycle. jjw-ncl-merge.nu
  registered as jj merge tool for .ontology/ NCL conflicts. init-repo.nu for new_project mode.
  jj/rad not in ontoref requirements — belong in orchestration project manifests.

  'Framework RepoKind: ontology/schemas/manifest.ncl gains 'Framework variant; ontoref
  self-identifies as framework — no domain activates for the protocol itself.

  Web presence: personal.html and provisioning.html domain subpages. index.html gains
  "Project Types — Domain Extensions" section with type cards and subpage links. Nav
  compacted (Arch/Prov labels, solid backdrop-filter background).

  on+re: vcs-abstraction (adrs: adr-013) and agent-workspace-orchestration Practice nodes;
  21 manifest capabilities; state.ncl catalysts updated.
This commit is contained in:
Jesús Pérez 2026-04-07 23:08:29 +01:00
parent 13b03d6edf
commit 472952e29b
Signed by: jesus
GPG key ID: 9F243E355E0BC939
70 changed files with 7550 additions and 111 deletions

170
.coder/odoo_privado.md Normal file
View file

@ -0,0 +1,170 @@
Gran pregunta — estás tocando un tema muy relevante: cómo exponer datos de un ERP (Odoo) a un LLM vía MCP sin comprometer la privacidad. Voy a desglosar las tres estrategias que mencionas y cómo se complementan.
---
## 1. MCP Server como capa de acceso a Odoo
Lo primero es entender la arquitectura base. Un MCP Server actúa como intermediario entre Claude y Odoo:
```
Claude ↔ MCP Server ↔ Odoo (XML-RPC / JSON-RPC)
```
El MCP Server expone **tools** (funciones) que Claude puede llamar, pero **tú controlas qué datos salen y cómo**. Odoo ya tiene una API bien documentada (`xmlrpc/2/object`, `jsonrpc`).
---
## 2. Las tres estrategias de privacidad
### A) RAG (Retrieval-Augmented Generation)
En lugar de enviar toda la base de datos al LLM, extraes solo los fragmentos relevantes:
**Cómo funciona con Odoo + MCP:**
- Tu MCP Server recibe la consulta de Claude (ej: "¿Cuántas facturas pendientes hay?").
- El server consulta Odoo vía API, obtiene **solo** los datos necesarios.
- Opcionalmente los pasa por un vector store (ChromaDB, Qdrant, FAISS) para búsqueda semántica.
- Devuelve a Claude solo el contexto mínimo necesario.
**Ventaja:** Claude nunca ve el dataset completo — solo fragmentos filtrados.
```python
# Ejemplo conceptual en el MCP Server
@tool("buscar_facturas")
def buscar_facturas(query: str):
# 1. Buscar en Odoo solo lo relevante
facturas = odoo.execute('account.move', 'search_read',
[['state', '=', 'posted']],
fields=['name', 'amount_total', 'partner_id'], # campos limitados
limit=10
)
# 2. Devolver solo resumen, no datos crudos
return resumir(facturas)
```
---
### B) Anonimización
Reemplazas datos sensibles antes de que lleguen al LLM:
**Técnicas principales:**
| Técnica | Ejemplo | Uso |
|---|---|---|
| **Pseudonimización** | "Cliente A", "Cliente B" | Análisis sin identificar |
| **Hashing** | `SHA256(email)``a3f2c...` | Vincular sin exponer |
| **Generalización** | "Madrid" → "Centro España" | Análisis geográfico |
| **Supresión** | Eliminar NIF, teléfono | Campos innecesarios |
| **K-anonimato** | Agrupar en rangos | Datos estadísticos |
```python
# En tu MCP Server, capa de anonimización
def anonimizar_partner(partner):
return {
"id_alias": hashlib.sha256(str(partner['id']).encode()).hexdigest()[:8],
"sector": partner.get('industry_id', 'N/A'),
"rango_facturacion": clasificar_rango(partner['total_invoiced']),
# SIN nombre, NIF, email, teléfono
}
```
---
### C) DAGs con índices ocultos
Esta es la más sofisticada. Creas un **grafo dirigido acíclico (DAG)** que representa las relaciones entre entidades, pero con índices opacos:
**Concepto:**
```
[Nodo_A7x] --compra_a--> [Nodo_P3k] --contiene--> [Nodo_L9m]
(cliente) (pedido) (línea)
```
Claude puede navegar la estructura y razonar sobre relaciones, pero:
- Los IDs reales de Odoo están cifrados o mapeados a alias.
- Los valores sensibles están generalizados.
- El mapeo inverso (alias → ID real) **solo existe en tu servidor**, nunca sale al LLM.
```python
# Construir DAG con índices ocultos
import networkx as nx
dag = nx.DiGraph()
# Mapeo privado (solo en servidor)
mapa_privado = {} # {"nodo_a7x": {"odoo_id": 42, "model": "res.partner"}}
for partner in odoo_partners:
alias = generar_alias(partner['id'])
mapa_privado[alias] = {"odoo_id": partner['id'], "model": "res.partner"}
dag.add_node(alias, tipo="cliente",
sector=partner['industry'],
rango=clasificar_rango(partner['revenue']))
for order in partner['sale_order_ids']:
alias_order = generar_alias(f"order_{order['id']}")
dag.add_edge(alias, alias_order, relacion="compra")
```
**Ventaja:** Claude puede hacer preguntas como "¿qué nodos tipo cliente tienen más de 5 conexiones de compra?" sin saber quién es quién.
---
## 3. Arquitectura combinada (recomendada)
Las tres técnicas se complementan perfectamente:
```
┌─────────────────────┐
│ Claude │
└─────────┬───────────┘
│ MCP Protocol
┌─────────▼───────────┐
│ MCP Server │
│ │
│ ┌───────────────┐ │
│ │ Capa RAG │ │ ← Solo fragmentos relevantes
│ └───────┬───────┘ │
│ ┌───────▼───────┐ │
│ │ Anonimización │ │ ← Sin PII
│ └───────┬───────┘ │
│ ┌───────▼───────┐ │
│ │ DAG + Índices │ │ ← Estructura sin identidad
│ │ ocultos │ │
│ └───────┬───────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ Mapeo privado │ │ ← NUNCA sale del servidor
│ └───────┬───────┘ │
└──────────┼──────────┘
│ XML-RPC / JSON-RPC
┌──────────▼──────────┐
│ Odoo │
└─────────────────────┘
```
**Flujo:**
1. Claude pide "analiza los clientes con más pedidos este trimestre"
2. El MCP Server consulta Odoo (RAG: solo datos del trimestre)
3. Anonimiza nombres, NIFs, emails
4. Construye/consulta el DAG con alias
5. Devuelve a Claude: "El nodo C3x tiene 47 pedidos, sector: Tecnología, rango facturación: Alto"
6. Si Claude necesita actuar (ej: enviar email), el servidor traduce el alias internamente
---
## 4. Consideraciones prácticas
**Seguridad:** El MCP Server debe correr en tu infraestructura (no en cloud público) o usar cifrado E2E. Las credenciales de Odoo nunca viajan al LLM.
**RGPD/LOPD:** La anonimización ayuda con el cumplimiento, pero consulta con un DPO si manejas datos de ciudadanos EU.
**Performance:** Cachea el DAG y los vectores RAG — reconstruir en cada consulta sería lento. Usa webhooks de Odoo para invalidar caché.
**Granularidad de permisos:** Puedes mapear los roles de Odoo (`res.groups`) a permisos en el MCP Server, para que cada usuario solo acceda a lo que le corresponde.
¿Quieres que te arme un prototipo funcional de alguna de estas capas — por ejemplo un MCP Server básico en Python que se conecte a Odoo con anonimización?

View file

@ -0,0 +1,128 @@
# Cómo puedo leer los datos de Odoo, usarlos y mantenerlos privados con la IA
¿ usando RAG ? ¿ anonimizándolos ? ¿ creando DAGs con índices ocultos ?
Cómo exponer datos de un ERP (Odoo) a un LLM vía MCP sin comprometer la privacidad.
Las tres estrategias que mencionas y cómo se complementan.
---
## 1. MCP Server como capa de acceso a Odoo
Lo primero es entender la arquitectura base. Un MCP Server actúa como intermediario entre Servicio_IA y Odoo:
```
Servicio_IA ↔ MCP Server ↔ Odoo (XML-RPC / JSON-RPC)
```
El MCP Server expone **tools** (funciones) que el Servicio_IA puede llamar, pero **tú controlas qué datos salen y cómo**. Odoo ya tiene una API bien documentada (`xmlrpc/2/object`, `jsonrpc`).
---
## 2. Las tres estrategias de privacidad
### A) RAG (Retrieval-Augmented Generation)
En lugar de enviar toda la base de datos al LLM, extraes solo los fragmentos relevantes:
**Cómo funciona con Odoo + MCP:**
- Tu MCP Server recibe la consulta del Servicio_IA (ej: "¿Cuántas facturas pendientes hay?").
- El server consulta Odoo vía API, obtiene **solo** los datos necesarios.
- Opcionalmente los pasa por un vector store (ChromaDB, Qdrant, FAISS) para búsqueda semántica.
- Devuelve a Servicio_IA solo el contexto mínimo necesario.
**Ventaja:** el Servicio_IA nunca ve el dataset completo — solo fragmentos filtrados.
---
### B) Anonimización
Reemplazas datos sensibles antes de que lleguen al LLM:
**Técnicas principales:**
| Técnica | Ejemplo | Uso |
|---|---|---|
| **Pseudonimización** | "Cliente A", "Cliente B" | Análisis sin identificar |
| **Hashing** | `SHA256(email)``a3f2c...` | Vincular sin exponer |
| **Generalización** | "Madrid" → "Centro España" | Análisis geográfico |
| **Supresión** | Eliminar NIF, teléfono | Campos innecesarios |
| **K-anonimato** | Agrupar en rangos | Datos estadísticos |
---
### C) DAGs con índices ocultos
Esta es la más sofisticada. Creas un **grafo dirigido acíclico (DAG)** que representa las relaciones entre entidades, pero con índices opacos:
**Concepto:**
```
[Nodo_A7x] --compra_a--> [Nodo_P3k] --contiene--> [Nodo_L9m]
(cliente) (pedido) (línea)
```
Servicio_IA puede navegar la estructura y razonar sobre relaciones, pero:
- Los IDs reales de Odoo están cifrados o mapeados a alias.
- Los valores sensibles están generalizados.
- El mapeo inverso (alias → ID real) **solo existe en tu servidor**, nunca sale al LLM.
**Ventaja:** Servicio_IA puede hacer preguntas como "¿qué nodos tipo cliente tienen más de 5 conexiones de compra?" sin saber quién es quién.
---
## 3. Arquitectura combinada (recomendada)
Las tres técnicas se complementan perfectamente:
```
┌─────────────────────┐
│ Servicio_IA │
└─────────┬───────────┘
│ MCP Protocol
┌─────────▼───────────┐
│ MCP Server │
│ │
│ ┌───────────────┐ │
│ │ Capa RAG │ │ ← Solo fragmentos relevantes
│ └───────┬───────┘ │
│ ┌───────▼───────┐ │
│ │ Anonimización │ │ ← Sin PII
│ └───────┬───────┘ │
│ ┌───────▼───────┐ │
│ │ DAG + Índices │ │ ← Estructura sin identidad
│ │ ocultos │ │
│ └───────┬───────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ Mapeo privado │ │ ← NUNCA sale del servidor
│ └───────┬───────┘ │
└──────────┼──────────┘
│ XML-RPC / JSON-RPC
┌──────────▼──────────┐
│ Odoo │
└─────────────────────┘
```
**Flujo:**
1. El Servicio_IA pide "analiza los clientes con más pedidos este trimestre"
2. El MCP Server consulta Odoo (RAG: solo datos del trimestre)
3. Anonimiza nombres, NIFs, emails
4. Construye/consulta el DAG con alias
5. Devuelve al Servicio_IA: "El nodo C3x tiene 47 pedidos, sector: Tecnología, rango facturación: Alto"
6. Si el Servicio_IA necesita actuar (ej: enviar email), el servidor traduce el alias internamente
---
## 4. Consideraciones prácticas
**Seguridad:** El MCP Server debe correr en tu infraestructura (no en cloud público) o usar cifrado E2E. Las credenciales de Odoo nunca viajan al LLM.
**RGPD/LOPD:** La anonimización ayuda con el cumplimiento, pero consulta con un DPO si manejas datos de ciudadanos EU.
**Performance:** Cachea el DAG y los vectores RAG — reconstruir en cada consulta sería lento. Usa webhooks de Odoo para invalidar caché.
**Granularidad de permisos:** Puedes mapear los roles de Odoo (`res.groups`) a permisos en el MCP Server, para que cada usuario solo acceda a lo que le corresponde.

View file

@ -116,6 +116,9 @@
".claude/**",
".wrks/**",
".vale/**",
"vendor/**"
"vendor/**",
"assets/ontology_ops/**",
"assets/oddo_privado.md",
"assets/oddo_privado_howto.md"
]
}

View file

@ -84,9 +84,10 @@ let d = import "../ontology/defaults/core.ncl" in
"adrs/adr-009-manifest-self-interrogation-layer-three-semantic-axes.ncl",
"adrs/adr-010-protocol-migration-system.ncl",
"adrs/adr-011-mode-guards-and-convergence.ncl",
"adrs/adr-012-domain-extension-system.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"],
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"],
},
d.make_node {
@ -336,9 +337,10 @@ let d = import "../ontology/defaults/core.ncl" in
name = "Personal Ontology Schemas",
pole = 'Yin,
level = 'Practice,
description = "Typed NCL schema layer for personal and career artifacts: career.ncl (Skills, WorkExperience, Talks, Positioning, CompanyTargets, PublicationCards), personal.ncl (Content and Opportunity lifecycle — BlogPost to CV to Application, Job to Conference to Grant), project-card.ncl (canonical display metadata for portfolio and cv_repo publication). All types carry linked_nodes referencing .ontology/core.ncl node IDs — bridging career artifacts into the DAG.",
description = "Typed NCL schema layer for personal and career artifacts: career.ncl (Skills, WorkExperience, Talks, Positioning, CompanyTargets, PublicationCards), personal.ncl (Content and Opportunity lifecycle — BlogPost to CV to Application, Job to Conference to Grant), project-card.ncl (canonical display metadata for portfolio and cv_repo publication), links.ncl (typed Link record with LinkKind enum — replaces raw urls/docs/emails/slides_url/video_url/repository string arrays across all personal+career+core schemas). All types carry linked_nodes referencing .ontology/core.ncl node IDs — bridging career artifacts into the DAG.",
invariant = false,
artifact_paths = [
"ontology/schemas/links.ncl",
"ontology/schemas/career.ncl",
"ontology/schemas/personal.ncl",
"ontology/schemas/project-card.ncl",
@ -426,6 +428,57 @@ let d = import "../ontology/defaults/core.ncl" in
adrs = ["adr-007", "adr-008"],
},
d.make_node {
id = "domain-extension-system",
name = "Domain Extension System",
pole = 'Yang,
level = 'Practice,
description = "Bash-layer dispatch for repo_kind-conditional CLI domains. Consumer projects with specific repo_kinds get project-type-aware CLI commands via ore <domain-id> <command>. Each domain ships three files under $ONTOREF_ROOT/domains/{id}/: domain.ncl (NCL contract: commands, pages, repo_kinds, short_alias), commands.nu (Nu script with def main entry point), repo_kinds.txt (grep-readable list of matching repo_kind values). install.nu copies all domains/ to $data_dir and generates aliases.txt + standalone bin wrappers from short_alias. help.nu and describe.nu read domain.ncl at runtime for dynamic help and capabilities output. repo_kind extracted from project manifest via grep (not nickel export) to avoid import-path failures. Session 2026-04-05: personal domain (PersonalOntology — state, next, validate, audit, career, cfp, opportunities, content) and provisioning domain (DevWorkspace/Mixed — state, next, connections, gates, card, capabilities, backlog) implemented. Session 2026-04-06: framework domain (Library/Service/Tool — state, next, connections, gates, capabilities, validate) added to cover Library and Service projects (librosys, DD7pasos, rustelo, website-impl) that have connections.ncl and gate.ncl but no workspace-specific commands.",
invariant = false,
artifact_paths = [
"domains/",
"domains/schema.ncl",
"domains/personal/domain.ncl",
"domains/personal/commands.nu",
"domains/personal/repo_kinds.txt",
"domains/provisioning/domain.ncl",
"domains/provisioning/commands.nu",
"domains/provisioning/repo_kinds.txt",
"domains/framework/domain.ncl",
"domains/framework/commands.nu",
"domains/framework/repo_kinds.txt",
"install/ontoref-global",
"install/install.nu",
"reflection/nulib/help.nu",
"reflection/modules/describe.nu",
],
adrs = ["adr-012"],
},
d.make_node {
id = "vcs-abstraction",
name = "VCS Abstraction Layer",
pole = 'Yang,
level = 'Practice,
description = "Uniform VCS API over jj and git. Filesystem-based detection (.jj/ vs .git/) — no config, no env var. Exposes show-committed, restore-file, remote-url, current-branch, uncommitted-files, and commit-count with identical semantics regardless of VCS backend. All ontoref modules consume vcs.nu instead of hardcoding git commands.",
artifact_paths = [
"reflection/modules/vcs.nu",
],
adrs = ["adr-013"],
},
d.make_node {
id = "agent-workspace-orchestration",
name = "Agent Workspace Orchestration",
pole = 'Yang,
level = 'Practice,
description = "jj + ontoref + Radicle lifecycle wrapper for agent workspaces. jjw agent create spawns a jj workspace and starts an ontoref run; jjw agent step reports progress; jjw agent publish validates, pushes, and opens a Radicle patch (or git push fallback); jjw agent merge commits and cleans the workspace. NCL conflict resolution via jjw-ncl-merge.nu registered as a jj merge tool — enables automated .ontology/ conflict resolution during workspace merge.",
artifact_paths = [
"reflection/bin/jjw.nu",
"reflection/bin/jjw-ncl-merge.nu",
],
},
d.make_node {
id = "ci-pipelines",
name = "CI/CD Pipelines",
@ -547,6 +600,32 @@ let d = import "../ontology/defaults/core.ncl" in
{ from = "self-describing", to = "protocol-migration-system", kind = 'ManifestsIn, weight = 'Medium,
note = "Ontoref runs its own migration checks against itself — the migration system is self-applied." },
# Domain Extension System edges
{ from = "domain-extension-system", to = "formalization-vs-adoption", kind = 'Resolves, weight = 'High,
note = "Domain commands remove the adoption friction of project-type-specific CLI — PersonalOntology and DevWorkspace get discoverable commands without local scripts." },
{ from = "domain-extension-system", to = "personal-ontology-schemas", kind = 'DependsOn, weight = 'High,
note = "The personal domain reads career.ncl, personal.ncl, and backlog.ncl — it is the CLI surface for the personal ontology schema layer." },
{ from = "domain-extension-system", to = "describe-query-layer", kind = 'Complements, weight = 'High,
note = "describe capabilities runs resolve-domain-extension and renders DOMAIN EXTENSION section; ore help <domain-id> reads domain.ncl commands[] dynamically." },
{ from = "domain-extension-system", to = "adopt-ontoref-tooling", kind = 'Complements, weight = 'High,
note = "Domain extensions are part of the adoption surface — new repo_kinds get project-type-aware CLI by shipping a domain directory." },
{ from = "domain-extension-system", to = "protocol-not-runtime", kind = 'Complements, weight = 'Medium,
note = "Domain dispatch is bash+grep — no runtime dependency on nickel for the dispatch path itself. commands.nu is an isolated Nu process." },
# VCS Abstraction edges
{ from = "vcs-abstraction", to = "reflection-modes", kind = 'DependsOn, weight = 'Medium,
note = "opmode.nu and git-event.nu consume vcs.nu — mode execution is VCS-aware." },
{ from = "vcs-abstraction", to = "project-onboarding", kind = 'DependsOn, weight = 'Medium,
note = "init-repo.nu uses vcs detection to choose jj colocated vs git init during onboarding." },
# Agent Workspace Orchestration edges
{ from = "agent-workspace-orchestration", to = "vcs-abstraction", kind = 'DependsOn, weight = 'High,
note = "jjw delegates VCS detection and operations to vcs.nu." },
{ from = "agent-workspace-orchestration", to = "reflection-modes", kind = 'DependsOn, weight = 'High,
note = "jjw agent create starts an ontoref run; jjw agent step reports steps; jjw agent merge completes the run." },
{ from = "agent-workspace-orchestration", to = "no-enforcement", kind = 'Complements, weight = 'Medium,
note = "Radicle publish is optional — jjw degrades to git push if no rad remote is configured." },
# Config Surface edges
{ from = "config-surface", to = "ontoref-daemon", kind = 'ManifestsIn, weight = 'High },
{ from = "config-surface", to = "ontoref-ontology-crate", kind = 'DependsOn, weight = 'High,

View file

@ -2,7 +2,7 @@ let m = import "../ontology/defaults/manifest.ncl" in
m.make_manifest {
project = "ontoref",
repo_kind = 'DevWorkspace,
repo_kind = 'Framework,
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 = [
@ -414,9 +414,27 @@ m.make_manifest {
summary = "CLI and form-driven onboarding for new projects into the ontoref protocol: scaffolds .ontology/, adrs/, reflection/, .ontoref/config.ncl, and registers the project with the daemon. Includes templates for project.ncl and remote-project.ncl.",
rationale = "Adoption friction is the main barrier to protocol spread. Onboarding must be a guided, repeatable process — not a manual checklist. The adopt_ontoref mode and forms reduce the first adoption to answering structured questions.",
how = "reflection/modes/adopt_ontoref.ncl orchestrates the full onboarding DAG. reflection/forms/adopt_ontoref.ncl collects project metadata. reflection/templates/adopt_ontoref.nu.j2 generates the adoption script. templates/ provides starter files for all protocol directories. install/gen-projects.nu handles daemon project registration.",
artifacts = ["reflection/modes/adopt_ontoref.ncl", "reflection/forms/adopt_ontoref.ncl", "templates/", "install/gen-projects.nu", "reflection/bin/ontoref.nu"],
artifacts = ["reflection/modes/adopt_ontoref.ncl", "reflection/forms/adopt_ontoref.ncl", "templates/", "install/gen-projects.nu", "reflection/bin/ontoref.nu", "reflection/bin/init-repo.nu"],
nodes = ["project-onboarding", "adopt-ontoref-tooling", "ci-pipelines"],
},
m.make_capability {
id = "vcs-abstraction",
name = "VCS Abstraction Layer",
summary = "Uniform VCS API over jj and git. Filesystem-based detection — no config required. Provides show-committed, restore-file, remote-url, current-branch, uncommitted-files, and commit-count with identical semantics. All ontoref modules consume vcs.nu; git is the default, jj is opt-in.",
rationale = "Hardcoding git commands in modules creates friction for jj users. A thin abstraction layer decouples protocol tooling from VCS choice.",
how = "reflection/modules/vcs.nu exports typed commands. opmode.nu and reflection/hooks/git-event.nu import it. Detection is filesystem-only (.jj/ presence) — no env vars, no config files.",
artifacts = ["reflection/modules/vcs.nu"],
nodes = ["vcs-abstraction"],
},
m.make_capability {
id = "agent-workspace-orchestration",
name = "Agent Workspace Orchestration",
summary = "jj + ontoref + Radicle lifecycle wrapper for agent workspaces. jjw agent create/step/publish/merge/discard cover the full agent task lifecycle. jjw-ncl-merge.nu is a jj merge driver for .ontology/ NCL conflicts. Both jj and Radicle are entirely optional — not required by the protocol.",
rationale = "Agent sessions working on concurrent tasks need isolated workspaces. jj workspaces are cheaper than git worktrees and support NCL conflict resolution natively via custom merge tools.",
how = "reflection/bin/jjw.nu wraps jj workspace commands, ontoref run/step calls, and optional rad patch open. reflection/bin/jjw-ncl-merge.nu is registered in ~/.config/jj/config.toml as a merge tool — not automatically installed.",
artifacts = ["reflection/bin/jjw.nu", "reflection/bin/jjw-ncl-merge.nu"],
nodes = ["agent-workspace-orchestration"],
},
m.make_capability {
id = "web-presence",
name = "Web Presence",

View file

@ -24,7 +24,7 @@ let d = import "../ontology/defaults/state.ncl" in
from = "adoption-tooling-complete",
to = "protocol-stable",
condition = "ADR-001 accepted, ontoref.dev published, at least two external projects consuming the protocol.",
catalyst = "10 projects consuming the protocol: vapora, stratumiops, kogral, typedialog, secretumvault, rustelo, librecloud_renew, website-impl, jpl_ontology, provisioning. ADR-001 Accepted. Auth model, install pipeline, personal/career schemas, content modes, API catalog (#[onto_api], ADR-007), config surface (ADR-008), manifest self-interrogation (ADR-009), protocol migration system (ADR-010), mode guards and convergence (ADR-011) all complete. Session 2026-03-30: manifest expanded to 19 capabilities; manifest coverage validation (audit + pre-commit + SessionStart); 3 new migrations (0010-0012).",
catalyst = "10 projects consuming the protocol: vapora, stratumiops, kogral, typedialog, secretumvault, rustelo, librecloud_renew, website-impl, jpl_ontology, provisioning. ADR-001 Accepted. Auth model, install pipeline, personal/career schemas, content modes, API catalog (#[onto_api], ADR-007), config surface (ADR-008), manifest self-interrogation (ADR-009), protocol migration system (ADR-010), mode guards and convergence (ADR-011) all complete. Session 2026-03-30: manifest expanded to 19 capabilities; manifest coverage validation (audit + pre-commit + SessionStart); 3 new migrations (0010-0012). Session 2026-04-05: domain extension system (ADR-012) — bash-layer dispatch for repo_kind-conditional CLI domains; personal domain (PersonalOntology: career, cfp + Sessionize integration, opportunities, content) and provisioning domain (DevWorkspace/Mixed: state, connections, gates, card, backlog); short_alias wrappers (personal, prov); ore help and describe capabilities domain-aware. Session 2026-04-06: typed link schema (ontology/schemas/links.ncl — LinkKind enum + Link record) replacing raw string arrays (urls/docs/emails/slides_url/video_url/repository) across personal/career/core schemas and backlog; error UX in domain commands (print --stderr + exit 1 replacing error make); dispatch hardening for bare subcommand arms. Session 2026-04-07: 'Framework RepoKind added — ontoref declares its own kind; no domain activates for the framework itself. VCS abstraction layer (reflection/modules/vcs.nu) — uniform jj/git API used by all modules; jj is opt-in, git is the default. Agent workspace orchestration (reflection/bin/jjw.nu + jjw-ncl-merge.nu) — jj + ontoref + Radicle lifecycle wrapper; jj/rad requirements not propagated to consumer projects. ADR-013: VCS abstraction layer — filesystem detection, single-module contract, opt-in jj/rad. manifest: 21 capabilities (vcs-abstraction, agent-workspace-orchestration added).",
blocker = "ontoref.dev not yet published.",
horizon = 'Months,
},
@ -52,7 +52,7 @@ let d = import "../ontology/defaults/state.ncl" in
from = "modes-and-web-present",
to = "fully-self-described",
condition = "At least 3 ADRs accepted, reflection/backlog.ncl present, describe project returns complete picture.",
catalyst = "ADR-001ADR-006 authored (6 ADRs present). Auth model, project onboarding, and session management nodes added in 2026-03-13. Personal/career/project-card schemas, 5 content modes, search bookmarks, and ADR-006 (Nu 0.111 compat) added in session 2026-03-15. Session 2026-03-23: api-catalog-surface node added (#[onto_api] proc-macro + inventory catalog), describe-query-layer updated (diff + api subcommands), adopt-ontoref-tooling updated (update_ontoref mode + manifest/connections templates + enrichment prompt), ontoref-daemon updated (11 pages, 29 MCP tools, per-file versioning, API catalog endpoint). Session 2026-03-26: config-surface node added — typed DaemonNclConfig (parse-at-boundary pattern), #[derive(ConfigFields)] coherence registry, override-layer mutation API (PUT /config/{section}), NCL contracts (.ontoref/contracts.ncl: LogConfig + DaemonConfig), manifest config_surface with multi-consumer sections. ADR-007 (inventory/onto_api) extended to ConfigFields; ADR-008 (NCL-first config validation + override-layer mutation). Session 2026-03-26 (2nd): manifest-self-description node added. ADR-009. Session 2026-03-29: browser-style panel navigation. Session 2026-03-30: manifest expanded 3→19 capabilities (complete action surface: modes, compose, plans, backlog graduation, notifications, coder pipeline, forms, templates, drift, quick actions, migration, config, search bookmarks, onboarding, web presence). audit-manifest-coverage validator + pre-commit hook + SessionStart hook. Mode schema extended: Guard type (Block/Warn severity pre-flight checks), Converge type (RetryFailed/RetryAll post-execution loops). ADR-011. Migrations 0010-0012. Bug fix: find-unclaimed-artifacts absolute vs relative path comparison. Justfile split (build/test/dev/ci/assets). Anti-slop novelty-check in coder pipeline (Jaccard overlap against published+QA). Health 43%→100%.",
catalyst = "ADR-001ADR-006 authored (6 ADRs present). Auth model, project onboarding, and session management nodes added in 2026-03-13. Personal/career/project-card schemas, 5 content modes, search bookmarks, and ADR-006 (Nu 0.111 compat) added in session 2026-03-15. Session 2026-03-23: api-catalog-surface node added (#[onto_api] proc-macro + inventory catalog), describe-query-layer updated (diff + api subcommands), adopt-ontoref-tooling updated (update_ontoref mode + manifest/connections templates + enrichment prompt), ontoref-daemon updated (11 pages, 29 MCP tools, per-file versioning, API catalog endpoint). Session 2026-03-26: config-surface node added — typed DaemonNclConfig (parse-at-boundary pattern), #[derive(ConfigFields)] coherence registry, override-layer mutation API (PUT /config/{section}), NCL contracts (.ontoref/contracts.ncl: LogConfig + DaemonConfig), manifest config_surface with multi-consumer sections. ADR-007 (inventory/onto_api) extended to ConfigFields; ADR-008 (NCL-first config validation + override-layer mutation). Session 2026-03-26 (2nd): manifest-self-description node added. ADR-009. Session 2026-03-29: browser-style panel navigation. Session 2026-03-30: manifest expanded 3→19 capabilities (complete action surface: modes, compose, plans, backlog graduation, notifications, coder pipeline, forms, templates, drift, quick actions, migration, config, search bookmarks, onboarding, web presence). audit-manifest-coverage validator + pre-commit hook + SessionStart hook. Mode schema extended: Guard type (Block/Warn severity pre-flight checks), Converge type (RetryFailed/RetryAll post-execution loops). ADR-011. Migrations 0010-0012. Bug fix: find-unclaimed-artifacts absolute vs relative path comparison. Justfile split (build/test/dev/ci/assets). Anti-slop novelty-check in coder pipeline (Jaccard overlap against published+QA). Health 43%→100%. Session 2026-04-05: domain-extension-system node added. ADR-012. personal domain (jpl_ontology PersonalOntology: cfp/Sessionize integration, opportunities arrays, career, content) and provisioning domain (DevWorkspace/Mixed: state, card, connections, gates, capabilities, backlog). Bash-layer dispatch + ore help + describe capabilities domain-aware. Short alias system (personal, prov). Session 2026-04-06: personal-ontology-schemas node updated — links.ncl typed link schema; typed link migration across personal/career/core/backlog schemas. Session 2026-04-07: vcs-abstraction node (reflection/modules/vcs.nu) and agent-workspace-orchestration node (reflection/bin/jjw.nu + jjw-ncl-merge.nu) added. 'Framework RepoKind: ontoref self-identifies as framework, no domain activates. manifest: 21 capabilities (vcs-abstraction, agent-workspace-orchestration added).",
blocker = "none",
horizon = 'Weeks,
},

View file

@ -7,6 +7,59 @@ ADRs referenced below live in `adrs/` as typed Nickel records.
## [Unreleased]
### VCS abstraction layer and agent workspace orchestration
#### `'Framework` RepoKind
- `ontology/schemas/manifest.ncl`: `'Framework` added to `repo_kind_type` enum. Reserved for
projects that define protocol schemas — no consumer obligation, no domain activation.
- `.ontology/manifest.ncl`: `repo_kind` corrected from `'DevWorkspace` to `'Framework`.
ontoref is the protocol definition; the provisioning domain activating for its own repo
was semantically incorrect.
#### VCS abstraction layer (`reflection/modules/vcs.nu`)
- New module exporting a uniform VCS API over jj and git: `detect`, `is-repo`,
`show-committed`, `restore-file`, `remote-url`, `current-branch`, `uncommitted-files`,
`commit-count`.
- Detection is filesystem-based (`.jj/` vs `.git/`) — no config, no env var.
- `reflection/modules/opmode.nu` and `reflection/hooks/git-event.nu` migrated to consume
`vcs.nu` instead of hardcoded `^git` calls.
- jj is strictly opt-in: all commands degrade to git when `.jj/` is absent.
`jj` and `rad` are NOT listed as requirements in ontoref's manifest — they belong
in the manifest of orchestration projects (e.g. vapora) that depend on them.
#### Agent workspace orchestration (`reflection/bin/jjw.nu`)
- New orchestration wrapper: `jjw agent create | step | publish | merge | discard`.
Wraps jj workspace commands, `ontoref run/step` calls, and optional `rad patch open`.
- `jjw agent create`: creates a jj workspace at `.agents/<run_id>/`, writes `.ontoref-run`
metadata, starts an ontoref run for the given mode.
- `jjw agent publish`: validates uncommitted changes, pushes to remote, opens a Radicle
patch if a `rad` remote is configured; falls back to `git push` otherwise.
- `jjw agent merge`: advances main, cleans the workspace, marks the ontoref run complete.
- `reflection/bin/jjw-ncl-merge.nu`: jj custom merge tool for `.ontology/` NCL conflicts.
Register via `~/.config/jj/config.toml` `[merge-tools.ncl]` — not auto-installed.
- `reflection/bin/init-repo.nu`: VCS-aware repo initializer called by the `new_project`
mode `init_repo` step. Detects jj/git state; defaults to `jj git init --colocate`
(preferred for Radicle) when no VCS is present.
#### ADR-013
- `adrs/adr-013-vcs-abstraction-layer.ncl`: documents the decision, two hard constraints
(no direct ^git/^jj outside vcs.nu; jj/rad must not be required=true in ontoref manifest),
and three rejected alternatives (env var, per-module inline detection, command-translation shim).
#### on+re update
- `core.ncl`: `vcs-abstraction` (adrs: ["adr-013"]) and `agent-workspace-orchestration`
Practice nodes added with `artifact_paths` and edges into `reflection-modes`,
`project-onboarding`, `no-enforcement`.
- `manifest.ncl`: `vcs-abstraction` and `agent-workspace-orchestration` capabilities added
(21 total). `reflection/bin/init-repo.nu` added to `project-onboarding` artifacts.
---
### Rust doc authoring pattern — canonical `///` convention
#### `#[onto_api]``description` now optional

View file

@ -156,6 +156,27 @@ kind: `'Tool | 'Service | 'EnvVar | 'Infrastructure`), `critical_deps[]` (extern
required `failure_impact` and optional `mitigation`). `describe requirements` surfaces these; `describe
guides` and `ontoref_guides` include all three arrays in their output. ([ADR-009](adrs/adr-009-manifest-self-interrogation-layer-three-semantic-axes.ncl))
**Domain Extension System** — CLI commands conditional on `repo_kind`. The ontoref bash wrapper
resolves the first argument against `$ONTOREF_ROOT/domains/{id}/repo_kinds.txt` before delegating to
the Nu dispatcher; if the project's `repo_kind` matches, the domain's `commands.nu` is dispatched
directly. Each domain ships `domain.ncl` (typed contract: commands, pages, short_alias, repo_kinds),
`commands.nu` (Nu entry point), and `repo_kinds.txt` (grep-readable, sub-1ms dispatch). Two domains
shipped: `personal` (PersonalOntology — cfp, career, content, opportunities) and `provisioning`
(DevWorkspace/Mixed — state, connections, gates, card, backlog). Short aliases (`personal`, `prov`)
work as both `ore prov <cmd>` and standalone `prov <cmd>`. `ore help` and `describe capabilities`
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))
**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
filesystem-based (`.jj/` vs `.git/`), no config required. jj is opt-in: all operations degrade to
git when `.jj/` is absent. `reflection/bin/jjw.nu` wraps jj workspaces, ontoref runs, and optional
Radicle patch submission into a single `jjw agent create|step|publish|merge|discard` lifecycle for
agent-driven development. `jjw-ncl-merge.nu` is a jj merge tool for `.ontology/` NCL conflicts,
registered manually in `~/.config/jj/config.toml`. jj and Radicle are not protocol requirements —
consumer projects use plain git without any configuration change.
## Install
```sh
@ -182,7 +203,7 @@ Global config at `~/.config/ontoref/config.ncl` (type-checked Nickel). Global NA
```sh
cd /path/to/my-project
ontoref setup # idempotent; kind: Service by default
ontoref setup --kind Library # Library | DevWorkspace | PublishedCrate | AgentResource | Mixed
ontoref setup --kind Library # Library | Service | DevWorkspace | PublishedCrate | AgentResource | Mixed | PersonalOntology
ontoref setup --parent /path/to/fw # implementation child: adds framework layer + browse mode
ontoref setup --gen-keys ["admin:dev" "viewer:ci"] # bootstrap auth keys (no-op if keys already exist)
```

View file

@ -36,7 +36,7 @@
claim = "",
scope = "",
severity = "Hard",
check_hint = "",
check = { tag = 'NuCmd, cmd = "", expect_exit = 0 },
rationale = "",
},
],

View file

@ -0,0 +1,88 @@
let d = import "adr-defaults.ncl" in
d.make_adr {
id = "adr-012",
title = "Domain Extension System — Bash-Layer Dispatch for repo_kind-Conditional CLI Domains",
status = 'Accepted,
date = "2026-04-05",
context = "Consumer projects have project-type-specific CLI needs that ontoref's generic command set cannot satisfy. A PersonalOntology project has CFP pipelines, career schemas, and opportunity tracking that no other repo_kind needs. A DevWorkspace project has workspace cards, cluster state dimensions, and production-readiness gates. These capabilities lived as local scripts (scripts/jpl.nu) — invisible to ontoref's help system, absent from describe capabilities, and not portable to other PersonalOntology projects. The problem had two layers: (1) how to conditionally load domain-specific Nu commands without breaking the existing dispatcher, and (2) how to make domain commands discoverable (help, describe capabilities) and aliasable (ore prov, personal state). Nu's module system (`use`, `source`, `overlay use`) is compile-time: paths must be known at parse time and `overlay use` creates an isolated namespace that cannot extend `def main` in the existing dispatcher. Runtime loading of arbitrary Nu modules is architecturally impossible in the current Nu model.",
decision = "Implement a bash-layer domain dispatch system. The ontoref bash wrapper (install/ontoref-global) resolves the first CLI argument against $ONTOREF_ROOT/domains/{arg}/repo_kinds.txt before delegating to the Nu dispatcher. If the argument matches a domain directory name (or a registered alias from domains/aliases.txt) AND the current project's repo_kind appears in that domain's repo_kinds.txt, dispatch directly to nu domains/{id}/commands.nu passing remaining args. Each domain ships three files: domain.ncl (NCL contract declaring commands, pages, repo_kinds, short_alias), commands.nu (Nu script with def main [...args] entry point), and repo_kinds.txt (plain-text list of matching repo_kind values, grep-readable by the bash wrapper without running nickel). install.nu copies the entire domains/ tree to $data_dir/domains/ and generates domains/aliases.txt mapping short_alias → domain_id. Short aliases also create standalone bin wrappers at $bin_dir/alias.",
rationale = [
{
claim = "Bash layer is the correct dispatch boundary for runtime-conditional Nu module selection",
detail = "Nu resolves `use` and `source` at parse time — runtime loading of arbitrary module paths is impossible without spawning a new Nu process. The bash wrapper already mediates between the caller and the Nu dispatcher; extending it with a domain lookup before calling Nu is a natural seam. The domain's commands.nu is a self-contained Nu script with def main as its entry point — no module loading involved, just a new process.",
},
{
claim = "repo_kinds.txt is grep-readable without nickel — critical for dispatch performance",
detail = "The bash wrapper calls domain dispatch on every invocation. Using nickel export to read repo_kind from domain.ncl would add 200-400ms per invocation. repo_kinds.txt is a plain-text file (one repo_kind per line) that grep can check in under 1ms. Similarly, repo_kind is extracted from the project manifest via grep on the NCL source (repo_kind = 'Tag) rather than nickel export — avoiding import-path resolution failures for manifests that import schema files.",
},
{
claim = "NCL domain.ncl contract provides discoverability without hardcoding",
detail = "help.nu and describe.nu read domain.ncl at runtime to render domain commands in ore help and ore describe capabilities. The Nu dispatcher never needs to know about domain commands at parse time — it only sees them if ore help <domain-id> is invoked, at which point it shells out to nickel export domain.ncl. This keeps the Nu dispatcher static while allowing dynamic domain registration.",
},
{
claim = "short_alias enables both ore-level and standalone invocation with one declaration",
detail = "domain.ncl declares short_alias (e.g. 'prov', 'personal'). install.nu generates two artifacts: domains/aliases.txt (consumed by the bash wrapper for ore prov → ore provisioning resolution) and $bin_dir/alias (standalone wrapper for prov state). Both derived from the same field — single source of truth per domain.",
},
],
consequences = {
positive = [
"PersonalOntology and DevWorkspace/Mixed projects have discoverable, aliasable CLI commands without local scripts",
"ore help personal / ore help provisioning renders from domain.ncl — no hardcoded help text",
"ore describe capabilities shows DOMAIN EXTENSION section automatically for any matching project",
"New domains require only three files: domain.ncl, commands.nu, repo_kinds.txt — no changes to the Nu dispatcher",
"Short aliases (personal, prov) work both as ore prov and standalone prov with the same domain-membership enforcement",
],
negative = [
"Domain commands always spawn a new Nu process (cannot share session state with the main dispatcher)",
"commands.nu cannot import from the main reflection/ Nu library without explicit path setup",
"Domain dispatch adds one grep + one stat call per invocation (sub-millisecond, but measurable)",
],
},
alternatives_considered = [
{
option = "Nu overlay use for runtime domain loading",
why_rejected = "overlay use creates an isolated namespace — commands defined in an overlayed module cannot be called by name in the parent scope. It is also parse-time in module context. Confirmed broken: nu -c 'use FILE *; command args' causes infinite recursion when called from def main.",
},
{
option = "Add domain commands directly to reflection/bin/ontoref.nu",
why_rejected = "Would require hardcoding every domain's commands in the main dispatcher, or using dynamic path strings in `use` which Nu forbids. Also violates the no-enforcement axiom — the main dispatcher should not know about PersonalOntology specifics.",
},
{
option = "Project-local scripts/ (scripts/jpl.nu approach)",
why_rejected = "Invisible to ore help and ore describe capabilities. Not portable across PersonalOntology projects. Namespace requires prefix (jpl cfp vs cfp). Dispatch requires knowing the script path.",
},
],
constraints = [
{
id = "domain-files-required",
claim = "Every domain directory under $ONTOREF_ROOT/domains/{id}/ must contain domain.ncl, commands.nu, and repo_kinds.txt",
scope = "domains/",
severity = "Hard",
check = { tag = 'NuCmd, cmd = "ls $env.ONTOREF_ROOT/domains/ | where type == 'dir' | get name | each { |d| let missing = ['domain.ncl' 'commands.nu' 'repo_kinds.txt'] | where { |f| not ($\"($d)/($f)\" | path exists) }; if ($missing | is-not-empty) { error make { msg: $\"domain ($d | path basename) missing: ($missing | str join ', ')\" } } }; true", expect_exit = 0 },
rationale = "The bash wrapper assumes all three files exist once a domain directory is found. Missing files produce confusing errors at dispatch time.",
},
{
id = "commands-nu-def-main",
claim = "commands.nu must declare def main [...args: string] as its entry point — no dynamic use/source calls inside Nu scripts",
scope = "domains/*/commands.nu",
severity = "Hard",
check = { tag = 'NuCmd, cmd = "glob $\"($env.ONTOREF_ROOT)/domains/*/commands.nu\" | each { |f| if not (open --raw $f | str contains 'def main') { error make { msg: $\"($f): missing def main\" } } }; true", expect_exit = 0 },
rationale = "The bash wrapper calls nu commands.nu <args>. Nu invokes def main with the remaining args. Without def main, all args are ignored. Dynamic use/source cause infinite recursion.",
},
],
related_adrs = ["adr-001", "adr-006"],
ontology_check = {
decision_string = "domain extension bash dispatch repo_kind conditional Nu",
invariants_at_risk = [],
verdict = "Safe",
},
}

View file

@ -0,0 +1,82 @@
let d = import "adr-defaults.ncl" in
d.make_adr {
id = "adr-013",
title = "VCS Abstraction Layer — Uniform jj/git API via vcs.nu",
status = 'Accepted,
date = "2026-04-07",
context = "ontoref modules that interact with version control (opmode.nu, git-event.nu, jjw.nu, init-repo.nu) historically hardcoded ^git subcommands. As jj adoption grows among contributors and orchestration projects (vapora), hardcoded git calls produce silent failures: jj repos have .jj/ but may lack a .git/ HEAD in expected locations, and jj semantics differ (the working copy is always a commit, @- is the parent, `jj file show` replaces `git show HEAD:path`). The dual-VCS problem had two layers: (1) detection — which VCS is active in a given project root, and (2) semantics — same logical operation (show last committed state, restore a file, get remote URL) expressed differently per VCS. Spreading detection logic across modules produces duplication and makes future VCS additions (e.g. Pijul, Sapling) a multi-file change.",
decision = "Introduce reflection/modules/vcs.nu as the single VCS abstraction layer. It exports: detect (returns 'jj' | 'git' | 'none' via filesystem check — .jj/ presence), is-repo, show-committed (jj: `jj file show -r @-`; git: `git show HEAD:path`), restore-file (jj: `jj restore --from @-`; git: `git checkout --`), remote-url (jj: `jj git remote list`; git: `git remote get-url origin`), current-branch (jj: `jj log -r @ --no-graph -T bookmarks`; git: `git branch --show-current`), uncommitted-files (jj: `jj diff --summary -r @`; git: `git status --porcelain`), commit-count (jj: `jj log --no-graph -T '' | lines | length`; git: `git rev-list --count HEAD`). All ontoref modules must import vcs.nu and call these exports — direct ^git or ^jj subprocess calls inside modules are prohibited. jj and rad are not listed as requirements in ontoref's manifest: they are opt-in tools whose requirements belong in orchestration projects that depend on them.",
rationale = [
{
claim = "Filesystem detection is the correct boundary — no config, no env var",
detail = "Requiring contributors to set ONTOREF_VCS=jj or maintain a config entry creates state that can desync from reality. .jj/ presence is authoritative: if the directory exists, jj is the VCS regardless of any other indicator. This also makes detection work correctly in multi-VCS scenarios (jj colocated repos have both .jj/ and .git/ — detection correctly prefers jj).",
},
{
claim = "Single module boundary isolates VCS semantic differences from all callers",
detail = "jj's working-copy-as-commit model means @- (parent) is the last deliberately committed state, not HEAD. show-committed encodes this semantic difference once in vcs.nu. Every caller that needs 'the last committed content' calls show-committed and gets the right answer for both VCS backends without knowing which one is active.",
},
{
claim = "opt-in jj/rad — requirements live in the orchestration layer, not in ontoref",
detail = "ontoref ships jjw.nu and vcs.nu as tooling; whether a project uses jj or Radicle is decided by the orchestration project (e.g. vapora). Listing jj/rad as ontoref requirements would force every ontoref consumer to acknowledge tools they will never use. Requirements for opt-in tools belong in the manifest of the project that orchestrates them.",
},
],
consequences = {
positive = [
"All VCS interaction is centralized — adding Pijul or Sapling support requires changes only to vcs.nu",
"opmode.nu and git-event.nu work correctly in both git and jj repos without caller changes",
"jjw.nu agent workspace lifecycle works transparently over jj without any git fallback complexity in the orchestration layer",
"Detection is sub-millisecond — no nickel export, no network, no subprocess for the detection itself",
],
negative = [
"Modules that previously called ^git directly must be updated to import vcs.nu — migration cost for existing modules",
"vcs.nu adds a Nu module import to every module that touches VCS — minor parse-time overhead",
],
},
alternatives_considered = [
{
option = "Env var ONTOREF_VCS to select backend",
why_rejected = "Creates mutable state that can desync from the actual repo state. A repo cloned fresh has no env var set; a contributor switching between git and jj repos would need to update the env var manually. Filesystem detection is always correct without configuration.",
},
{
option = "Per-module inline detection (duplicate detect logic in each file)",
why_rejected = "Already the de-facto state before vcs.nu. Duplicated detection means any change to jj semantics (e.g. a jj CLI flag change) requires hunting every module. The abstraction cost is one import line per module.",
},
{
option = "Wrap the entire CLI in a shim that translates git commands to jj",
why_rejected = "Shim-layer translation is fragile — git and jj command surfaces are not isomorphic (jj has no git stash equivalent; jj describe vs git commit -m). The operations ontoref needs are a small, well-defined set; a typed Nu module is a cleaner contract than a command-translation shim.",
},
],
constraints = [
{
id = "vcs-module-single-source",
claim = "All VCS subprocess calls in reflection/modules/ and reflection/bin/ must go through vcs.nu exports — no direct ^git or ^jj calls outside vcs.nu itself",
scope = "reflection/modules/, reflection/bin/",
severity = 'Hard,
check = { tag = 'NuCmd, cmd = "glob $\"($env.ONTOREF_ROOT)/reflection/{modules,bin}/*.nu\" | where { |f| ($f | path basename) != 'vcs.nu' } | each { |f| let hits = open --raw $f | lines | where { |l| ($l | str trim | str starts-with '^git ') or ($l | str trim | str starts-with '^jj ') }; if ($hits | is-not-empty) { error make { msg: $\"($f): direct VCS call found\" } } }; true", expect_exit = 0 },
rationale = "The abstraction only holds if it is the single call site. Any direct ^git call in a module bypasses the detection logic and breaks jj repos silently.",
},
{
id = "jj-rad-not-in-ontoref-requirements",
claim = "jj and rad must not appear as required = true entries in .ontology/manifest.ncl requirements[]",
scope = ".ontology/manifest.ncl",
severity = 'Hard,
check = { tag = 'NuCmd, cmd = "let reqs = (do { ^nickel export $\"($env.ONTOREF_PROJECT_ROOT)/.ontology/manifest.ncl\" } | complete | if $in.exit_code == 0 { $in.stdout | from json | get requirements? | default [] } else { [] }); let forced = $reqs | where { |r| ($r.id == 'jj' or $r.id == 'rad') and $r.required == true }; if ($forced | is-not-empty) { error make { msg: $\"jj/rad must not be required=true in ontoref manifest\" } }; true", expect_exit = 0 },
rationale = "jj and rad are opt-in tools. Marking them required=true in ontoref's manifest propagates a false requirement to every consumer that reads describe requirements.",
},
],
related_adrs = ["adr-012"],
ontology_check = {
decision_string = "vcs abstraction jj git uniform module",
invariants_at_risk = [],
verdict = 'Safe,
},
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>URL</key>
<string>https://atlan.com/know/what-is-ontology-in-ai/</string>
</dict>
</plist>

View file

@ -0,0 +1,21 @@
This is a great question — let me build this explanation visually so that the concepto quede claro de forma intuitiva.Voy a construirte una explicación visual e intuitiva de este concepto. La "ontología operacional" suena intimidante, pero en esencia responde a una pregunta simple: **¿qué cosas existen cuando usas IA, y cómo se relacionan entre sí?**
Piénsalo como el "mapa del territorio" que necesitas para moverte con soltura. Vamos por capas:Ahora te lo desgloso en lenguaje llano. La ontología operacional es simplemente **el inventario de todo lo que participa cuando usas IA**, organizado para que entiendas qué puedes controlar y qué no.
**La columna izquierda es el flujo principal** — lo que tú vives directamente:
**Tú** eres el punto de partida. Llegas con una intención ("necesito un resumen", "quiero ideas para un nombre") y un contexto personal (tu industria, tu nivel de conocimiento, tus preferencias). La IA no sabe nada de esto salvo que tú se lo digas.
**El prompt** es tu única vía de comunicación. No es solo "lo que escribes", es *cómo* lo escribes. Un prompt vago produce resultados vagos. Uno específico, con rol, contexto y formato deseado, produce resultados útiles. Es como la diferencia entre decirle a un taxista "llévame por ahí" versus darle la dirección exacta.
**El modelo** es la caja que procesa tu mensaje. Tú no ves qué pasa dentro, pero sí puedes influir en su comportamiento a través del prompt, las herramientas que activas y el contexto que proporcionas.
**El output** es lo que recibes. Puede ser texto, una tabla, un archivo, código, un diagrama. La clave es que no es "la verdad" — es una *generación* basada en probabilidades que necesitas evaluar.
**La flecha punteada que sube** es quizás lo más importante: la iteración. Rara vez el primer resultado es perfecto. Lees el output, ajustas tu prompt, y vuelves a intentar. Este ciclo es donde realmente se aprende a usar IA.
**La columna derecha son las dimensiones invisibles** — lo que existe detrás de escena y afecta todo:
El **contexto** incluye el historial de tu conversación, las instrucciones del sistema y cualquier documento que hayas subido. El **conocimiento base** es lo que la IA aprendió en su entrenamiento más lo que puede buscar en internet. Las **herramientas** son capacidades extras (buscar en la web, crear archivos, hacer cálculos). Las **restricciones** son los límites éticos y técnicos. Y el **formato de salida** determina cómo se presenta la respuesta.
¿Te gustaría que profundice en alguna de estas piezas? Puedes tocar cualquier caja del diagrama para explorar más, o preguntarme directamente.

View file

@ -0,0 +1,44 @@
# Cómo la IA usa la estocástica
Imagina que estás escribiendo un mensaje de texto en el teléfono y el teclado te sugiere la siguiente palabra. Eso, en esencia, es el corazón de cómo funciona gran parte de la IA moderna. Vamos paso a paso.
**¿Qué significa "estocástico"?**
Es simplemente una palabra elegante para decir "con algo de azar controlado". Un proceso estocástico es aquel donde el resultado no está 100% determinado, sino que depende de probabilidades. Lanzar un dado es estocástico: no sabes qué saldrá, pero sí sabes que cada cara tiene ~16.7% de probabilidad.
**¿Cómo lo usa la IA al generar texto?**
Cuando un modelo como yo genera una respuesta, no "piensa" una frase completa de golpe. Genera **una palabra (o fragmento) a la vez**, eligiendo entre miles de candidatas. Para cada posición, el modelo calcula una distribución de probabilidades. Por ejemplo, si la frase empieza con "El gato se subió al…", las probabilidades podrían verse así:
- "tejado" → 30%
- "árbol" → 25%
- "muro" → 15%
- "sofá" → 10%
- miles de otras opciones → el resto
Aquí es donde entra lo estocástico: el modelo **no siempre elige la palabra más probable**. En lugar de eso, "lanza un dado cargado" donde las opciones con más probabilidad tienen más chances de salir, pero las menos probables también pueden aparecer.
**¿Por qué no elegir siempre la más probable?**
Si siempre eligiera la palabra con mayor probabilidad, las respuestas serían repetitivas, predecibles y aburridas. El componente aleatorio es lo que le da creatividad, variedad y naturalidad al texto. Es como la diferencia entre un músico que toca siempre las mismas notas "seguras" y uno que improvisa dentro de una escala.
**El "termostato" de la aleatoriedad: la temperatura**
Los ingenieros controlan *cuánta* aleatoriedad hay con un parámetro llamado **temperatura**:
- **Temperatura baja** (cerca de 0): el modelo se vuelve casi determinista, elige casi siempre la palabra más probable. Útil para respuestas precisas, como resolver un cálculo.
- **Temperatura alta** (cerca de 1 o más): el modelo se atreve más, elige palabras menos obvias. Útil para escritura creativa o lluvia de ideas.
Piénsalo como un chef: con temperatura baja, sigue la receta al pie de la letra; con temperatura alta, improvisa y experimenta con ingredientes inesperados.
**¿Dónde más aparece la estocástica en la IA?**
No solo en la generación de texto. Aparece en varias etapas:
- **Entrenamiento**: los datos se presentan al modelo en orden aleatorio (barajados como un mazo de cartas) para que aprenda mejor y no memorice patrones superficiales.
- **Dropout**: durante el entrenamiento, se "apagan" neuronas al azar para que la red no dependa demasiado de ninguna conexión específica. Es como entrenar a un equipo de fútbol obligando a distintos jugadores a sentarse en cada práctica para que todos sepan cubrir todas las posiciones.
- **Exploración**: en IA para juegos o robótica, el agente a veces toma decisiones al azar para descubrir estrategias nuevas en vez de quedarse con la primera que funcionó.
**En resumen**
La IA no es una calculadora que siempre da la misma respuesta. Es más parecida a un músico de jazz muy bien entrenado: conoce las reglas de la armonía (las probabilidades aprendidas de millones de textos), pero en cada actuación introduce pequeñas variaciones aleatorias que hacen que el resultado sea único, natural y, con suerte, interesante.

206
assets/ontology_ops/info.md Normal file
View file

@ -0,0 +1,206 @@
An ontology defines the formal vocabulary of a domain: the classes of things that exist, the properties that describe them, and the relationships that connect them. In data management and AI, ontologies make business meaning machine-readable so that AI agents, governance policies, and analytics tools operate from a single, consistent understanding of enterprise concepts.
Core Components:
1. Structure: Classes, properties, and relationships in machine-readable formats like RDF and OWL
2. Reasoning: Enables inference — AI derives new facts from stated facts using formal logic
3. Governance: Defines what business terms mean, how they relate, and who owns them
4. Interoperability: Shared vocabulary allowing different AI systems and platforms to communicate
Attribute Detail
Definition A formal model of concepts, properties, and relationships within a domain
Origin Philosophy (Aristotle); formalized for computer science by Tom Gruber (1993)
Key standards RDF (Resource Description Framework), OWL (Web Ontology Language), both W3C
Primary use in AI Grounding AI agents with structured business context for accurate outputs
Related concepts Taxonomy (hierarchical classification), Schema (data structure), Semantic layer (query abstraction), Knowledge graph (data instantiation)
Enterprise value Reduces AI hallucination, enables cross-platform governance, accelerates AI agent deployment
Governance role Defines what business terms mean, how they relate, and who owns them
Modern implementation Active metadata platforms bootstrap ontology incrementally, not through multi-year modeling
When did ontology become important?
The idea is not new. Philosophers have debated ontology since Aristotle. Computer scientists formalized it in the 1990s. What changed is the stakes. 88% of organizations now use AI in at least one business function, but fewer than 40% have scaled beyond pilot. That gap between pilot and production is, in large part, a context gap. Ontology fills it.
Ontology formalizes “what things are” and “how they relate” in a domain
Taxonomies classify; ontologies define meaning, constraints, and reasoning rules
In AI, ontology is the structural layer between raw metadata and intelligent action
Enterprise ontology covers business terms, data assets, policies, and their connections
Modern ontologies are living, evolving structures, not static academic models
Taxonomies classify things. Ontologies define meaning, relationships, and reasoning rules.
Taxonomies classify things. Ontologies define meaning, relationships, and reasoning rules. Image by Atlan.
How ontology works in AI and data management
Ontology works by defining classes (categories of entities), properties (attributes and data types), and relationships (how entities connect) in machine-readable formats like RDF and OWL. AI systems use this formal structure to perform inference, resolve ambiguity, and navigate enterprise data without requiring manual mapping for every new use case.
What are classes, properties, and relationships in ontology?
Every ontology rests on three building blocks. Classes are the categories: “Revenue,” “Customer,” “Data Pipeline.” Properties describe each class: Revenue has a currency, a fiscal period, a flag for whether it includes returns. Relationships connect classes to each other: Revenue belongs to Financial Metrics, Customer owns Account, Account generates Revenue.
This is not abstract. Consider a financial reporting team and a sales ops team that both use the word “revenue.” Finance means GAAP-recognized revenue. Sales means bookings. Without an ontology that defines both as distinct classes with explicit properties and constraints, an AI agent has no way to know which one a query refers to. It picks whichever definition it encounters first. Gartner predicts 40% of enterprise applications will feature task-specific AI agents by 2026, up from less than 5% in 2025. Each of those agents needs this kind of formal semantic structure to function. A business glossary defines the human-readable layer; ontology makes that layer machine-readable.
How do RDF and OWL make ontology machine-readable?
RDF (Resource Description Framework) represents facts as triples: subject, predicate, object. “Revenue belongs_to Financial_Metrics” is one triple. Stack enough triples and you have a graph of your domain. The W3C published RDF 1.1 as a formal recommendation in 2014, and it remains the foundational standard for linked data and the Semantic Web.
OWL (Web Ontology Language) adds expressive logic on top of RDF. Class hierarchies, cardinality constraints (“a Customer must have exactly one primary Account”), equivalence declarations (“Bookings_Revenue is equivalent to Sales_Revenue”). SPARQL is the query language for traversing these structures. These are W3C standards, which means ontology built on RDF and OWL is interoperable across tools and platforms. That interoperability is what separates open ontology from proprietary approaches that lock semantic models into a single vendor. A semantic layer maps business terms to physical data for querying; ontology defines what those terms mean at a deeper structural level. dbt semantic layer integration bridges the two in practice.
How does ontology enable inference and reasoning?
Ontology enables machines to derive new facts from stated facts. If the ontology says “Revenue includes Subscription_Revenue” and “Subscription_Revenue excludes One_Time_Fees,” an AI agent infers that Revenue excludes One_Time_Fees. No one programmed that rule explicitly. The ontologys formal logic made it derivable.
This is what separates ontology-grounded retrieval from pure vector search. Embedding-based retrieval finds documents that are statistically similar to a query. Ontology-grounded retrieval finds answers that are logically consistent with defined business meaning. RAG systems with structured knowledge context reduce AI hallucinations by over 40% compared to traditional approaches, according to the MEGA-RAG study published in PubMed Central. Enterprise RAG deployments frequently underperform when they lack semantic structure, producing answers that are statistically plausible but logically inconsistent with business definitions. A context graph unifies semantic meaning with operational lineage, giving AI agents both the “what” and the “where” of enterprise data.
Comparison: ontology vs. taxonomy vs. schema vs. semantic layer vs. knowledge graph
Concept What it defines Structure Machine-readable? Supports reasoning? Example
Ontology Formal model of concepts, properties, and relationships in a domain Graph (classes + properties + axioms) Yes (RDF/OWL) Yes, inference and logical constraints “Revenue is a Financial Metric that includes Subscription Revenue and excludes One-Time Fees”
Taxonomy Hierarchical classification of terms Tree (parent-child) Partially (SKOS) No, classification only “Financial Metrics > Revenue > Subscription Revenue”
Schema Structure and data types of a dataset Tabular (columns + types + constraints) Yes (DDL, JSON Schema) No, structural validation only “revenue_amount: DECIMAL(18,2), NOT NULL”
Semantic layer Abstraction mapping business terms to physical data for querying Mapping layer (metrics + dimensions) Yes (dbt, LookML) No, query translation only “Revenue = SUM(order_amount) WHERE status = completed
Knowledge graph Data instances organized by an ontologys structure Graph (nodes + edges with real data) Yes (RDF triples, property graphs) Yes, if backed by an ontology “Acme Corp [has_revenue] $4.2M [in_period] Q1-2026”
Why enterprise AI needs ontology
Enterprise AI needs ontology because AI agents cannot reliably interpret data without a formal model of business meaning. Ontology grounds AI outputs in defined concepts and relationships, reduces hallucination by providing structured context for retrieval, and enables governance to scale across platforms without manual re-mapping.
How do ontologies ground AI agents in business meaning?
AI agents retrieve data. They do not understand it. When an agent queries “revenue,” it pulls whatever matches from the nearest data source. If your finance team defines revenue as GAAP-recognized net revenue, your sales team defines it as bookings, and your product team defines it as MRR, the agent picks whichever definition it hits first.
A separate Deloitte survey found that 62% of data leaders cite “lack of semantic consistency” as the primary barrier to scaling AI across business units. An ontology prevents this by giving agents an explicit map of what “revenue” means in context — what it includes, what it excludes, and which teams definition applies. Active metadata enriches every data asset with governed meaning, turning static documentation into live signals that agents query at inference time.
How does ontology reduce AI hallucination?
Start with the cost: poor data quality costs the average organization $12.9 million annually, according to Gartner. Hallucination compounds that by generating confident but incorrect outputs that propagate through downstream decisions.
RAG (Retrieval-Augmented Generation) helps, but RAG alone retrieves documents — it does not understand concepts. An embedding search for “quarterly revenue” returns statistically similar text. An ontology-grounded search returns the specific definition of quarterly revenue, its component metrics, its exclusions, and its relationship to annual revenue. The difference is the gap between plausible and correct. A data catalog for AI provides the operational layer for ontology-grounded retrieval.
How does ontology enable cross-platform governance?
Here is the scenario most governance teams recognize: you define a masking policy for PII fields in Snowflake, then redefine it in Databricks, then again in your BI layer. Each platform has its own naming conventions, its own metadata structure, its own interpretation of what “PII” means. Divergence is inevitable.
Ontology breaks the cycle. When “PII_Field” is defined as a class with a “requires_masking” constraint, that rule propagates across every platform that reads the ontology. Define once, enforce everywhere. By 2027, organizations adopting active metadata practices will increase by more than 75% across data, analytics, and AI, according to Gartners Market Guide for Active Metadata Management. See data governance platforms that operationalize this approach.
The Data Stack Is Being Rebuilt for AI. Here's What's Coming.
What happens when AI isn't just a feature on top of data, but a consumer of it? Based on insights from 550+ data leaders, Prukalpa Sankar (Co-Founder & Co-CEO, Atlan) shares seven predictions—from the rise of data agents to why the context layer becomes the new infrastructure.
How to build an enterprise ontology
Building an enterprise ontology starts with a focused domain. Identify 3-5 high-value business domains, define core classes and relationships in each, connect them to existing metadata in your data catalog, and iterate. Active metadata platforms bootstrap ontology incrementally from existing lineage, glossary terms, and usage patterns.
Prerequisites:
Data catalog with asset inventory across key platforms
Business glossary with at least core terms defined
Column-level lineage mapped for priority data assets
Executive sponsor (CDO or VP Data) who owns semantic standards
Cross-functional stakeholders identified (data engineering, governance, analytics, AI/ML)
Seven steps to your first production ontology:
Select 3-5 priority domains. Choose domains where AI agents are active or planned: financial metrics, customer entities, product hierarchies. Start narrow. A focused ontology that covers one domain well is worth more than a sprawling model that covers everything poorly.
Audit existing semantic assets. Inventory business glossary terms, dbt semantic models, schema documentation, and tribal knowledge. Most organizations already have 40-60% of an ontology scattered across tools. MuleSofts 2025 Connectivity Benchmark found that only 28% of enterprise applications are integrated, despite organizations averaging 897 apps — semantic definitions fragment across these disconnected systems. The work is consolidation, not creation from scratch.
Define classes and properties. For each domain, identify the core classes (entities), their properties (attributes), and data types. Use existing glossary terms as the starting vocabulary. “Revenue” becomes a class. “Currency,” “fiscal_period,” and “includes_returns” become its properties.
Map relationships and constraints. Define how classes connect: “Revenue includes Subscription_Revenue,” “Customer owns Account,” “PII_Field requires Masking_Policy.” Add cardinality constraints where business logic demands them. Teams that define explicit cardinality constraints in their ontology reduce downstream data quality incidents by 34%, according to Gartner.
Connect to live metadata. Link ontology classes to physical data assets in your catalog. Each class maps to tables, columns, dashboards, and pipelines through column-level lineage. This is what separates a useful ontology from a theoretical model.
Validate with AI agent use cases. Test ontology coverage by running target AI agent queries against it. Where the agent cannot resolve a concept, the ontology has a gap. Fill it.
Establish governance and iteration cadence. Assign domain owners, set review cycles (monthly minimum), and automate drift detection. Ontology is never “done.” Business definitions change. New data assets appear. The ontology must keep pace.
Build your first production ontology in seven clear, repeatable steps.
Build your first production ontology in seven clear, repeatable steps. Image by Atlan.
Organizations with active metadata management reduce time to deliver new data assets by up to 70%, according to Gartner. An enterprise context layer operationalizes ontology so it stays connected to the living data stack.
Common pitfalls:
Boil-the-ocean scope: trying to model the entire enterprise in one initiative instead of starting with 3-5 domains
Treating ontology as a one-time project instead of a living, governed structure with ownership and review cycles
Building ontology in isolation from the data catalog and lineage, which creates a disconnected model nobody uses
Over-engineering with academic OWL constructs when simpler property-graph models serve the same purpose for your use cases
How to evaluate ontology approaches
Evaluating ontology approaches requires assessing five criteria: openness of architecture, integration with existing data stack tools, support for incremental adoption, governance lifecycle capabilities, and AI agent compatibility.
Criterion What to assess Red flag Green flag
Architecture openness Does the ontology layer work across your full data stack or only within one vendor? Proprietary ontology locked to a single platform Open APIs, MCP support, multi-platform connectors
Incremental adoption Can you start with 3-5 domains and expand, or does it require full upfront modeling? Requires complete enterprise model before delivering value Domain-by-domain rollout with immediate value
Governance lifecycle Does ontology evolve as business context changes, with versioning and ownership? Static model with no change management Automated drift detection, domain ownership, review cadence
AI agent compatibility Can AI agents query the ontology dynamically for context during inference? Ontology is a reference document, not a queryable layer Real-time API or MCP access for AI agents
Stack integration depth Does it connect to your existing catalog, lineage, glossary, and semantic layer? Standalone ontology tool requiring separate data entry Native integration with catalog, lineage, dbt, BI tools
Total cost of adoption What is the time-to-value: weeks, months, or years? 12+ month implementation before first use case Weeks to first domain, months to cross-domain coverage
More than 80% of enterprises will have used generative AI APIs or deployed generative AI-enabled applications by 2026, according to Gartner. The ontology approach you choose now determines whether those AI deployments produce correct answers or plausible ones.
Questions to ask vendors:
How does your ontology layer integrate with our existing data catalog and lineage?
Can AI agents query ontology classes and relationships at inference time via API or MCP?
What does the governance model look like? Who owns ontology domains, and how are changes reviewed?
Can we start with a single domain and expand, or is full enterprise modeling required upfront?
How does your ontology handle evolution when business definitions change?
Does your ontology interoperate with open standards (RDF, OWL, property graphs) or is it proprietary?
What is the typical time-to-value for a first production ontology domain?
See how a context layer for Snowflake shows open ontology working in a real production environment.
Adaptive Data Governance for the AI Era
Comprehensive resource on adaptive data governance strategies for the AI era, covering modern approaches to unifying context across data and AI systems, enabling collaboration, and activating metadata for both human and AI-powered experiences.
Enter your email
How Atlan approaches ontology
Atlan operationalizes ontology through its metadata lakehouse and 4-graph architecture: a data graph, governance graph, knowledge graph, and active ontology graph that evolve as the data stack and business context change. AI agents access this living ontology through Atlans MCP server, grounding outputs in governed, structured context without requiring static upfront modeling.
Most organizations have semantic meaning scattered across glossaries, dbt models, schema documentation, and tribal knowledge. AI agents cannot access this fragmented context at inference time. Closed-architecture ontology platforms require migrating to a single vendor stack, which means ripping out existing tools before seeing any value.
Atlan takes a different approach. The active ontology is not a standalone modeling tool. It is a living graph within a metadata lakehouse that connects to 100+ data stack integrations. The four graphs work together: a data graph maps assets, schemas, and lineage across every platform. A governance graph tracks policies, ownership, quality rules, and compliance. A knowledge graph stores business terms, classifications, and semantic relationships. The active ontology graph maintains formal classes, properties, constraints, and reasoning rules that evolve automatically as the underlying data changes.
AI agents access this ontology through the MCP server (Model Context Protocol), which makes enterprise ontology queryable by any AI system. Every AI application in your stack can pull structured business context at inference time. Organizations bootstrap ontology from existing catalog metadata, glossary terms, and lineage. No multi-year modeling project. Domain-by-domain rollout with AI agents grounding outputs in governed context from day one.
Atlan is recognized as a Leader in the Gartner Magic Quadrant for Metadata Management Solutions 2025, a Leader in the Gartner Magic Quadrant for Data and Analytics Governance 2026, and a Leader in the Forrester Wave Q3 2024. Explore Atlans modern data catalog to see how it works.
See how Atlan operationalizes ontology for your AI agents.
FAQs about ontology
What is ontology in artificial intelligence and why does it matter?
Ontology in AI is a formal model defining concepts, properties, and relationships so machines interpret data accurately. AI agents need structured context to ground outputs in business meaning. Without ontology, AI treats every query as a new problem with no shared understanding of what terms mean.
What is the difference between ontology and taxonomy in data management?
A taxonomy organizes terms into parent-child hierarchies. An ontology goes further: it defines properties, constraints, and logical relationships between concepts, enabling machine reasoning. A taxonomy tells you “Revenue” is a “Financial Metric.” An ontology tells you what Revenue includes, excludes, and how it connects to other entities.
How do ontologies help AI agents understand enterprise data?
Ontologies give AI agents a formal map of business meaning. When an agent queries “revenue,” the ontology specifies which definition applies, what it includes or excludes, and how it relates to concepts like bookings or ARR. This prevents the agent from hallucinating or retrieving the wrong definition.
What is the difference between an ontology and a semantic layer?
A semantic layer maps business terms to physical data so analysts query metrics without writing SQL. An ontology defines formal meaning, relationships, and constraints between concepts at a deeper level. The semantic layer answers “where is this metric?” The ontology answers “what does this metric mean and how does it relate to other concepts?”
How do you build an enterprise ontology without a multi-year project?
Start with 3-5 priority domains where AI agents are active. Audit semantic assets in your data catalog, glossary, and schema documentation. Define core classes and relationships per domain. Connect to live metadata through lineage. Iterate monthly. Active metadata platforms bootstrap ontology incrementally from existing assets.
What role does ontology play in data governance?
Ontology provides governance with a formal semantic backbone. Policies reference ontology classes to define what terms mean, who owns them, and what rules apply. When ontology defines “PII_Field” with a “requires_masking” constraint, that rule propagates automatically across every platform that reads the ontology.
Is ontology the same as a knowledge graph?
Ontology and knowledge graph are related but distinct. An ontology defines formal structure: classes, properties, relationships, and constraints. A knowledge graph instantiates that structure with real data. The ontology says “Customer is a class with properties name, industry, and account_tier.” The knowledge graph says “Acme Corp is a Customer in Manufacturing with Enterprise tier.”
How does ontology reduce AI hallucination in enterprise applications?
Ontology reduces hallucination by providing a verified, structured layer of business meaning that agents consult before generating outputs. Instead of relying on pattern matching over unstructured documents, the agent queries the ontology to confirm what concepts mean, how they connect, and what constraints apply.
Why ontology is the missing layer between metadata and AI action
Ontology is the formal semantic layer that transforms disconnected metadata into a machine-readable model AI agents can reason over. Organizations that operationalize ontology through active metadata platforms reduce hallucination, scale governance across platforms, and deploy AI agents faster.
The pattern is consistent across the data in this guide. 88% of organizations use AI, but fewer than 40% have scaled past pilot. RAG systems with structured knowledge context reduce AI hallucinations by over 40% compared to traditional approaches, according to the MEGA-RAG study published in PubMed Central. The gap between pilot and production is a context gap. Ontology fills it by giving AI agents a formal model of what business terms mean, how they connect, and what constraints apply.
The key is starting incrementally. Pick 3-5 high-value domains. Audit existing semantic assets. Define classes, properties, and relationships. Connect them to live metadata through column-level lineage. Validate against real AI agent queries. Iterate monthly.
Evaluate any ontology approach on openness, incremental adoption, governance lifecycle, and AI agent compatibility. The right approach connects to your existing data catalog and lineage rather than replacing them. Active metadata platforms make ontology operational without the multi-year academic modeling project that stalled previous generations.
Your AI agents are already answering questions about your data. The question is whether those answers are grounded in verified meaning or statistical approximation.

View file

@ -0,0 +1,72 @@
<svg width="100%" viewBox="0 0 680 480" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</marker>
<mask id="imagine-text-gaps-qqsl7n" maskUnits="userSpaceOnUse"><rect x="0" y="0" width="680" height="480" fill="white"/><rect x="199.20655822753906" y="14.305530548095703" width="281.5868835449219" height="21.47225570678711" fill="black" rx="2"/><rect x="291.94305419921875" y="63.263877868652344" width="96.11387634277344" height="21.47225570678711" fill="black" rx="2"/><rect x="186.9730224609375" y="84.4444351196289" width="306.053955078125" height="19.11113929748535" fill="black" rx="2"/><rect x="239.27911376953125" y="138.26388549804688" width="201.4417724609375" height="21.47225570678711" fill="black" rx="2"/><rect x="221.48590087890625" y="159.44442749023438" width="237.0281982421875" height="19.11113929748535" fill="black" rx="2"/><rect x="303.5567932128906" y="219.2638702392578" width="72.88640594482422" height="21.47225570678711" fill="black" rx="2"/><rect x="154.2199249267578" y="242.44442749023438" width="371.5601501464844" height="19.11113929748535" fill="black" rx="2"/><rect x="207.94638061523438" y="258.4444580078125" width="264.10723876953125" height="19.11113929748535" fill="black" rx="2"/><rect x="260.5107116699219" y="308.26385498046875" width="158.97857666015625" height="21.47225570678711" fill="black" rx="2"/><rect x="141.7613525390625" y="329.4444580078125" width="396.477294921875" height="19.11113929748535" fill="black" rx="2"/><rect x="293.0756530761719" y="383.26385498046875" width="93.84868621826172" height="21.47225570678711" fill="black" rx="2"/><rect x="154.87661743164062" y="404.4444274902344" width="370.24676513671875" height="19.11113929748535" fill="black" rx="2"/><rect x="26" y="440.1944274902344" width="235.28688049316406" height="19.11113929748535" fill="black" rx="2"/><rect x="396" y="440.1944274902344" width="261.1263427734375" height="19.11113929748535" fill="black" rx="2"/><rect x="361.4732971191406" y="445.15277099609375" width="17.053401947021484" height="17.694470405578613" fill="black" rx="2"/><rect x="631.119140625" y="445.15277099609375" width="17.76173686981201" height="17.694470405578613" fill="black" rx="2"/></mask></defs>
<text x="340" y="30" text-anchor="middle" style="fill:rgb(250, 249, 245);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">Dónde encaja la ontología en el flujo de IA</text>
<!-- Layer 1: Datos crudos -->
<g onclick="sendPrompt('¿Qué son los datos crudos y por qué no bastan para la IA?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="40" y="55" width="600" height="52" rx="10" stroke-width="0.5" style="fill:rgb(68, 68, 65);stroke:rgb(180, 178, 169);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="340" y="74" text-anchor="middle" dominant-baseline="central" style="fill:rgb(211, 209, 199);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Datos crudos</text>
<text x="340" y="94" text-anchor="middle" dominant-baseline="central" style="fill:rgb(180, 178, 169);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Tablas, columnas, archivos — sin contexto de negocio</text>
</g>
<!-- Arrow -->
<line x1="340" y1="107" x2="340" y2="125" marker-end="url(#arrow)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Layer 2: Esquema + Catálogo -->
<g onclick="sendPrompt('¿Qué es un esquema y un catálogo de datos?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="40" y="130" width="600" height="52" rx="10" stroke-width="0.5" style="fill:rgb(12, 68, 124);stroke:rgb(133, 183, 235);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="340" y="149" text-anchor="middle" dominant-baseline="central" style="fill:rgb(181, 212, 244);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Esquema + catálogo de datos</text>
<text x="340" y="169" text-anchor="middle" dominant-baseline="central" style="fill:rgb(133, 183, 235);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Estructura técnica: tipos, nombres, linaje</text>
</g>
<!-- Arrow -->
<line x1="340" y1="182" x2="340" y2="200" marker-end="url(#arrow)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Layer 3: Ontología — highlighted -->
<g onclick="sendPrompt('¿Cómo se construye una ontología empresarial paso a paso?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="40" y="205" width="600" height="72" rx="10" stroke-width="1" style="fill:rgb(60, 52, 137);stroke:rgb(175, 169, 236);color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="340" y="230" text-anchor="middle" dominant-baseline="central" style="fill:rgb(206, 203, 246);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Ontología</text>
<text x="340" y="252" text-anchor="middle" dominant-baseline="central" style="fill:rgb(175, 169, 236);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Significado formal: clases, propiedades, relaciones, reglas lógicas</text>
<text x="340" y="268" text-anchor="middle" dominant-baseline="central" style="fill:rgb(175, 169, 236);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">La máquina entiende QUÉ significan los datos</text>
</g>
<!-- Arrow -->
<line x1="340" y1="277" x2="340" y2="295" marker-end="url(#arrow)" mask="url(#imagine-text-gaps-qqsl7n)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Layer 4: Grafo de conocimiento -->
<g onclick="sendPrompt('¿Qué es un grafo de conocimiento y cómo usa la ontología?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="40" y="300" width="600" height="52" rx="10" stroke-width="0.5" style="fill:rgb(8, 80, 65);stroke:rgb(93, 202, 165);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="340" y="319" text-anchor="middle" dominant-baseline="central" style="fill:rgb(159, 225, 203);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Grafo de conocimiento</text>
<text x="340" y="339" text-anchor="middle" dominant-baseline="central" style="fill:rgb(93, 202, 165);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Datos reales instanciados: "Acme Corp es un Cliente en Manufactura"</text>
</g>
<!-- Arrow -->
<line x1="340" y1="352" x2="340" y2="370" marker-end="url(#arrow)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Layer 5: Agente de IA -->
<g onclick="sendPrompt('¿Cómo usa un agente de IA la ontología para responder preguntas?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="40" y="375" width="600" height="52" rx="10" stroke-width="0.5" style="fill:rgb(113, 43, 19);stroke:rgb(240, 153, 123);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="340" y="394" text-anchor="middle" dominant-baseline="central" style="fill:rgb(245, 196, 179);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Agente de IA</text>
<text x="340" y="414" text-anchor="middle" dominant-baseline="central" style="fill:rgb(240, 153, 123);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Consulta la ontología antes de responder → menos alucinaciones</text>
</g>
<!-- Side annotations -->
<text x="30" y="454" text-anchor="start" style="fill:rgb(194, 192, 182);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:auto">Sin ontología: la IA adivina significados →</text>
<text x="330" y="454" text-anchor="start" style="fill:rgb(194, 192, 182);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:auto">Con ontología: la IA razona sobre significados →</text>
<!-- Visual indicator: X vs Check -->
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<circle cx="280" cy="450" r="10" stroke-width="0.5" style="fill:rgb(121, 31, 31);stroke:rgb(240, 149, 149);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="280" y="450" text-anchor="middle" dominant-baseline="central" style="font-size:11px;fill:rgb(247, 193, 193);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:11px;font-weight:500;text-anchor:middle;dominant-baseline:central"></text>
</g>
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<circle cx="625" cy="450" r="10" stroke-width="0.5" style="fill:rgb(39, 80, 10);stroke:rgb(151, 196, 89);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="625" y="450" text-anchor="middle" dominant-baseline="central" style="font-size:11px;fill:rgb(192, 221, 151);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:11px;font-weight:500;text-anchor:middle;dominant-baseline:central"></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,97 @@
<svg width="100%" viewBox="0 0 680 540" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</marker>
</defs>
<!-- Title region -->
<text x="340" y="30" text-anchor="middle" style="fill:rgb(250, 249, 245);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">Los tres bloques de toda ontología</text>
<!-- CLASES -->
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="40" y="60" width="180" height="170" rx="14" stroke-width="0.5" style="fill:rgb(60, 52, 137);stroke:rgb(175, 169, 236);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="130" y="88" text-anchor="middle" dominant-baseline="central" style="fill:rgb(206, 203, 246);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Clases</text>
<text x="130" y="108" text-anchor="middle" dominant-baseline="central" style="fill:rgb(175, 169, 236);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Categorías de cosas</text>
</g>
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="60" y="130" width="140" height="36" rx="6" stroke-width="0.5" style="fill:rgb(60, 52, 137);stroke:rgb(175, 169, 236);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="130" y="148" text-anchor="middle" dominant-baseline="central" style="fill:rgb(175, 169, 236);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Cliente</text>
</g>
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="60" y="176" width="140" height="36" rx="6" stroke-width="0.5" style="fill:rgb(60, 52, 137);stroke:rgb(175, 169, 236);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="130" y="194" text-anchor="middle" dominant-baseline="central" style="fill:rgb(175, 169, 236);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Ingreso</text>
</g>
<!-- PROPIEDADES -->
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="250" y="60" width="180" height="170" rx="14" stroke-width="0.5" style="fill:rgb(8, 80, 65);stroke:rgb(93, 202, 165);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="340" y="88" text-anchor="middle" dominant-baseline="central" style="fill:rgb(159, 225, 203);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Propiedades</text>
<text x="340" y="108" text-anchor="middle" dominant-baseline="central" style="fill:rgb(93, 202, 165);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Atributos que describen</text>
</g>
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="270" y="130" width="140" height="36" rx="6" stroke-width="0.5" style="fill:rgb(8, 80, 65);stroke:rgb(93, 202, 165);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="340" y="148" text-anchor="middle" dominant-baseline="central" style="fill:rgb(93, 202, 165);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">nombre, industria</text>
</g>
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="270" y="176" width="140" height="36" rx="6" stroke-width="0.5" style="fill:rgb(8, 80, 65);stroke:rgb(93, 202, 165);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="340" y="194" text-anchor="middle" dominant-baseline="central" style="fill:rgb(93, 202, 165);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">moneda, período</text>
</g>
<!-- RELACIONES -->
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="460" y="60" width="180" height="170" rx="14" stroke-width="0.5" style="fill:rgb(113, 43, 19);stroke:rgb(240, 153, 123);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="550" y="88" text-anchor="middle" dominant-baseline="central" style="fill:rgb(245, 196, 179);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Relaciones</text>
<text x="550" y="108" text-anchor="middle" dominant-baseline="central" style="fill:rgb(240, 153, 123);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Cómo se conectan</text>
</g>
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="480" y="130" width="140" height="36" rx="6" stroke-width="0.5" style="fill:rgb(113, 43, 19);stroke:rgb(240, 153, 123);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="550" y="148" text-anchor="middle" dominant-baseline="central" style="fill:rgb(240, 153, 123);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Cliente → Cuenta</text>
</g>
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="480" y="176" width="140" height="36" rx="6" stroke-width="0.5" style="fill:rgb(113, 43, 19);stroke:rgb(240, 153, 123);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="550" y="194" text-anchor="middle" dominant-baseline="central" style="fill:rgb(240, 153, 123);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Cuenta → Ingreso</text>
</g>
<!-- Arrows between columns -->
<line x1="222" y1="148" x2="268" y2="148" marker-end="url(#arrow)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<line x1="432" y1="148" x2="478" y2="148" marker-end="url(#arrow)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Separator -->
<line x1="40" y1="260" x2="640" y2="260" stroke="var(--color-border-tertiary)" stroke-width="0.5" style="fill:rgb(0, 0, 0);stroke:rgba(222, 220, 209, 0.15);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Comparison table header -->
<text x="340" y="290" text-anchor="middle" style="fill:rgb(250, 249, 245);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">Ontología vs. sus conceptos vecinos</text>
<!-- Row headers -->
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="40" y="310" width="120" height="36" rx="6" stroke-width="0.5" style="fill:rgb(68, 68, 65);stroke:rgb(180, 178, 169);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="100" y="328" text-anchor="middle" dominant-baseline="central" style="fill:rgb(211, 209, 199);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Taxonomía</text>
</g>
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="40" y="356" width="120" height="36" rx="6" stroke-width="0.5" style="fill:rgb(68, 68, 65);stroke:rgb(180, 178, 169);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="100" y="374" text-anchor="middle" dominant-baseline="central" style="fill:rgb(211, 209, 199);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Esquema</text>
</g>
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="40" y="402" width="120" height="36" rx="6" stroke-width="0.5" style="fill:rgb(68, 68, 65);stroke:rgb(180, 178, 169);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="100" y="420" text-anchor="middle" dominant-baseline="central" style="fill:rgb(211, 209, 199);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Capa semántica</text>
</g>
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="40" y="448" width="120" height="36" rx="6" stroke-width="0.5" style="fill:rgb(68, 68, 65);stroke:rgb(180, 178, 169);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="100" y="466" text-anchor="middle" dominant-baseline="central" style="fill:rgb(211, 209, 199);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Grafo de conoc.</text>
</g>
<!-- Descriptions -->
<text x="180" y="332" dominant-baseline="central" style="fill:rgb(194, 192, 182);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">Clasifica en árbol: Métrica → Ingreso → Suscripción</text>
<text x="180" y="378" dominant-baseline="central" style="fill:rgb(194, 192, 182);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">Estructura de datos: columna, tipo, restricción</text>
<text x="180" y="424" dominant-baseline="central" style="fill:rgb(194, 192, 182);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">Traduce términos de negocio a consultas de datos</text>
<text x="180" y="470" dominant-baseline="central" style="fill:rgb(194, 192, 182);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">Datos reales organizados según la ontología</text>
<!-- Ontology highlight -->
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="40" y="494" width="120" height="36" rx="6" stroke-width="0.5" style="fill:rgb(60, 52, 137);stroke:rgb(175, 169, 236);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="100" y="512" text-anchor="middle" dominant-baseline="central" style="fill:rgb(206, 203, 246);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Ontología</text>
</g>
<text x="180" y="516" dominant-baseline="central" style="fill:rgb(194, 192, 182);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:central">Define significado formal + relaciones + reglas lógicas</text>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,94 @@
<svg width="100%" viewBox="0 0 680 620" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</marker>
<mask id="imagine-text-gaps-ug4a1o" maskUnits="userSpaceOnUse"><rect x="0" y="0" width="680" height="620" fill="white"/><rect x="269.4939270019531" y="34.30551528930664" width="141.0120849609375" height="21.472270965576172" fill="black" rx="2"/><rect x="109.16131591796875" y="101.26386260986328" width="101.67733764648438" height="21.472270965576172" fill="black" rx="2"/><rect x="97.87960052490234" y="126.44442749023438" width="124.24076843261719" height="19.11115264892578" fill="black" rx="2"/><rect x="123.49036407470703" y="231.2638702392578" width="73.01927947998047" height="21.472270965576172" fill="black" rx="2"/><rect x="106.5972900390625" y="256.44439697265625" width="106.80538940429688" height="19.11115264892578" fill="black" rx="2"/><rect x="105.02567291259766" y="361.26385498046875" width="109.9486312866211" height="21.472270965576172" fill="black" rx="2"/><rect x="107.89221954345703" y="386.4444274902344" width="104.21554565429688" height="19.11115264892578" fill="black" rx="2"/><rect x="125.44197082519531" y="491.26385498046875" width="69.11605453491211" height="21.472270965576172" fill="black" rx="2"/><rect x="91.95098114013672" y="516.4443969726562" width="136.09800720214844" height="19.11115264892578" fill="black" rx="2"/><rect x="-24.514450073242188" y="306.1944274902344" width="97.02889251708984" height="19.11115264892578" fill="black" rx="2"/><rect x="444.6635437011719" y="113.26387023925781" width="70.67291641235352" height="21.472270965576172" fill="black" rx="2"/><rect x="384.66900634765625" y="136.44442749023438" width="190.66195678710938" height="19.11115264892578" fill="black" rx="2"/><rect x="430.5447998046875" y="213.26385498046875" width="98.91040802001953" height="21.472270965576172" fill="black" rx="2"/><rect x="395.35675048828125" y="236.44442749023438" width="169.28646850585938" height="19.11115264892578" fill="black" rx="2"/><rect x="430.4119873046875" y="313.26385498046875" width="99.17603302001953" height="21.472270965576172" fill="black" rx="2"/><rect x="409.8481140136719" y="336.4444274902344" width="140.3037567138672" height="19.11115264892578" fill="black" rx="2"/><rect x="417.3557434082031" y="413.26385498046875" width="125.28851318359375" height="21.472270965576172" fill="black" rx="2"/><rect x="399.03125" y="436.44439697265625" width="161.93748474121094" height="19.11115264892578" fill="black" rx="2"/><rect x="411.8144836425781" y="513.2638549804688" width="136.3710174560547" height="21.472270965576172" fill="black" rx="2"/><rect x="386.0414123535156" y="536.4443969726562" width="187.9171600341797" height="19.11115264892578" fill="black" rx="2"/></mask></defs>
<!-- Outer container: El sistema completo -->
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="30" y="20" width="620" height="580" rx="20" stroke-width="0.5" style="fill:rgb(68, 68, 65);stroke:rgb(180, 178, 169);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="340" y="50" text-anchor="middle" style="fill:rgb(211, 209, 199);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">El sistema completo</text>
</g>
<!-- Layer 1: Tú -->
<g onclick="sendPrompt('Explícame más sobre el rol del usuario en la ontología operacional')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="60" y="80" width="200" height="90" rx="12" stroke-width="0.5" style="fill:rgb(60, 52, 137);stroke:rgb(175, 169, 236);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="160" y="112" text-anchor="middle" dominant-baseline="central" style="fill:rgb(206, 203, 246);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Tú (el usuario)</text>
<text x="160" y="136" text-anchor="middle" dominant-baseline="central" style="fill:rgb(175, 169, 236);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Intención + contexto</text>
</g>
<!-- Layer 2: El prompt -->
<g onclick="sendPrompt('¿Cómo construir mejores prompts?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="60" y="210" width="200" height="90" rx="12" stroke-width="0.5" style="fill:rgb(8, 80, 65);stroke:rgb(93, 202, 165);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="160" y="242" text-anchor="middle" dominant-baseline="central" style="fill:rgb(159, 225, 203);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">El prompt</text>
<text x="160" y="266" text-anchor="middle" dominant-baseline="central" style="fill:rgb(93, 202, 165);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Tu mensaje a la IA</text>
</g>
<!-- Layer 3: El modelo -->
<g onclick="sendPrompt('¿Cómo procesa la IA mi mensaje?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="60" y="340" width="200" height="90" rx="12" stroke-width="0.5" style="fill:rgb(113, 43, 19);stroke:rgb(240, 153, 123);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="160" y="372" text-anchor="middle" dominant-baseline="central" style="fill:rgb(245, 196, 179);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">El modelo de IA</text>
<text x="160" y="396" text-anchor="middle" dominant-baseline="central" style="fill:rgb(240, 153, 123);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Procesa y genera</text>
</g>
<!-- Layer 4: El output -->
<g onclick="sendPrompt('¿Cómo evaluar la calidad de un output de IA?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="60" y="470" width="200" height="90" rx="12" stroke-width="0.5" style="fill:rgb(99, 56, 6);stroke:rgb(239, 159, 39);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="160" y="502" text-anchor="middle" dominant-baseline="central" style="fill:rgb(250, 199, 117);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">El output</text>
<text x="160" y="526" text-anchor="middle" dominant-baseline="central" style="fill:rgb(239, 159, 39);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">La respuesta generada</text>
</g>
<!-- Arrows down the left column -->
<line x1="160" y1="170" x2="160" y2="208" marker-end="url(#arrow)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<line x1="160" y1="300" x2="160" y2="338" marker-end="url(#arrow)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<line x1="160" y1="430" x2="160" y2="468" marker-end="url(#arrow)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<!-- Return arrow (feedback loop) -->
<path d="M60 515 L36 515 L36 125 L58 125" fill="none" marker-end="url(#arrow)" stroke-dasharray="4 4" mask="url(#imagine-text-gaps-ug4a1o)" style="fill:none;stroke:rgb(156, 154, 146);color:rgb(255, 255, 255);stroke-width:1.5px;stroke-dasharray:4px, 4px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="24" y="320" text-anchor="middle" transform="rotate(-90, 24, 320)" style="fill:rgb(194, 192, 182);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Iteras y mejoras</text>
<!-- Right side: Las dimensiones invisibles -->
<!-- Contexto -->
<g onclick="sendPrompt('¿Qué es el contexto en una conversación con IA?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="340" y="100" width="280" height="70" rx="12" stroke-width="0.5" style="fill:rgb(12, 68, 124);stroke:rgb(133, 183, 235);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="480" y="124" text-anchor="middle" dominant-baseline="central" style="fill:rgb(181, 212, 244);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Contexto</text>
<text x="480" y="146" text-anchor="middle" dominant-baseline="central" style="fill:rgb(133, 183, 235);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Memoria, historial, instrucciones</text>
</g>
<!-- Herramientas -->
<g onclick="sendPrompt('¿Qué herramientas puede usar la IA?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="340" y="200" width="280" height="70" rx="12" stroke-width="0.5" style="fill:rgb(39, 80, 10);stroke:rgb(151, 196, 89);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="480" y="224" text-anchor="middle" dominant-baseline="central" style="fill:rgb(192, 221, 151);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Herramientas</text>
<text x="480" y="246" text-anchor="middle" dominant-baseline="central" style="fill:rgb(151, 196, 89);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Búsqueda, archivos, cálculos</text>
</g>
<!-- Restricciones -->
<g onclick="sendPrompt('¿Qué limitaciones tiene la IA?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="340" y="300" width="280" height="70" rx="12" stroke-width="0.5" style="fill:rgb(114, 36, 62);stroke:rgb(237, 147, 177);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="480" y="324" text-anchor="middle" dominant-baseline="central" style="fill:rgb(244, 192, 209);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Restricciones</text>
<text x="480" y="346" text-anchor="middle" dominant-baseline="central" style="fill:rgb(237, 147, 177);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Límites, ética, precisión</text>
</g>
<!-- Formato -->
<g onclick="sendPrompt('¿Cómo pedir el formato correcto a la IA?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="340" y="400" width="280" height="70" rx="12" stroke-width="0.5" style="fill:rgb(68, 68, 65);stroke:rgb(180, 178, 169);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="480" y="424" text-anchor="middle" dominant-baseline="central" style="fill:rgb(211, 209, 199);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Formato de salida</text>
<text x="480" y="446" text-anchor="middle" dominant-baseline="central" style="fill:rgb(180, 178, 169);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Texto, tabla, código, archivo</text>
</g>
<!-- Conocimiento -->
<g onclick="sendPrompt('¿Qué sabe y qué no sabe la IA?')" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="340" y="500" width="280" height="70" rx="12" stroke-width="0.5" style="fill:rgb(113, 43, 19);stroke:rgb(240, 153, 123);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="480" y="524" text-anchor="middle" dominant-baseline="central" style="fill:rgb(245, 196, 179);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Conocimiento base</text>
<text x="480" y="546" text-anchor="middle" dominant-baseline="central" style="fill:rgb(240, 153, 123);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Entrenamiento + búsqueda web</text>
</g>
<!-- Connecting lines from right column to model -->
<line x1="338" y1="135" x2="262" y2="255" stroke="var(--color-border-secondary)" stroke-width="0.5" stroke-dasharray="3 3" style="fill:rgb(0, 0, 0);stroke:rgba(222, 220, 209, 0.3);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-dasharray:3px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<line x1="338" y1="235" x2="262" y2="355" stroke="var(--color-border-secondary)" stroke-width="0.5" stroke-dasharray="3 3" style="fill:rgb(0, 0, 0);stroke:rgba(222, 220, 209, 0.3);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-dasharray:3px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<line x1="338" y1="335" x2="262" y2="375" stroke="var(--color-border-secondary)" stroke-width="0.5" stroke-dasharray="3 3" style="fill:rgb(0, 0, 0);stroke:rgba(222, 220, 209, 0.3);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-dasharray:3px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<line x1="338" y1="435" x2="262" y2="395" stroke="var(--color-border-secondary)" stroke-width="0.5" stroke-dasharray="3 3" style="fill:rgb(0, 0, 0);stroke:rgba(222, 220, 209, 0.3);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-dasharray:3px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<line x1="338" y1="535" x2="262" y2="385" stroke="var(--color-border-secondary)" stroke-width="0.5" stroke-dasharray="3 3" style="fill:rgb(0, 0, 0);stroke:rgba(222, 220, 209, 0.3);color:rgb(255, 255, 255);stroke-width:0.5px;stroke-dasharray:3px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
</svg>

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -0,0 +1,23 @@
Ahora entiendo mucho mejor lo que buscas. No era "cómo funciona usar IA", sino el concepto formal de **ontología en IA**: cómo se estructura el conocimiento para que las máquinas entiendan significado, no solo datos. Vamos a ello.Ahora sí vamos con la explicación correcta.
**Ontología en IA** no es "cómo usas la IA". Es el **modelo formal que le dice a una máquina qué cosas existen en un dominio, qué las describe y cómo se conectan entre sí**. Viene de la filosofía (Aristóteles ya se preguntaba "¿qué cosas existen?"), pero en los años 90 los informáticos lo adaptaron para que las máquinas pudieran "entender" significado, no solo procesar texto.
Toda ontología se construye con tres bloques. Las **clases** son las categorías de entidades de tu mundo: "Cliente", "Ingreso", "Producto", "Pedido". Las **propiedades** son los atributos que describen cada clase: un Ingreso tiene una moneda, un período fiscal, un indicador de si incluye devoluciones. Y las **relaciones** conectan las clases entre sí: un Cliente posee una Cuenta, esa Cuenta genera Ingresos.
¿Por qué esto importa para la IA? Porque sin ontología, cuando le preguntas a un agente de IA "¿cuántos ingresos tuvimos este trimestre?", el agente no sabe si te refieres a ingresos reconocidos según normas contables, a reservas de ventas, o a facturación recurrente mensual. Simplemente toma la primera definición que encuentra. La ontología le da al agente un mapa explícito de lo que "ingreso" significa en contexto: qué incluye, qué excluye y qué definición aplica según el equipo.
Ahora veamos **por qué la IA empresarial necesita esto** y cómo encaja en el panorama más amplio:Piénsalo como las capas de un edificio, de abajo hacia arriba:
**Datos crudos** son las tablas y archivos en bruto. No tienen contexto — es como tener miles de cajas sin etiquetas en un almacén.
**Esquema y catálogo** le ponen estructura técnica: nombres de columnas, tipos de datos, de dónde viene cada dato. Ahora las cajas tienen etiquetas, pero las etiquetas son códigos técnicos.
**La ontología** es la capa que falta en la mayoría de las organizaciones. Es donde defines formalmente que "Ingreso" es una clase que incluye "Ingreso por suscripción" pero excluye "Pagos únicos", que pertenece a la categoría "Métricas financieras" y está vinculada a "Cuenta" que a su vez pertenece a "Cliente". Esto permite algo crucial: **la inferencia**. Si la ontología dice que el Ingreso incluye Ingreso por Suscripción y que este excluye Pagos Únicos, la máquina puede deducir *sin que nadie se lo programe* que el Ingreso también excluye Pagos Únicos.
**El grafo de conocimiento** toma esa estructura y la llena con datos reales: "Acme Corp es un Cliente de la industria Manufactura con nivel Enterprise".
**El agente de IA** consulta todo esto cuando le haces una pregunta. En vez de adivinar qué significa "ingreso", navega la ontología, encuentra la definición correcta y responde con precisión.
Para resumir la diferencia clave entre los "primos" de la ontología: la taxonomía solo clasifica en árbol (padre-hijo); el esquema define estructura técnica; la capa semántica traduce términos de negocio a consultas; el grafo de conocimiento almacena datos reales. La ontología es la única que define **significado formal con reglas lógicas que permiten razonamiento automático**.
¿Quieres que profundice en alguna de las capas? Puedes tocar cualquier caja del diagrama, o preguntarme sobre cómo se construye una ontología paso a paso, o cómo se ve en formatos técnicos como RDF y OWL explicados de forma sencilla.

View file

@ -0,0 +1,27 @@
# Ontología en IA
Cómo se estructura el conocimiento para que las máquinas entiendan significado, no solo datos. Explicación correcta.
<img src="ontologia_ia_clases_propiedades_relaciones.svg">
**Ontología en IA** no es "cómo usas la IA". Es el **modelo formal que le dice a una máquina qué cosas existen en un dominio, qué las describe y cómo se conectan entre sí**. Viene de la filosofía (Aristóteles ya se preguntaba "¿qué cosas existen?"), pero en los años 90 los informáticos lo adaptaron para que las máquinas pudieran "entender" significado, no solo procesar texto.
Toda ontología se construye con tres bloques. Las **clases** son las categorías de entidades de tu mundo: "Cliente", "Ingreso", "Producto", "Pedido". Las **propiedades** son los atributos que describen cada clase: un Ingreso tiene una moneda, un período fiscal, un indicador de si incluye devoluciones. Y las **relaciones** conectan las clases entre sí: un Cliente posee una Cuenta, esa Cuenta genera Ingresos.
¿Por qué esto importa para la IA? Porque sin ontología, cuando le preguntas a un agente de IA "¿cuántos ingresos tuvimos este trimestre?", el agente no sabe si te refieres a ingresos reconocidos según normas contables, a reservas de ventas, o a facturación recurrente mensual. Simplemente toma la primera definición que encuentra. La ontología le da al agente un mapa explícito de lo que "ingreso" significa en contexto: qué incluye, qué excluye y qué definición aplica según el equipo.
Ahora veamos **por qué la IA empresarial necesita esto** y cómo encaja en el panorama más amplio:Piénsalo como las capas de un edificio, de abajo hacia arriba:
<img src="ontologia_cadena_contexto_ia.svg">
**Datos crudos** son las tablas y archivos en bruto. No tienen contexto — es como tener miles de cajas sin etiquetas en un almacén.
**Esquema y catálogo** le ponen estructura técnica: nombres de columnas, tipos de datos, de dónde viene cada dato. Ahora las cajas tienen etiquetas, pero las etiquetas son códigos técnicos.
**La ontología** es la capa que falta en la mayoría de las organizaciones. Es donde defines formalmente que "Ingreso" es una clase que incluye "Ingreso por suscripción" pero excluye "Pagos únicos", que pertenece a la categoría "Métricas financieras" y está vinculada a "Cuenta" que a su vez pertenece a "Cliente". Esto permite algo crucial: **la inferencia**. Si la ontología dice que el Ingreso incluye Ingreso por Suscripción y que este excluye Pagos Únicos, la máquina puede deducir *sin que nadie se lo programe* que el Ingreso también excluye Pagos Únicos.
**El grafo de conocimiento** toma esa estructura y la llena con datos reales: "Acme Corp es un Cliente de la industria Manufactura con nivel Enterprise".
**El agente de IA** consulta todo esto cuando le haces una pregunta. En vez de adivinar qué significa "ingreso", navega la ontología, encuentra la definición correcta y responde con precisión.
Para resumir la diferencia clave entre los "primos" de la ontología: la taxonomía solo clasifica en árbol (padre-hijo); el esquema define estructura técnica; la capa semántica traduce términos de negocio a consultas; el grafo de conocimiento almacena datos reales. La ontología es la única que define **significado formal con reglas lógicas que permiten razonamiento automático**.

File diff suppressed because one or more lines are too long

View file

@ -5,7 +5,7 @@
set -e
BASE_DIR="$(dirname "$0")"
FILES=("index.html" "architecture-diagram.html" "architecture-diagram-details.html")
FILES=("index.html" "personal.html" "provisioning.html" "architecture-diagram.html" "architecture-diagram-details.html")
minify_file() {
local filename="$1"

1
assets/web/personal.html Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -66,8 +66,10 @@
right: 2rem;
z-index: 100;
display: flex;
gap: 0.5rem;
background: rgba(255, 255, 255, 0.05);
gap: 0.25rem;
background: rgba(15, 23, 42, 0.88);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
border: 1px solid rgba(96, 165, 250, 0.3);
border-radius: 20px;
padding: 0.3rem 0.3rem;
@ -77,7 +79,7 @@
background: transparent;
border: none;
color: #94a3b8;
padding: 0.5rem 1rem;
padding: 0.4rem 0.75rem;
border-radius: 18px;
cursor: pointer;
font-weight: 700;
@ -997,7 +999,7 @@
);
}
html.light .language-toggle {
background: rgba(0, 0, 0, 0.05);
background: rgba(248, 250, 252, 0.92);
border-color: rgba(96, 165, 250, 0.35);
}
html.light .lang-btn {
@ -1293,10 +1295,26 @@
<a
href="architecture-diagram.html"
class="lang-btn"
data-en="Architecture"
data-es="Arquitectura"
data-en="Arch"
data-es="Arch"
data-key="ontoref-architecture-title"
>Architecture</a
>Arch</a
>
<a
href="personal.html"
class="lang-btn"
data-en="Personal"
data-es="Personal"
data-key="nav-personal"
>Personal</a
>
<a
href="provisioning.html"
class="lang-btn"
data-en="Prov"
data-es="Prov"
data-key="nav-provisioning"
>Prov</a
>
<button
id="theme-btn"
@ -2107,6 +2125,182 @@
</div>
</section>
<!-- ── PROJECT TYPES / DOMAIN EXTENSIONS ── -->
<section class="section">
<h2 class="section-title">
<span
data-en="Project Types &mdash; Domain Extensions"
data-es="Tipos de Proyecto &mdash; Extensiones de Dominio"
data-key="ontoref-domains-title"
>Project Types &mdash; Domain Extensions</span
>
</h2>
<p
style="
color: #94a3b8;
margin-bottom: 2rem;
font-size: 0.95rem;
line-height: 1.7;
max-width: 800px;
margin-left: auto;
margin-right: auto;
text-align: center;
"
data-en="The CLI adapts to your project type. <code>ontoref setup --kind X</code> writes the <code>repo_kind</code> field into your manifest and activates domain-specific commands under a short alias. Domain modules extend the ontology schema, add CLI subcommands, and surface new daemon UI pages — without touching the core protocol."
data-es="La CLI se adapta al tipo de proyecto. <code>ontoref setup --kind X</code> escribe el campo <code>repo_kind</code> en tu manifest y activa comandos específicos de dominio bajo un alias corto. Los módulos de dominio extienden el esquema de ontología, añaden subcomandos CLI y exponen nuevas páginas de UI en el daemon — sin tocar el protocolo base."
data-key="ontoref-domains-intro"
>
The CLI adapts to your project type. <code>ontoref setup --kind X</code> writes the <code>repo_kind</code> field into your manifest and activates domain-specific commands under a short alias. Domain modules extend the ontology schema, add CLI subcommands, and surface new daemon UI pages — without touching the core protocol.
</p>
<div class="features-grid">
<!-- Generic -->
<div class="feature-box" style="border-left-color: #60a5fa">
<div class="feature-title" style="color: #60a5fa">
Generic
</div>
<div
style="
font-size: 0.78rem;
color: #475569;
font-family: 'JetBrains Mono', monospace;
margin-bottom: 1rem;
"
>
Library &middot; Service &middot; PublishedCrate
</div>
<ul class="feature-text">
<li
data-en="Standard protocol surface — no domain activation"
data-es="Superficie de protocolo estándar — sin activación de dominio"
data-key="ontoref-domain-generic-1"
>Standard protocol surface &mdash; no domain activation</li>
<li
data-en="Full ontology, ADR, reflection mode, and backlog support"
data-es="Soporte completo de ontología, ADR, modos de reflexión y backlog"
data-key="ontoref-domain-generic-2"
>Full ontology, ADR, reflection mode, and backlog support</li>
<li
data-en="Daemon UI: /overview · /graph · /adrs · /backlog · /coder"
data-es="UI del daemon: /overview · /graph · /adrs · /backlog · /coder"
data-key="ontoref-domain-generic-3"
>Daemon UI: /overview &middot; /graph &middot; /adrs &middot; /backlog &middot; /coder</li>
<li
data-en="No alias — all commands via ontoref describe / ontoref adr / etc."
data-es="Sin alias — todos los comandos vía ontoref describe / ontoref adr / etc."
data-key="ontoref-domain-generic-4"
>No alias &mdash; all commands via <code style="font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: #60a5fa; background: rgba(96,165,250,0.1); padding: 0.05rem 0.3rem; border-radius: 3px">ontoref</code></li>
</ul>
<div class="code-block" style="margin-top: 1.25rem; padding: 1rem; font-size: 0.75rem">
<span class="kw">ontoref</span> setup --kind Library<br />
<span class="kw">ontoref</span> describe project<br />
<span class="kw">ontoref</span> adr list --status Accepted
</div>
</div>
<!-- Personal Ontology -->
<div class="feature-box" style="border-left-color: #c084fc; background: linear-gradient(135deg, rgba(192,132,252,0.08) 0%, rgba(192,132,252,0.04) 100%)">
<div class="feature-title" style="color: #c084fc">
Personal Ontology
</div>
<div
style="
font-size: 0.78rem;
color: #475569;
font-family: 'JetBrains Mono', monospace;
margin-bottom: 1rem;
"
>
PersonalOntology &mdash; alias: <span style="color: #c084fc">personal</span>
</div>
<ul class="feature-text">
<li
data-en="Career history as queryable graph: skills, talks, publications, positioning"
data-es="Historial profesional como grafo consultable: habilidades, charlas, publicaciones, posicionamiento"
data-key="ontoref-domain-personal-1"
>Career history as queryable graph: skills, talks, publications, positioning</li>
<li
data-en="CFP pipeline with stage tracking (Watching → Delivered)"
data-es="Pipeline de CFP con seguimiento de etapas (Watching → Delivered)"
data-key="ontoref-domain-personal-2"
>CFP pipeline with stage tracking (Watching &rarr; Delivered)</li>
<li
data-en="Opportunities and content lifecycle management"
data-es="Gestión del ciclo de vida de oportunidades y contenido"
data-key="ontoref-domain-personal-3"
>Opportunities and content lifecycle management</li>
<li
data-en="Daemon UI: /career (Skills · Experience · Talks) and /personal (Content · Opportunities · CFP)"
data-es="UI del daemon: /career (Skills · Experience · Talks) y /personal (Content · Opportunities · CFP)"
data-key="ontoref-domain-personal-4"
>Daemon UI: /career &middot; /personal</li>
</ul>
<div class="code-block" style="margin-top: 1.25rem; padding: 1rem; font-size: 0.75rem; border-color: rgba(192,132,252,0.3)">
<span class="kw">ontoref</span> setup --kind PersonalOntology<br />
<span class="kw">personal</span> career skills --tier Expert<br />
<span class="kw">personal</span> cfp --stage Drafting
</div>
<div style="margin-top: 1rem; font-size: 0.82rem">
<a href="personal.html" style="color: #c084fc; text-decoration: none; font-weight: 600"
data-en="&rarr; Personal domain docs"
data-es="&rarr; Documentación del dominio Personal"
data-key="ontoref-domain-personal-link"
>&rarr; Personal domain docs</a>
</div>
</div>
<!-- DevWorkspace / Mixed -->
<div class="feature-box" style="border-left-color: #f97316; background: linear-gradient(135deg, rgba(249,115,22,0.08) 0%, rgba(249,115,22,0.04) 100%)">
<div class="feature-title" style="color: #f97316">
DevWorkspace / Mixed
</div>
<div
style="
font-size: 0.78rem;
color: #475569;
font-family: 'JetBrains Mono', monospace;
margin-bottom: 1rem;
"
>
DevWorkspace &middot; Mixed &mdash; alias: <span style="color: #f97316">prov</span>
</div>
<ul class="feature-text">
<li
data-en="Infrastructure state as FSM: dimensions, transitions, blockers, catalysts"
data-es="Estado de infraestructura como FSM: dimensiones, transiciones, bloqueadores, catalizadores"
data-key="ontoref-domain-prov-1"
>Infrastructure state as FSM: dimensions, transitions, blockers, catalysts</li>
<li
data-en="Upstream/downstream connection graph and membrane gate status"
data-es="Grafo de conexiones upstream/downstream y estado de membranas"
data-key="ontoref-domain-prov-2"
>Upstream/downstream connection graph and membrane gate status</li>
<li
data-en="Workspace card: identity, clusters, platform capabilities"
data-es="Tarjeta de workspace: identidad, clusters, capacidades de plataforma"
data-key="ontoref-domain-prov-3"
>Workspace card: identity, clusters, platform capabilities</li>
<li
data-en="Daemon UI: /provisioning (State · Connections · Gates)"
data-es="UI del daemon: /provisioning (State · Connections · Gates)"
data-key="ontoref-domain-prov-4"
>Daemon UI: /provisioning (State &middot; Connections &middot; Gates)</li>
</ul>
<div class="code-block" style="margin-top: 1.25rem; padding: 1rem; font-size: 0.75rem; border-color: rgba(249,115,22,0.3)">
<span class="kw">ontoref</span> setup --kind DevWorkspace<br />
<span class="kw">prov</span> state<br />
<span class="kw">prov</span> connections
</div>
<div style="margin-top: 1rem; font-size: 0.82rem">
<a href="provisioning.html" style="color: #f97316; text-decoration: none; font-weight: 600"
data-en="&rarr; Provisioning domain docs"
data-es="&rarr; Documentación del dominio Provisioning"
data-key="ontoref-domain-prov-link"
>&rarr; Provisioning domain docs</a>
</div>
</div>
</div>
</section>
<!-- ── DAEMON & MCP ── -->
<section class="section">
<h2 class="section-title">

1160
assets/web/src/personal.html Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -238,6 +238,9 @@ pub struct ProjectRegistry {
primary_slug: String,
stale_actor_timeout: u64,
max_notifications: usize,
/// Insertion order preserved from projects.ncl declaration order.
/// Primary slug is first; additional entries follow in Vec order.
order: std::sync::RwLock<Vec<String>>,
}
impl ProjectRegistry {
@ -260,15 +263,20 @@ impl ProjectRegistry {
primary_slug: primary_slug.clone(),
stale_actor_timeout,
max_notifications,
// order is built from additional_entries; primary is inserted at its
// natural position (or prepended if it doesn't appear in the list).
order: std::sync::RwLock::new(Vec::new()),
};
registry.contexts.insert(primary_slug.clone(), primary_ctx);
let mut primary_seen = false;
for entry in additional_entries {
if entry.slug == primary_slug {
warn!(
slug = %entry.slug,
"additional registry entry collides with primary slug — skipping"
);
// Primary is already in contexts — just record its natural position.
primary_seen = true;
if let Ok(mut ord) = registry.order.write() {
ord.push(primary_slug.clone());
}
continue;
}
if entry.push_only {
@ -284,6 +292,9 @@ impl ProjectRegistry {
ack_required: vec![],
config_surface: None,
});
if let Ok(mut ord) = registry.order.write() {
ord.push(entry.slug.clone());
}
registry.contexts.insert(entry.slug, Arc::new(ctx));
continue;
}
@ -315,9 +326,19 @@ impl ProjectRegistry {
ack_required: vec![],
config_surface,
});
if let Ok(mut ord) = registry.order.write() {
ord.push(entry.slug.clone());
}
registry.contexts.insert(entry.slug, Arc::new(ctx));
}
// If primary never appeared in additional_entries, prepend it.
if !primary_seen {
if let Ok(mut ord) = registry.order.write() {
ord.insert(0, primary_slug);
}
}
registry
}
@ -367,6 +388,9 @@ impl ProjectRegistry {
ack_required: vec![],
config_surface,
});
if let Ok(mut ord) = self.order.write() {
ord.push(entry.slug.clone());
}
self.contexts.insert(entry.slug, Arc::new(ctx));
Ok(())
}
@ -374,15 +398,34 @@ impl ProjectRegistry {
/// Remove a project by slug.
pub fn remove_project(&self, slug: &str) {
self.contexts.remove(slug);
if let Ok(mut ord) = self.order.write() {
ord.retain(|s| s != slug);
}
}
pub fn get(&self, slug: &str) -> Option<Arc<ProjectContext>> {
self.contexts.get(slug).map(|r| Arc::clone(&*r))
}
/// Returns all project contexts in declaration order (projects.ncl order).
/// Primary project is always first.
pub fn all(&self) -> Vec<Arc<ProjectContext>> {
let mut list: Vec<_> = self.contexts.iter().map(|r| Arc::clone(&*r)).collect();
list.sort_by(|a, b| a.slug.cmp(&b.slug));
let order = self.order.read().unwrap_or_else(|e| e.into_inner());
let mut list: Vec<Arc<ProjectContext>> = order
.iter()
.filter_map(|slug| self.contexts.get(slug).map(|r| Arc::clone(&*r)))
.collect();
// Append any contexts not in order (defensive — should not happen).
let ordered_slugs: std::collections::HashSet<&str> =
order.iter().map(|s| s.as_str()).collect();
let mut extras: Vec<_> = self
.contexts
.iter()
.filter(|r| !ordered_slugs.contains(r.key().as_str()))
.map(|r| Arc::clone(&*r))
.collect();
extras.sort_by(|a, b| a.slug.cmp(&b.slug));
list.extend(extras);
list
}

View file

@ -215,6 +215,118 @@ fn extract_card_from_config(json: &serde_json::Value) -> serde_json::Value {
})
}
struct DomainCtx {
has_origin: bool,
origin_id: String,
origin_name: String,
origin_path: String,
origin_integrated: bool,
origin_slug: Option<String>,
has_provides: bool,
provides_id: String,
provides_name: String,
provides_kind: String,
impl_projects: Vec<serde_json::Value>,
}
impl Default for DomainCtx {
fn default() -> Self {
Self {
has_origin: false,
origin_id: String::new(),
origin_name: String::new(),
origin_path: String::new(),
origin_integrated: true,
origin_slug: None,
has_provides: false,
provides_id: String::new(),
provides_name: String::new(),
provides_kind: String::new(),
impl_projects: Vec::new(),
}
}
}
/// Resolve domain_origin and domain_provides context from a manifest JSON
/// value.
async fn resolve_domain_ctx(
manifest_json: &serde_json::Value,
registry: &Arc<crate::registry::ProjectRegistry>,
) -> DomainCtx {
let mut dc = DomainCtx::default();
if let Some(origin) = manifest_json.get("domain_origin") {
dc.has_origin = true;
dc.origin_id = origin
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
dc.origin_name = origin
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
dc.origin_path = origin
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
dc.origin_integrated = origin
.get("integrated")
.and_then(|v| v.as_bool())
.unwrap_or(true);
if !dc.origin_path.is_empty() {
let target = std::path::PathBuf::from(&dc.origin_path);
dc.origin_slug = registry
.all()
.into_iter()
.find(|pc| pc.root == target)
.map(|pc| pc.slug.clone());
}
}
if let Some(provides) = manifest_json.get("domain_provides") {
dc.has_provides = true;
dc.provides_id = provides
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
dc.provides_name = provides
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
dc.provides_kind = provides
.get("kind")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
for proj_ctx in registry.all() {
let mpath = proj_ctx.root.join(".ontology").join("manifest.ncl");
if !mpath.exists() {
continue;
}
let Ok((mj, _)) = proj_ctx
.cache
.export(&mpath, proj_ctx.import_path.as_deref())
.await
else {
continue;
};
let matches = mj
.get("domain_origin")
.and_then(|o| o.get("id"))
.and_then(|v| v.as_str())
== Some(dc.provides_id.as_str());
if matches {
dc.impl_projects
.push(serde_json::json!({ "slug": proj_ctx.slug }));
}
}
}
dc
}
/// Insert logo and MCP metadata into a Tera context.
/// Logos are loaded from `.ontoref/config.ncl`; MCP availability is
/// compile-time.
@ -224,6 +336,7 @@ pub(crate) async fn insert_brand_ctx(
cache: &Arc<crate::cache::NclCache>,
import_path: Option<&str>,
base_url: &str,
registry: &Arc<crate::registry::ProjectRegistry>,
) {
let (logo, logo_dark) = load_logos(root, cache, import_path, base_url).await;
ctx.insert("logo", &logo);
@ -249,6 +362,48 @@ pub(crate) async fn insert_brand_ctx(
};
ctx.insert("card_repo", &repo_url);
ctx.insert("card_docs", &docs_url);
// Domain extension availability — drives conditional nav rendering in
// base.html. personal/career: file presence (only PersonalOntology projects
// declare these). provisioning: explicit opt-in via .ontoref/config.ncl
// `domain_provisioning = true`. Using an opt-in rather than repo_kind
// avoids showing Provisioning for tool projects (e.g. ontoref itself)
// that happen to be DevWorkspace but are not managed workspaces.
ctx.insert(
"has_personal",
&root.join(".ontology").join("personal.ncl").exists(),
);
ctx.insert(
"has_career",
&root.join(".ontology").join("career.ncl").exists(),
);
let is_provisioning = load_config_json(root, cache, import_path)
.await
.and_then(|c| c.get("domain_provisioning").and_then(|v| v.as_bool()))
.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.
let manifest_path = root.join(".ontology").join("manifest.ncl");
let domain = if manifest_path.exists() {
match cache.export(&manifest_path, import_path).await {
Ok((mj, _)) => resolve_domain_ctx(&mj, registry).await,
Err(_) => DomainCtx::default(),
}
} else {
DomainCtx::default()
};
ctx.insert("has_domain_origin", &domain.has_origin);
ctx.insert("domain_origin_id", &domain.origin_id);
ctx.insert("domain_origin_name", &domain.origin_name);
ctx.insert("domain_origin_path", &domain.origin_path);
ctx.insert("domain_origin_integrated", &domain.origin_integrated);
ctx.insert("domain_origin_slug", &domain.origin_slug);
ctx.insert("has_domain_provides", &domain.has_provides);
ctx.insert("domain_provides_id", &domain.provides_id);
ctx.insert("domain_provides_name", &domain.provides_name);
ctx.insert("domain_provides_kind", &domain.provides_kind);
ctx.insert("domain_impl_projects", &domain.impl_projects);
}
/// Insert MCP metadata and daemon version into a Tera context.
@ -339,6 +494,7 @@ pub async fn dashboard(State(state): State<AppState>) -> Result<Html<String>, Ui
&state.cache,
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
)
.await;
@ -370,6 +526,7 @@ pub async fn graph(State(state): State<AppState>) -> Result<Html<String>, UiErro
&state.cache,
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
)
.await;
@ -429,6 +586,7 @@ pub async fn sessions(State(state): State<AppState>) -> Result<Html<String>, UiE
&state.cache,
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
)
.await;
@ -477,6 +635,7 @@ pub async fn notifications_page(State(state): State<AppState>) -> Result<Html<St
&state.cache,
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
)
.await;
@ -503,6 +662,7 @@ pub async fn search_page(State(state): State<AppState>) -> Result<Html<String>,
&state.cache,
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
)
.await;
render(tera, "pages/search.html", &ctx).await
@ -533,6 +693,7 @@ pub async fn search_page_mp(
&ctx_ref.cache,
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
render(tera, "pages/search.html", &ctx).await
@ -602,6 +763,7 @@ pub async fn modes(State(state): State<AppState>) -> Result<Html<String>, UiErro
&state.cache,
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
)
.await;
@ -1059,6 +1221,7 @@ pub async fn dashboard_mp(
&ctx_ref.cache,
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
@ -1177,6 +1340,7 @@ pub async fn api_catalog_page_mp(
&ctx_ref.cache,
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
@ -1289,6 +1453,7 @@ pub async fn graph_mp(
&ctx_ref.cache,
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
@ -1340,6 +1505,7 @@ pub async fn sessions_mp(
&ctx_ref.cache,
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
@ -1424,6 +1590,7 @@ pub async fn notifications_mp(
&ctx_ref.cache,
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
@ -2389,6 +2556,7 @@ pub async fn backlog_page(State(state): State<AppState>) -> Result<Html<String>,
&state.cache,
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
)
.await;
render(tera, "pages/backlog.html", &ctx).await
@ -2424,6 +2592,7 @@ pub async fn backlog_page_mp(
&proj.cache,
proj.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
render(tera, "pages/backlog.html", &ctx).await
@ -3172,6 +3341,7 @@ pub async fn modes_mp(
&ctx_ref.cache,
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
@ -3262,6 +3432,7 @@ pub async fn compose_page_mp(
&ctx_ref.cache,
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
@ -3470,6 +3641,7 @@ pub async fn actions_page(State(state): State<AppState>) -> Result<Html<String>,
&state.cache,
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
)
.await;
render(tera, "pages/actions.html", &ctx).await
@ -3500,6 +3672,7 @@ pub async fn actions_page_mp(
&ctx_ref.cache,
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
render(tera, "pages/actions.html", &ctx).await
@ -3525,6 +3698,7 @@ pub async fn qa_page(State(state): State<AppState>) -> Result<Html<String>, UiEr
&state.cache,
state.nickel_import_path.as_deref(),
"/ui",
&state.registry,
)
.await;
render(tera, "pages/qa.html", &ctx).await
@ -3555,6 +3729,7 @@ pub async fn qa_page_mp(
&ctx_ref.cache,
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
render(tera, "pages/qa.html", &ctx).await
@ -4048,6 +4223,7 @@ pub async fn config_page_mp(
&ctx_ref.cache,
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
@ -4140,8 +4316,233 @@ pub async fn adrs_page_mp(
&ctx_ref.cache,
ctx_ref.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
render(tera, "pages/adrs.html", &ctx).await
}
// ── Domain extension pages
// ────────────────────────────────────────────────────
/// Personal ontology page — contents pipeline + opportunities.
/// Loads .ontology/personal.ncl and reflection/backlog.ncl (CFP pipeline).
pub async fn personal_page_mp(
State(state): State<AppState>,
Path(slug): Path<String>,
auth: AuthUser,
) -> Result<Html<String>, UiError> {
let tera = tera_ref(&state)?;
let ctx_ref = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
let base_url = format!("/ui/{slug}");
let import_path = ctx_ref.import_path.clone();
let cache = ctx_ref.cache.clone();
let root = ctx_ref.root.clone();
// personal.ncl → contents + opportunities
let personal_path = root.join(".ontology").join("personal.ncl");
let (contents_json, opportunities_json) = if personal_path.exists() {
match cache.export(&personal_path, import_path.as_deref()).await {
Ok((v, _)) => {
let contents = serde_json::to_string(
v.get("contents")
.unwrap_or(&serde_json::Value::Array(vec![])),
)
.unwrap_or_else(|_| "[]".to_string());
let opps = serde_json::to_string(
v.get("opportunities")
.unwrap_or(&serde_json::Value::Array(vec![])),
)
.unwrap_or_else(|_| "[]".to_string());
(contents, opps)
}
Err(e) => {
tracing::warn!(error = %e, "personal_page: personal.ncl export failed");
("[]".to_string(), "[]".to_string())
}
}
} else {
("[]".to_string(), "[]".to_string())
};
// reflection/backlog.ncl → cfp pipeline
let backlog_path = root.join("reflection").join("backlog.ncl");
let cfp_json = if backlog_path.exists() {
match cache.export(&backlog_path, import_path.as_deref()).await {
Ok((v, _)) => {
serde_json::to_string(v.get("cfp").unwrap_or(&serde_json::Value::Array(vec![])))
.unwrap_or_else(|_| "[]".to_string())
}
Err(e) => {
tracing::warn!(error = %e, "personal_page: backlog.ncl export failed");
"[]".to_string()
}
}
} else {
"[]".to_string()
};
let mut ctx = Context::new();
ctx.insert("slug", &slug);
ctx.insert("base_url", &base_url);
ctx.insert("current_role", &auth_role_str(&auth));
ctx.insert("contents_json", &contents_json);
ctx.insert("opportunities_json", &opportunities_json);
ctx.insert("cfp_json", &cfp_json);
insert_brand_ctx(
&mut ctx,
&root,
&cache,
import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
render(tera, "pages/personal.html", &ctx).await
}
/// Career page — skills, work experiences, talks, positioning.
/// Loads .ontology/career.ncl.
pub async fn career_page_mp(
State(state): State<AppState>,
Path(slug): Path<String>,
auth: AuthUser,
) -> Result<Html<String>, UiError> {
let tera = tera_ref(&state)?;
let ctx_ref = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
let base_url = format!("/ui/{slug}");
let import_path = ctx_ref.import_path.clone();
let cache = ctx_ref.cache.clone();
let root = ctx_ref.root.clone();
let career_path = root.join(".ontology").join("career.ncl");
let (skills_json, experiences_json, talks_json, positioning_json, publications_json) =
if career_path.exists() {
match cache.export(&career_path, import_path.as_deref()).await {
Ok((v, _)) => {
let empty = serde_json::Value::Array(vec![]);
let skills = serde_json::to_string(v.get("skills").unwrap_or(&empty))
.unwrap_or_else(|_| "[]".to_string());
let experiences = serde_json::to_string(v.get("experiences").unwrap_or(&empty))
.unwrap_or_else(|_| "[]".to_string());
let talks = serde_json::to_string(v.get("talks").unwrap_or(&empty))
.unwrap_or_else(|_| "[]".to_string());
let positioning = serde_json::to_string(v.get("positioning").unwrap_or(&empty))
.unwrap_or_else(|_| "[]".to_string());
let publications =
serde_json::to_string(v.get("publications").unwrap_or(&empty))
.unwrap_or_else(|_| "[]".to_string());
(skills, experiences, talks, positioning, publications)
}
Err(e) => {
tracing::warn!(error = %e, "career_page: career.ncl export failed");
(
"[]".to_string(),
"[]".to_string(),
"[]".to_string(),
"[]".to_string(),
"[]".to_string(),
)
}
}
} else {
(
"[]".to_string(),
"[]".to_string(),
"[]".to_string(),
"[]".to_string(),
"[]".to_string(),
)
};
let mut ctx = Context::new();
ctx.insert("slug", &slug);
ctx.insert("base_url", &base_url);
ctx.insert("current_role", &auth_role_str(&auth));
ctx.insert("skills_json", &skills_json);
ctx.insert("experiences_json", &experiences_json);
ctx.insert("talks_json", &talks_json);
ctx.insert("positioning_json", &positioning_json);
ctx.insert("publications_json", &publications_json);
insert_brand_ctx(
&mut ctx,
&root,
&cache,
import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
render(tera, "pages/career.html", &ctx).await
}
/// Provisioning page — connections and gates.
/// Loads .ontology/connections.ncl and .ontology/gate.ncl.
pub async fn provisioning_page_mp(
State(state): State<AppState>,
Path(slug): Path<String>,
auth: AuthUser,
) -> Result<Html<String>, UiError> {
let tera = tera_ref(&state)?;
let ctx_ref = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
let base_url = format!("/ui/{slug}");
let import_path = ctx_ref.import_path.clone();
let cache = ctx_ref.cache.clone();
let root = ctx_ref.root.clone();
// connections.ncl → upstream / downstream / peers
let connections_path = root.join(".ontology").join("connections.ncl");
let connections_json = if connections_path.exists() {
match cache
.export(&connections_path, import_path.as_deref())
.await
{
Ok((v, _)) => serde_json::to_string(&v).unwrap_or_else(|_| "{}".to_string()),
Err(e) => {
tracing::warn!(error = %e, "provisioning_page: connections.ncl export failed");
"{}".to_string()
}
}
} else {
"{}".to_string()
};
// gate.ncl → membranas array
let gate_path = root.join(".ontology").join("gate.ncl");
let gates_json = if gate_path.exists() {
match cache.export(&gate_path, import_path.as_deref()).await {
Ok((v, _)) => {
let empty = serde_json::Value::Array(vec![]);
let membranas = v.get("membranas").unwrap_or(&empty);
serde_json::to_string(membranas).unwrap_or_else(|_| "[]".to_string())
}
Err(e) => {
tracing::warn!(error = %e, "provisioning_page: gate.ncl export failed");
"[]".to_string()
}
}
} else {
"[]".to_string()
};
let mut ctx = Context::new();
ctx.insert("slug", &slug);
ctx.insert("base_url", &base_url);
ctx.insert("current_role", &auth_role_str(&auth));
ctx.insert("connections_json", &connections_json);
ctx.insert("gates_json", &gates_json);
insert_brand_ctx(
&mut ctx,
&root,
&cache,
import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
render(tera, "pages/provisioning.html", &ctx).await
}

View file

@ -30,6 +30,7 @@ pub async fn login_page(
&proj.cache,
proj.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
}
@ -84,6 +85,7 @@ pub async fn login_submit(
&ctx.cache,
ctx.import_path.as_deref(),
&base_url,
&state.registry,
)
.await;
match render(tera, "pages/login.html", &tctx).await {

View file

@ -101,6 +101,9 @@ fn multi_router(state: AppState) -> axum::Router {
.route("/{slug}/api", get(handlers::api_catalog_page_mp))
.route("/{slug}/config", get(handlers::config_page_mp))
.route("/{slug}/adrs", get(handlers::adrs_page_mp))
.route("/{slug}/personal", get(handlers::personal_page_mp))
.route("/{slug}/career", get(handlers::career_page_mp))
.route("/{slug}/provisioning", get(handlers::provisioning_page_mp))
.route(
"/{slug}/migrations/pending",
get(handlers::pending_migrations_fragment_mp),

View file

@ -439,8 +439,78 @@
</ul>
</details>
</li>
{% if has_personal or has_career or has_provisioning %}
<!-- Domain section (mobile) -->
<li>
<details>
<summary class="gap-1.5 font-medium text-base-content/70">
<svg class="nav-icon 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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
<span class="nav-label">Domain</span>
</summary>
<ul>
{% if has_personal %}
<li><a href="{{ base_url }}/personal" class="gap-1.5">Personal</a></li>
{% endif %}
{% if has_career %}
<li><a href="{{ base_url }}/career" class="gap-1.5">Career</a></li>
{% endif %}
{% if has_provisioning %}
<li><a href="{{ base_url }}/provisioning" class="gap-1.5">Provisioning</a></li>
{% endif %}
</ul>
</details>
</li>
{% endif %}
<li class="divider my-0.5"></li>
{% endif %} {% if not slug or current_role == "admin" %}
{% endif %}
{% if has_domain_origin or has_domain_provides %}
<li class="px-2 py-1 text-xs text-base-content/50 font-semibold uppercase tracking-wide">Domain Ontology</li>
{% if has_domain_origin %}
<li>
{% if domain_origin_slug %}
<a href="/ui/{{ domain_origin_slug }}/" class="gap-1.5">
<span class="text-warning"></span> {{ domain_origin_name }}
<span class="text-base-content/40 text-xs">(impl)</span>
</a>
{% else %}
<span class="gap-1.5 text-base-content/50">
<span class="text-warning"></span> {{ domain_origin_name }}
</span>
{% endif %}
</li>
{% endif %}
{% if has_domain_provides %}
{% for impl in domain_impl_projects %}
<li>
<a href="/ui/{{ impl.slug }}/" class="gap-1.5">
<span class="text-success"></span> {{ impl.slug }}
<span class="text-base-content/40 text-xs">(impl)</span>
</a>
</li>
{% endfor %}
{% endif %}
<li class="divider my-0.5"></li>
{% endif %}
{% if mcp_active %}
<li>
<span class="gap-1.5 text-base-content/60 cursor-default">
<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="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2v-4M9 21H5a2 2 0 01-2-2v-4m0 0h18"/>
</svg>
MCP
<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 slug and current_role == "viewer" %}
<li class="px-2 py-0.5 text-xs text-base-content/40 font-mono">{{ slug }} · viewer</li>
{% endif %}
{% if not slug or current_role == "admin" %}
<li><a href="/ui/manage">Manage Projects</a></li>
{% endif %} {% if slug %}
<li><a href="/ui/{{ slug }}/logout">Logout</a></li>
@ -964,6 +1034,80 @@
</li>
</ul>
</div>
<!-- Domain: Personal · Career · Provisioning — only shown when project has the relevant files -->
{% if has_personal or has_career or has_provisioning %}
<div class="dropdown dropdown-hover">
<div
tabindex="0"
role="button"
class="btn btn-ghost btn-sm gap-1.5 {% block nav_group_domain %}{% endblock nav_group_domain %}"
>
<svg
class="nav-icon 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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<span class="nav-label">Domain</span>
<svg
class="w-3 h-3 opacity-60"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
<ul
tabindex="0"
class="dropdown-content menu bg-base-200 shadow-lg rounded-box z-50 w-44 p-2 mt-1"
>
{% if has_personal %}
<li>
<a href="{{ base_url }}/personal" class="gap-1.5 {% block nav_personal %}{% endblock nav_personal %}">
<svg class="nav-icon 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="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<span class="nav-label">Personal</span>
</a>
</li>
{% endif %}
{% if has_career %}
<li>
<a href="{{ base_url }}/career" class="gap-1.5 {% block nav_career %}{% endblock nav_career %}">
<svg class="nav-icon 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="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span class="nav-label">Career</span>
</a>
</li>
{% endif %}
{% if has_provisioning %}
<li>
<a href="{{ base_url }}/provisioning" class="gap-1.5 {% block nav_provisioning %}{% endblock nav_provisioning %}">
<svg class="nav-icon 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="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="nav-label">Provisioning</span>
</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
</div>
{% endif %}
</div>
@ -979,6 +1123,55 @@
>viewer</span
>
{% endif %}
{% if has_domain_origin or has_domain_provides %}
<div class="dropdown dropdown-end hidden sm:block">
<button tabindex="0" class="btn btn-xs btn-ghost px-1.5"
title="Domain ontology">
<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 6h16M4 10h16M4 14h16M4 18h16"/>
</svg>
</button>
<div tabindex="0"
class="dropdown-content z-[50] mt-1 w-56 rounded-lg shadow-lg bg-base-200 border border-base-content/10 p-3 space-y-2 text-xs">
{% if has_domain_origin %}
<div>
<p class="text-base-content/40 uppercase tracking-wide font-semibold mb-1">implements</p>
{% if domain_origin_slug %}
<a href="/ui/{{ domain_origin_slug }}/"
class="flex items-center gap-1.5 font-mono text-primary hover:underline truncate"
title="{{ domain_origin_name }} · {{ domain_origin_id }}">
<span class="text-warning flex-shrink-0"></span>
<span class="truncate">{{ domain_origin_name }}</span>
</a>
{% else %}
<span class="font-mono text-base-content/60 flex items-center gap-1.5">
<span class="text-warning flex-shrink-0"></span>
<span class="truncate">{{ domain_origin_name }}</span>
</span>
{% endif %}
</div>
{% endif %}
{% if has_domain_provides %}
<div class="{% if has_domain_origin %}border-t border-base-content/10 pt-2{% endif %}">
<p class="text-base-content/40 uppercase tracking-wide font-semibold mb-1">
provides · <span class="normal-case font-normal">{{ domain_provides_kind }}</span>
</p>
<p class="font-mono text-base-content/70 mb-1 truncate">{{ domain_provides_name }}</p>
{% for impl in domain_impl_projects %}
<a href="/ui/{{ impl.slug }}/"
class="flex items-center gap-1.5 font-mono text-success hover:underline truncate">
<span class="flex-shrink-0"></span>{{ impl.slug }}
</a>
{% endfor %}
{% if domain_impl_projects | length == 0 %}
<span class="text-base-content/30 italic">no implementations registered</span>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- User icon → logout -->
<a
href="/ui/{{ slug }}/logout"

View file

@ -0,0 +1,259 @@
{% extends "base.html" %}
{% import "macros/ui.html" as m %}
{% block title %}Career — {{ slug }} — Ontoref{% endblock title %}
{% block nav_career %}active{% endblock nav_career %}
{% block nav_group_domain %}active{% endblock nav_group_domain %}
{% block content %}
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold">Career</h1>
<p class="text-base-content/50 text-sm mt-1">Skills · Experience · Talks · Positioning</p>
</div>
</div>
<!-- Tabs -->
<div role="tablist" class="tabs tabs-bordered mb-4">
<a role="tab" class="tab tab-active" onclick="switchTab('skills', this)">Skills</a>
<a role="tab" class="tab" onclick="switchTab('experience', this)">Experience</a>
<a role="tab" class="tab" onclick="switchTab('talks', this)">Talks</a>
<a role="tab" class="tab" onclick="switchTab('positioning', this)">Positioning</a>
</div>
<!-- Skills tab -->
<div id="tab-skills">
<div class="flex flex-wrap gap-2 mb-3">
<input id="skill-filter" type="text" placeholder="Filter by name…"
class="input input-sm input-bordered flex-1 min-w-48 font-mono" oninput="renderSkills()">
<select id="skill-tier" class="select select-sm select-bordered" onchange="renderSkills()">
<option value="">All tiers</option>
<option value="Expert">Expert</option>
<option value="Advanced">Advanced</option>
<option value="Intermediate">Intermediate</option>
<option value="Foundational">Foundational</option>
</select>
</div>
<div class="overflow-x-auto">
<table class="table table-sm w-full bg-base-200 rounded-lg">
<thead>
<tr class="text-base-content/50 text-xs uppercase tracking-wider">
<th>Name</th><th>Tier</th><th>Proficiency</th><th>Years</th>
</tr>
</thead>
<tbody id="skills-body"></tbody>
</table>
</div>
</div>
<!-- Experience tab -->
<div id="tab-experience" class="hidden space-y-4">
<div id="experience-cards"></div>
</div>
<!-- Talks tab -->
<div id="tab-talks" class="hidden">
<div class="flex flex-wrap gap-2 mb-3">
<input id="talk-filter" type="text" placeholder="Filter by title, event…"
class="input input-sm input-bordered flex-1 min-w-48 font-mono" oninput="renderTalks()">
<select id="talk-status" class="select select-sm select-bordered" onchange="renderTalks()">
<option value="">All statuses</option>
<option value="Idea">Idea</option>
<option value="Proposed">Proposed</option>
<option value="Accepted">Accepted</option>
<option value="Delivered">Delivered</option>
<option value="Archived">Archived</option>
</select>
</div>
<div class="overflow-x-auto">
<table class="table table-sm w-full bg-base-200 rounded-lg">
<thead>
<tr class="text-base-content/50 text-xs uppercase tracking-wider">
<th>Title</th><th>Event</th><th>Date</th><th>Status</th><th>Links</th>
</tr>
</thead>
<tbody id="talks-body"></tbody>
</table>
</div>
</div>
<!-- Positioning tab -->
<div id="tab-positioning" class="hidden grid grid-cols-1 lg:grid-cols-2 gap-4">
<div id="positioning-cards"></div>
</div>
<!-- Detail modal -->
<dialog id="detail-modal" class="modal">
<div class="modal-box w-full max-w-2xl">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-3 top-3"></button>
</form>
<div id="modal-content" class="text-sm space-y-3"></div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<script>
const SKILLS = {{ skills_json | safe }};
const EXPERIENCES = {{ experiences_json | safe }};
const TALKS = {{ talks_json | safe }};
const POSITIONING = {{ positioning_json | safe }};
const TIER_BADGE = {
Expert: 'badge-primary', Advanced: 'badge-success',
Intermediate: 'badge-warning', Foundational: 'badge-ghost',
};
const TALK_BADGE = {
Idea: 'badge-ghost', Proposed: 'badge-info', Accepted: 'badge-warning',
Delivered: 'badge-success', Archived: 'badge-ghost',
};
function tierBadge(t) {
return `<span class="badge badge-xs font-mono ${TIER_BADGE[t] ?? 'badge-ghost'}">${t}</span>`;
}
function talkBadge(s) {
return `<span class="badge badge-xs font-mono ${TALK_BADGE[s] ?? 'badge-ghost'}">${s}</span>`;
}
function linkChip(lk) {
return `<a href="${lk.url}" target="_blank" class="badge badge-xs badge-outline font-mono">${lk.kind}${lk.label ? ': ' + lk.label : ''}</a>`;
}
function profBar(p) {
const pct = Math.round(p * 100);
return `<div class="flex items-center gap-2">
<div class="w-20 bg-base-300 rounded-full h-1.5">
<div class="bg-primary rounded-full h-1.5" style="width:${pct}%"></div>
</div>
<span class="font-mono text-xs text-base-content/50">${pct}%</span>
</div>`;
}
// ── Skills ────────────────────────────────────────────────────────────────────
let visibleSkills = [...SKILLS].sort((a, b) => b.proficiency - a.proficiency);
function renderSkills() {
const text = document.getElementById('skill-filter').value.toLowerCase();
const tier = document.getElementById('skill-tier').value;
const base = [...SKILLS].sort((a, b) => b.proficiency - a.proficiency);
visibleSkills = base.filter(s => {
const tm = !text || s.name.toLowerCase().includes(text) || s.id.toLowerCase().includes(text);
const tm2 = !tier || s.tier === tier;
return tm && tm2;
});
const tbody = document.getElementById('skills-body');
tbody.innerHTML = visibleSkills.length === 0
? '<tr><td colspan="4" class="text-center text-base-content/30 py-6">No skills</td></tr>'
: visibleSkills.map(s => `
<tr>
<td class="text-sm font-medium">${s.name}</td>
<td>${tierBadge(s.tier)}</td>
<td>${profBar(s.proficiency)}</td>
<td class="font-mono text-xs text-base-content/50">${s.years > 0 ? s.years + 'y' : '—'}</td>
</tr>`).join('');
}
// ── Experience ────────────────────────────────────────────────────────────────
function renderExperience() {
const container = document.getElementById('experience-cards');
container.innerHTML = EXPERIENCES.length === 0
? '<p class="text-base-content/30 text-sm py-6">No experiences</p>'
: EXPERIENCES.map(e => `
<div class="bg-base-200 rounded-lg p-4 mb-4 space-y-2">
<div class="flex items-start justify-between gap-2 flex-wrap">
<div>
<p class="font-bold">${e.position}</p>
<p class="text-sm text-primary font-medium">
${e.company_url ? `<a href="${e.company_url}" target="_blank" class="link link-hover">${e.company}</a>` : e.company}
</p>
</div>
<div class="text-right text-xs text-base-content/50 font-mono flex-shrink-0">
<p>${e.date_start} — ${e.date_end}</p>
${e.location ? `<p>${e.location}</p>` : ''}
</div>
</div>
${e.description ? `<p class="text-sm text-base-content/70">${e.description}</p>` : ''}
${(e.achievements||[]).length ? `
<ul class="text-xs text-base-content/60 space-y-1 pl-3">
${e.achievements.map(a => `<li class="list-disc">${a}</li>`).join('')}
</ul>` : ''}
${(e.tools||[]).length ? `
<div class="flex flex-wrap gap-1">
${e.tools.map(t => `<span class="badge badge-xs badge-outline font-mono">${t}</span>`).join('')}
</div>` : ''}
</div>`).join('');
}
// ── Talks ─────────────────────────────────────────────────────────────────────
let visibleTalks = TALKS;
function renderTalks() {
const text = document.getElementById('talk-filter').value.toLowerCase();
const status = document.getElementById('talk-status').value;
visibleTalks = TALKS.filter(t => {
const tm = !text || (t.title||'').toLowerCase().includes(text) || (t.event||'').toLowerCase().includes(text);
const sm = !status || t.status === status;
return tm && sm;
});
const tbody = document.getElementById('talks-body');
tbody.innerHTML = visibleTalks.length === 0
? '<tr><td colspan="5" class="text-center text-base-content/30 py-6">No talks</td></tr>'
: visibleTalks.map((t, i) => `
<tr class="hover cursor-pointer" onclick="showTalkDetail(${i})">
<td class="text-sm font-medium">${t.title}</td>
<td class="text-sm text-base-content/60">${t.event || '—'}</td>
<td class="font-mono text-xs text-base-content/50">${t.date || '—'}</td>
<td>${talkBadge(t.status)}</td>
<td class="text-xs">${(t.links||[]).length ? `<span class="badge badge-xs badge-outline">${(t.links||[]).length}</span>` : '—'}</td>
</tr>`).join('');
}
function showTalkDetail(i) {
const t = visibleTalks[i];
if (!t) return;
const links = (t.links||[]).map(linkChip).join(' ');
document.getElementById('modal-content').innerHTML = `
<div class="flex items-baseline gap-2 flex-wrap">
${talkBadge(t.status)}
<span class="font-mono text-xs text-base-content/40">${t.date || ''} ${t.location ? '· ' + t.location : ''}</span>
</div>
<h2 class="text-lg font-bold">${t.title}</h2>
<p class="text-sm text-base-content/60 font-medium">${t.event || ''}</p>
${t.description ? `<p class="text-base-content/70 text-sm leading-relaxed">${t.description}</p>` : ''}
${links ? `<div class="flex flex-wrap gap-1">${links}</div>` : ''}
${(t.linked_nodes||[]).length ? `<p class="text-xs text-base-content/40 font-mono">nodes: ${t.linked_nodes.join(', ')}</p>` : ''}
`;
document.getElementById('detail-modal').showModal();
}
// ── Positioning ───────────────────────────────────────────────────────────────
function renderPositioning() {
const container = document.getElementById('positioning-cards');
container.innerHTML = POSITIONING.length === 0
? '<p class="text-base-content/30 text-sm py-6">No positioning strategies</p>'
: POSITIONING.map(p => `
<div class="bg-base-200 rounded-lg p-4 space-y-2">
<p class="font-bold text-sm">${p.name}</p>
<p class="text-xs text-base-content/50">Target: ${p.target || '—'}</p>
<p class="text-sm text-base-content/70 leading-relaxed">${p.core_message}</p>
${p.note ? `<p class="text-xs text-base-content/40 italic">${p.note}</p>` : ''}
</div>`).join('');
}
// ── Tabs ──────────────────────────────────────────────────────────────────────
function switchTab(name, el) {
['skills','experience','talks','positioning'].forEach(t => {
document.getElementById('tab-' + t).classList.toggle('hidden', t !== name);
});
document.querySelectorAll('[role="tab"]').forEach(t => t.classList.remove('tab-active'));
el.classList.add('tab-active');
}
renderSkills();
renderExperience();
renderTalks();
renderPositioning();
</script>
{% endblock content %}

View file

@ -0,0 +1,263 @@
{% extends "base.html" %}
{% import "macros/ui.html" as m %}
{% block title %}Personal — {{ slug }} — Ontoref{% endblock title %}
{% block nav_personal %}active{% endblock nav_personal %}
{% block nav_group_domain %}active{% endblock nav_group_domain %}
{% block content %}
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold">Personal Ontology</h1>
<p class="text-base-content/50 text-sm mt-1">Content pipeline · Opportunities · CFP pipeline</p>
</div>
</div>
<!-- Tabs -->
<div role="tablist" class="tabs tabs-bordered mb-4">
<a role="tab" class="tab tab-active" onclick="switchTab('content', this)">Content</a>
<a role="tab" class="tab" onclick="switchTab('opportunities', this)">Opportunities</a>
<a role="tab" class="tab" onclick="switchTab('cfp', this)">CFP Pipeline</a>
</div>
<!-- Content tab -->
<div id="tab-content">
<div class="flex flex-wrap gap-2 mb-3">
<input id="content-filter" type="text" placeholder="Filter by title, kind…"
class="input input-sm input-bordered flex-1 min-w-48 font-mono" oninput="renderContent()">
<select id="content-status" class="select select-sm select-bordered" onchange="renderContent()">
<option value="">All statuses</option>
<option value="Idea">Idea</option>
<option value="Brief">Brief</option>
<option value="Draft">Draft</option>
<option value="Review">Review</option>
<option value="Published">Published</option>
<option value="Archived">Archived</option>
</select>
</div>
<div class="overflow-x-auto">
<table class="table table-sm w-full bg-base-200 rounded-lg">
<thead>
<tr class="text-base-content/50 text-xs uppercase tracking-wider">
<th>ID</th><th>Title</th><th>Kind</th><th>Status</th><th>Audience</th>
</tr>
</thead>
<tbody id="content-body"></tbody>
</table>
</div>
</div>
<!-- Opportunities tab -->
<div id="tab-opportunities" class="hidden">
<div class="flex flex-wrap gap-2 mb-3">
<input id="opp-filter" type="text" placeholder="Filter by name, kind…"
class="input input-sm input-bordered flex-1 min-w-48 font-mono" oninput="renderOpportunities()">
<select id="opp-status" class="select select-sm select-bordered" onchange="renderOpportunities()">
<option value="">All statuses</option>
<option value="Watching">Watching</option>
<option value="Evaluating">Evaluating</option>
<option value="Active">Active</option>
<option value="Submitted">Submitted</option>
<option value="Closed">Closed</option>
</select>
</div>
<div class="overflow-x-auto">
<table class="table table-sm w-full bg-base-200 rounded-lg">
<thead>
<tr class="text-base-content/50 text-xs uppercase tracking-wider">
<th>ID</th><th>Name</th><th>Kind</th><th>Status</th><th>Deadline</th><th>Links</th>
</tr>
</thead>
<tbody id="opp-body"></tbody>
</table>
</div>
</div>
<!-- CFP tab -->
<div id="tab-cfp" class="hidden">
<div class="flex flex-wrap gap-2 mb-3">
<input id="cfp-filter" type="text" placeholder="Filter by name, note…"
class="input input-sm input-bordered flex-1 min-w-48 font-mono" oninput="renderCfp()">
<select id="cfp-stage" class="select select-sm select-bordered" onchange="renderCfp()">
<option value="">All stages</option>
<option value="Watching">Watching</option>
<option value="Evaluating">Evaluating</option>
<option value="Drafting">Drafting</option>
<option value="Submitted">Submitted</option>
<option value="Accepted">Accepted</option>
<option value="Declined">Declined</option>
<option value="Delivered">Delivered</option>
</select>
</div>
<div id="cfp-cards" class="grid grid-cols-1 lg:grid-cols-2 gap-4"></div>
</div>
<!-- Detail modal -->
<dialog id="detail-modal" class="modal">
<div class="modal-box w-full max-w-2xl">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-3 top-3"></button>
</form>
<div id="modal-content" class="text-sm space-y-3"></div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<script>
const CONTENTS = {{ contents_json | safe }};
const OPPORTUNITIES = {{ opportunities_json | safe }};
const CFP = {{ cfp_json | safe }};
const STATUS_BADGE = {
Idea: 'badge-ghost', Brief: 'badge-info', Draft: 'badge-warning',
Review: 'badge-warning', Published: 'badge-success', Rejected: 'badge-error',
Archived: 'badge-ghost',
Watching: 'badge-ghost', Evaluating: 'badge-info', Active: 'badge-warning',
Submitted: 'badge-info', Closed: 'badge-ghost',
Drafting: 'badge-warning', Accepted: 'badge-success', Declined: 'badge-error',
Delivered: 'badge-success',
};
function badge(s) {
const cls = STATUS_BADGE[s] ?? 'badge-ghost';
return `<span class="badge badge-xs font-mono ${cls}">${s}</span>`;
}
function linkChip(lk) {
return `<a href="${lk.url}" target="_blank" class="badge badge-xs badge-outline font-mono">${lk.kind}${lk.label ? ': ' + lk.label : ''}</a>`;
}
// ── Content ──────────────────────────────────────────────────────────────────
let visibleContents = CONTENTS;
function renderContent() {
const text = document.getElementById('content-filter').value.toLowerCase();
const status = document.getElementById('content-status').value;
visibleContents = CONTENTS.filter(c => {
const tm = !text || c.id.toLowerCase().includes(text) || (c.title||'').toLowerCase().includes(text) || (c.kind||'').toLowerCase().includes(text);
const sm = !status || c.status === status;
return tm && sm;
});
const tbody = document.getElementById('content-body');
tbody.innerHTML = visibleContents.length === 0
? '<tr><td colspan="5" class="text-center text-base-content/30 py-6">No content items</td></tr>'
: visibleContents.map((c, i) => `
<tr class="hover cursor-pointer" onclick="showContentDetail(${i})">
<td class="font-mono text-xs text-primary">${c.id}</td>
<td class="text-sm">${c.title || '<span class="text-base-content/30"></span>'}</td>
<td class="font-mono text-xs">${c.kind}</td>
<td>${badge(c.status)}</td>
<td class="text-xs text-base-content/60">${c.audience || ''}</td>
</tr>`).join('');
}
function showContentDetail(i) {
const c = visibleContents[i];
if (!c) return;
const links = (c.links||[]).map(linkChip).join(' ');
document.getElementById('modal-content').innerHTML = `
<div class="flex items-baseline gap-2 flex-wrap">
<span class="font-mono font-bold text-primary">${c.id}</span>
${badge(c.status)} <span class="font-mono text-xs text-base-content/40">${c.kind}</span>
</div>
<h2 class="text-lg font-bold">${c.title || '—'}</h2>
<div class="flex gap-2 flex-wrap text-xs text-base-content/50">
${c.audience ? `<span>Audience: ${c.audience}</span>` : ''}
${(c.platforms||[]).length ? `<span>Platforms: ${c.platforms.join(', ')}</span>` : ''}
</div>
${links ? `<div class="flex flex-wrap gap-1">${links}</div>` : ''}
${c.note ? `<p class="text-base-content/70 text-sm leading-relaxed">${c.note}</p>` : ''}
${(c.linked_nodes||[]).length ? `<p class="text-xs text-base-content/40 font-mono">nodes: ${c.linked_nodes.join(', ')}</p>` : ''}
`;
document.getElementById('detail-modal').showModal();
}
// ── Opportunities ─────────────────────────────────────────────────────────────
let visibleOpps = OPPORTUNITIES;
function renderOpportunities() {
const text = document.getElementById('opp-filter').value.toLowerCase();
const status = document.getElementById('opp-status').value;
visibleOpps = OPPORTUNITIES.filter(o => {
const tm = !text || o.id.toLowerCase().includes(text) || (o.name||'').toLowerCase().includes(text) || (o.kind||'').toLowerCase().includes(text);
const sm = !status || o.status === status;
return tm && sm;
});
const tbody = document.getElementById('opp-body');
tbody.innerHTML = visibleOpps.length === 0
? '<tr><td colspan="6" class="text-center text-base-content/30 py-6">No opportunities</td></tr>'
: visibleOpps.map((o, i) => `
<tr class="hover cursor-pointer" onclick="showOppDetail(${i})">
<td class="font-mono text-xs text-primary">${o.id}</td>
<td class="text-sm">${o.name}</td>
<td class="font-mono text-xs">${o.kind}</td>
<td>${badge(o.status)}</td>
<td class="font-mono text-xs text-base-content/50">${o.deadline || '—'}</td>
<td class="text-xs">${(o.links||[]).length ? `<span class="badge badge-xs badge-outline">${(o.links||[]).length}</span>` : '—'}</td>
</tr>`).join('');
}
function showOppDetail(i) {
const o = visibleOpps[i];
if (!o) return;
const links = (o.links||[]).map(linkChip).join(' ');
document.getElementById('modal-content').innerHTML = `
<div class="flex items-baseline gap-2 flex-wrap">
<span class="font-mono font-bold text-primary">${o.id}</span>
${badge(o.status)} <span class="font-mono text-xs text-base-content/40">${o.kind}</span>
</div>
<h2 class="text-lg font-bold">${o.name}</h2>
${o.deadline ? `<p class="text-xs text-base-content/50 font-mono">Deadline: ${o.deadline}</p>` : ''}
${links ? `<div class="flex flex-wrap gap-1">${links}</div>` : ''}
${(o.fit_signals||[]).length ? `<p class="text-xs text-base-content/50">Fit: ${o.fit_signals.join(' · ')}</p>` : ''}
${o.note ? `<p class="text-base-content/70 text-sm leading-relaxed">${o.note}</p>` : ''}
${(o.linked_nodes||[]).length ? `<p class="text-xs text-base-content/40 font-mono">nodes: ${o.linked_nodes.join(', ')}</p>` : ''}
`;
document.getElementById('detail-modal').showModal();
}
// ── CFP ───────────────────────────────────────────────────────────────────────
function renderCfp() {
const text = document.getElementById('cfp-filter').value.toLowerCase();
const stage = document.getElementById('cfp-stage').value;
const items = CFP.filter(c => {
const tm = !text || c.id.toLowerCase().includes(text) || (c.name||'').toLowerCase().includes(text) || (c.note||'').toLowerCase().includes(text);
const sm = !stage || c.stage === stage;
return tm && sm;
});
const container = document.getElementById('cfp-cards');
container.innerHTML = items.length === 0
? '<p class="text-base-content/30 text-sm py-6 col-span-2">No CFP items</p>'
: items.map(c => {
const links = (c.links||[]).map(linkChip).join(' ');
return `
<div class="bg-base-200 rounded-lg p-4 space-y-2">
<div class="flex items-start justify-between gap-2">
<div>
<p class="font-semibold text-sm">${c.name}</p>
<p class="font-mono text-xs text-base-content/40">${c.id}</p>
</div>
${badge(c.stage)}
</div>
${c.deadline ? `<p class="text-xs text-base-content/50 font-mono">Deadline: ${c.deadline}</p>` : ''}
${links ? `<div class="flex flex-wrap gap-1">${links}</div>` : ''}
${c.next_action ? `<div class="border-t border-base-content/10 pt-2"><p class="text-xs text-base-content/50 uppercase tracking-wider mb-1">Next action</p><p class="text-xs text-base-content/70">${c.next_action}</p></div>` : ''}
${c.note ? `<p class="text-xs text-base-content/50 italic">${c.note}</p>` : ''}
</div>`;
}).join('');
}
// ── Tabs ──────────────────────────────────────────────────────────────────────
function switchTab(name, el) {
['content','opportunities','cfp'].forEach(t => {
document.getElementById('tab-' + t).classList.toggle('hidden', t !== name);
});
document.querySelectorAll('[role="tab"]').forEach(t => t.classList.remove('tab-active'));
el.classList.add('tab-active');
}
renderContent();
renderOpportunities();
renderCfp();
</script>
{% endblock content %}

View file

@ -29,25 +29,40 @@
<span class="ml-auto text-xs text-base-content/40">last project</span>
</a>
<div class="mb-6 flex items-center justify-between">
<div class="mb-6 flex flex-wrap items-center justify-between gap-3">
<div>
<h1 class="text-2xl font-bold">Projects</h1>
<p class="text-base-content/50 text-sm mt-1">{{ projects | length }} project{% if projects | length != 1 %}s{% endif %} registered</p>
<p class="text-base-content/50 text-sm mt-1" id="project-count-label">
{{ projects | length }} project{% if projects | length != 1 %}s{% endif %} registered
</p>
</div>
<div class="flex items-center gap-2">
<div class="relative">
<input id="project-filter" type="text" placeholder="Filter projects…"
autocomplete="off" spellcheck="false"
class="input input-sm input-bordered font-mono w-48 pl-8 focus:w-64 transition-all"
oninput="filterProjects(this.value)">
<svg class="w-4 h-4 absolute left-2 top-1/2 -translate-y-1/2 text-base-content/30 pointer-events-none"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/>
</svg>
</div>
<a href="/ui/manage" class="btn btn-sm btn-ghost gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
Manage
</a>
</div>
<a href="/ui/manage" class="btn btn-sm btn-ghost gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
Manage
</a>
</div>
{% if projects %}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4" id="project-grid">
{% for p in projects %}
<div class="card bg-base-200 border border-base-content/10">
<div class="card bg-base-200 border border-base-content/10" data-slug="{{ p.slug }}" data-filter-text="{{ p.slug }} {{ p.description | default(value='') }} {{ p.card.tagline | default(value='') }}">
<!-- Card header: always visible -->
<div class="card-body gap-0 py-4 px-5">
@ -386,4 +401,26 @@
};
})();
</script>
<script>
(function () {
var TOTAL = {{ projects | length }};
window.filterProjects = function(q) {
var needle = q.trim().toLowerCase();
var cards = document.querySelectorAll('#project-grid [data-slug]');
var visible = 0;
cards.forEach(function(card) {
var hay = card.getAttribute('data-filter-text').toLowerCase();
var show = !needle || hay.indexOf(needle) !== -1;
card.style.display = show ? '' : 'none';
if (show) visible++;
});
var lbl = document.getElementById('project-count-label');
if (lbl) {
lbl.textContent = needle
? visible + ' of ' + TOTAL + ' project' + (TOTAL !== 1 ? 's' : '')
: TOTAL + ' project' + (TOTAL !== 1 ? 's' : '') + ' registered';
}
};
})();
</script>
{% endblock content %}

View file

@ -0,0 +1,177 @@
{% extends "base.html" %}
{% import "macros/ui.html" as m %}
{% block title %}Provisioning — {{ slug }} — Ontoref{% endblock title %}
{% block nav_provisioning %}active{% endblock nav_provisioning %}
{% block nav_group_domain %}active{% endblock nav_group_domain %}
{% block content %}
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold">Provisioning</h1>
<p class="text-base-content/50 text-sm mt-1">Connections · Production Readiness Gates</p>
</div>
</div>
<!-- Tabs -->
<div role="tablist" class="tabs tabs-bordered mb-4">
<a role="tab" class="tab tab-active" onclick="switchTab('connections', this)">Connections</a>
<a role="tab" class="tab" onclick="switchTab('gates', this)">Gates</a>
</div>
<!-- Connections tab -->
<div id="tab-connections">
<div id="connections-content"></div>
</div>
<!-- Gates tab -->
<div id="tab-gates" class="hidden">
<div id="gates-content" class="grid grid-cols-1 lg:grid-cols-2 gap-4"></div>
</div>
<!-- Detail modal -->
<dialog id="detail-modal" class="modal">
<div class="modal-box w-full max-w-2xl">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-3 top-3"></button>
</form>
<div id="modal-content" class="text-sm space-y-3"></div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
<script>
const CONNECTIONS = {{ connections_json | safe }};
const GATES = {{ gates_json | safe }};
const KIND_BADGE = {
ServiceDependency: 'badge-primary',
LibraryDependency: 'badge-info',
DataDependency: 'badge-warning',
InfrastructurePeer: 'badge-ghost',
SharedOwnership: 'badge-ghost',
};
const TENSION_BADGE = {
Low: 'badge-success', Medium: 'badge-warning', High: 'badge-error',
};
function kindBadge(k) {
return `<span class="badge badge-xs font-mono ${KIND_BADGE[k] ?? 'badge-ghost'}">${k}</span>`;
}
function tensionBadge(t) {
return `<span class="badge badge-xs font-mono ${TENSION_BADGE[t] ?? 'badge-ghost'}">${t}</span>`;
}
// ── Connections ───────────────────────────────────────────────────────────────
function renderConnections() {
const container = document.getElementById('connections-content');
const sections = [
{ key: 'upstream', label: 'Upstream', desc: 'Projects this one depends on' },
{ key: 'downstream', label: 'Downstream', desc: 'Projects that depend on this one' },
{ key: 'peers', label: 'Peers', desc: 'Sibling projects in the ecosystem' },
];
const allEmpty = sections.every(s => !(CONNECTIONS[s.key]||[]).length);
if (allEmpty) {
container.innerHTML = '<p class="text-base-content/30 text-sm py-6">No connections declared</p>';
return;
}
container.innerHTML = sections.map(s => {
const items = CONNECTIONS[s.key] || [];
if (!items.length) return '';
return `
<div class="mb-6">
<div class="flex items-baseline gap-2 mb-2">
<h2 class="text-sm font-bold uppercase tracking-wider text-base-content/60">${s.label}</h2>
<span class="badge badge-xs badge-neutral">${items.length}</span>
</div>
<div class="overflow-x-auto">
<table class="table table-sm w-full bg-base-200 rounded-lg">
<thead>
<tr class="text-base-content/50 text-xs uppercase tracking-wider">
<th>Project</th><th>Kind</th><th>Via</th><th>Node</th><th>Note</th>
</tr>
</thead>
<tbody>
${items.map(c => `
<tr class="hover">
<td class="font-mono text-xs text-primary font-bold">
${c.url ? `<a href="${c.url}" target="_blank" class="link link-hover">${c.project}</a>` : c.project}
</td>
<td>${kindBadge(c.kind)}</td>
<td class="font-mono text-xs text-base-content/50">${c.via || '—'}</td>
<td class="font-mono text-xs text-base-content/60">${c.node || '—'}</td>
<td class="text-xs text-base-content/50">${c.note || ''}</td>
</tr>`).join('')}
</tbody>
</table>
</div>
</div>`;
}).join('');
}
// ── Gates ─────────────────────────────────────────────────────────────────────
function renderGates() {
const container = document.getElementById('gates-content');
container.innerHTML = GATES.length === 0
? '<p class="text-base-content/30 text-sm py-6 col-span-2">No gates declared</p>'
: GATES.map((g, i) => {
const protects = (g.protege || g.protects || []);
const conditions = (g.opening_conditions || g.conditions || []);
return `
<div class="bg-base-200 rounded-lg p-4 space-y-2 cursor-pointer hover:bg-base-300 transition-colors"
onclick="showGateDetail(${i})">
<div class="flex items-start justify-between gap-2">
<div>
<p class="font-bold text-sm">${g.name || g.id}</p>
<p class="font-mono text-xs text-base-content/40">${g.id}</p>
</div>
${g.tension ? tensionBadge(g.tension) : ''}
</div>
<p class="text-xs text-base-content/60 leading-relaxed">${g.description || ''}</p>
${protects.length ? `<p class="text-xs text-base-content/40 font-mono">protects: ${protects.join(', ')}</p>` : ''}
${conditions.length ? `<p class="text-xs text-base-content/50">Conditions: ${conditions.length}</p>` : ''}
</div>`;
}).join('');
}
function showGateDetail(i) {
const g = GATES[i];
if (!g) return;
const protects = (g.protege || g.protects || []);
const conditions = (g.opening_conditions || g.conditions || []);
document.getElementById('modal-content').innerHTML = `
<div class="flex items-baseline gap-2 flex-wrap">
<span class="font-mono font-bold text-primary">${g.id}</span>
${g.tension ? tensionBadge(g.tension) : ''}
</div>
<h2 class="text-lg font-bold">${g.name || g.id}</h2>
${g.description ? `<p class="text-base-content/70 text-sm leading-relaxed">${g.description}</p>` : ''}
${protects.length ? `<p class="text-xs text-base-content/50 font-mono">protects: ${protects.join(', ')}</p>` : ''}
${conditions.length ? `
<div>
<p class="text-xs font-semibold uppercase tracking-wider text-base-content/50 mb-1">Opening conditions</p>
<ul class="text-xs text-base-content/60 space-y-1 pl-3">
${conditions.map(c => `<li class="list-disc">${typeof c === 'string' ? c : JSON.stringify(c)}</li>`).join('')}
</ul>
</div>` : ''}
`;
document.getElementById('detail-modal').showModal();
}
// ── Tabs ──────────────────────────────────────────────────────────────────────
function switchTab(name, el) {
['connections','gates'].forEach(t => {
document.getElementById('tab-' + t).classList.toggle('hidden', t !== name);
});
document.querySelectorAll('[role="tab"]').forEach(t => t.classList.remove('tab-active'));
el.classList.add('tab-active');
}
renderConnections();
renderGates();
</script>
{% endblock content %}

View file

@ -0,0 +1,151 @@
#!/usr/bin/env nu
# domains/framework/commands.nu — Framework protocol CLI for ontoref.
# Dispatched by the ontoref bash wrapper when repo_kind in {Library, Service, Tool}:
# ore framework <command> [args] or ore fw <command> [args]
#
# Provides protocol connectivity commands (state, connections, gates, capabilities)
# for projects that are frameworks, libraries, or services — no workspace-specific ops.
def project-root [] {
$env.ONTOREF_PROJECT_ROOT? | default (pwd | path expand)
}
# ─── Loaders ──────────────────────────────────────────────────────────────────
def load-state [] { ^nickel export $"(project-root)/.ontology/state.ncl" | from json }
def load-core [] { ^nickel export $"(project-root)/.ontology/core.ncl" | from json }
def load-manifest [] { ^nickel export $"(project-root)/.ontology/manifest.ncl" | from json }
def load-optional [path: string] {
if ($path | path exists) { ^nickel export $path | from json } else { null }
}
# ─── state / next / validate ──────────────────────────────────────────────────
export def "state" [] {
load-state | get dimensions | each { |dim|
{ dimension: $dim.id, current: $dim.current_state, desired: $dim.desired_state, horizon: $dim.horizon }
}
}
export def "next" [] {
load-state | get dimensions | each { |dim|
let active = $dim.transitions | where from == $dim.current_state
if ($active | is-empty) {
{ dimension: $dim.id, from: $dim.current_state, to: "", condition: "no active transition", blocker: "", catalyst: "" }
} else {
let tr = $active | first
{ dimension: $dim.id, from: $tr.from, to: $tr.to, condition: $tr.condition, blocker: $tr.blocker, catalyst: $tr.catalyst }
}
}
}
export def "validate" [decision: string] {
let core = load-core
let words = ($decision | str downcase | split row " " | where { |w| ($w | str length) > 3 })
let affected = ($core.nodes | each { |n|
let text = $"($n.id) ($n.name) ($n.description)" | str downcase
if ($words | any { |w| $text =~ $w }) { $n } else { null }
} | compact)
{
decision: $decision,
invariants_at_risk: ($affected | where invariant == true | select id level name),
tensions_touched: ($affected | where level == "Tension" | select id name),
practices_touched: ($affected | where level == "Practice" | select id name),
verdict: "manual review required — if any invariant is contradicted, justification is required",
}
}
# ─── connections / gates ──────────────────────────────────────────────────────
export def "connections" [] {
let path = $"(project-root)/.ontology/connections.ncl"
let c = load-optional $path
if ($c | is-empty) {
print --stderr "No connections.ncl found for this project."
exit 1
}
if ($c.upstream? | default [] | is-not-empty) {
print ""
print "UPSTREAM"
$c.upstream | each { |u|
print $" ($u.project) kind=($u.kind) via=($u.via)"
if ($u.node? | default "" | is-not-empty) { print $" node: ($u.node)" }
print $" ($u.note)"
}
}
if ($c.downstream? | default [] | is-not-empty) {
print ""
print "DOWNSTREAM"
$c.downstream | each { |d|
print $" ($d.project) kind=($d.kind) via=($d.via)"
if ($d.node? | default "" | is-not-empty) { print $" node: ($d.node)" }
print $" ($d.note)"
}
}
if ($c.peers? | default [] | is-not-empty) {
print ""
print "PEERS"
$c.peers | each { |p| print $" ($p.project) kind=($p.kind) via=($p.via)" }
}
if ($c.upstream? | default [] | is-empty) and ($c.downstream? | default [] | is-empty) and ($c.peers? | default [] | is-empty) {
print "No connections declared."
}
}
export def "gates" [] {
let path = $"(project-root)/.ontology/gate.ncl"
let g = load-optional $path
if ($g | is-empty) {
print "No gate.ncl found for this project."
return
}
$g | get membranes | each { |m|
{
membrane: $m.id,
name: $m.name,
active: $m.active,
permeability: $m.permeability,
condition: ($m.opening_condition.description? | default ""),
}
}
}
# ─── capabilities ─────────────────────────────────────────────────────────────
export def "capabilities" [] {
let caps = load-manifest | get capabilities? | default []
if ($caps | is-empty) { print "No capabilities declared in manifest."; return }
$caps | select id name summary
}
# ─── help + main ──────────────────────────────────────────────────────────────
def show-help [] {
print "Framework — ontoref domain extension"
print ""
print "USAGE"
print " ore framework <command> [args] (alias: ore fw)"
print ""
print "COMMANDS"
print " state Current FSM position across all dimensions"
print " next Next valid transitions with blockers/catalysts"
print " connections Upstream/downstream project dependency graph"
print " gates Gate membrane status and opening conditions"
print " capabilities Project capabilities from manifest"
print " validate <decision> Check decision against ontological invariants"
}
def main [...args: string] {
if ($args | is-empty) or ($args | first) == "help" { show-help; return }
let sub = ($args | str join " ")
match $sub {
"state" => { state }
"next" => { next }
"connections" => { connections }
"gates" => { gates }
"capabilities" => { capabilities }
_ if ($sub | str starts-with "validate ") => { validate ($args | skip 1 | str join " ") }
_ => { print --stderr $"Unknown command: ($sub)"; show-help; exit 1 }
}
}

View file

@ -0,0 +1,22 @@
let s = import "../schema.ncl" in
s.Domain & {
id = "framework",
name = "Framework Protocol",
repo_kinds = ["Library", "Service", "Tool"],
description = "Protocol connectivity commands for Library, Service, and Tool projects: FSM state, dependency graph, gate membranes, and manifest capabilities.",
short_alias = "fw",
commands = [
{ id = "state", description = "Current FSM position across all dimensions with blockers and catalysts" },
{ id = "next", description = "Next valid transitions per dimension" },
{ id = "connections", description = "Upstream and downstream project dependency graph" },
{ id = "gates", description = "Gate membrane status and opening conditions" },
{ id = "capabilities", description = "Platform capabilities from manifest" },
{ id = "validate <decision>", description = "Check a decision against ontological invariants" },
],
pages = [
{ id = "framework", route = "/<slug>/provisioning", title = "Connections · Gates · State", template = "provisioning", nav_label = "Framework" },
],
}

View file

@ -0,0 +1,3 @@
Library
Service
Tool

View file

@ -0,0 +1,336 @@
#!/usr/bin/env nu
# domains/personal/commands.nu — PersonalOntology domain CLI for ontoref.
# Dispatched by the ontoref bash wrapper when repo_kind = PersonalOntology:
# ore personal <command> [args]
#
# Reads data from ONTOREF_PROJECT_ROOT (set by the bash wrapper).
# Code lives in ONTOREF_ROOT/domains/personal/. Data lives in the project.
def project-root [] {
$env.ONTOREF_PROJECT_ROOT? | default (pwd | path expand)
}
# ─── Loaders ──────────────────────────────────────────────────────────────────
def load-core [] { ^nickel export $"(project-root)/.ontology/core.ncl" | from json }
def load-state [] { ^nickel export $"(project-root)/.ontology/state.ncl" | from json }
def load-career [] { ^nickel export $"(project-root)/.ontology/career.ncl" | from json }
def load-personal [] { ^nickel export $"(project-root)/.ontology/personal.ncl" | from json }
def load-backlog [] { ^nickel export $"(project-root)/reflection/backlog.ncl" | from json }
# ─── Display helpers ─────────────────────────────────────────────────────────
def render-links [links: list] {
if ($links | is-empty) { return }
print ""
print "links:"
$links | each { |lk|
let label = if ($lk.label? | default "" | is-not-empty) { $" — ($lk.label)" } else { "" }
print $" [($lk.kind)] ($lk.url)($label)"
}
}
# ─── NCL patch helpers ────────────────────────────────────────────────────────
def ncl-validate [path: string] {
^nickel export $path | from json | ignore
}
def ncl-block-field-idx [lines: list<string>, target_id: string, field: string] {
let id_idx = (
$lines | enumerate
| where { |l| $l.item | str contains $"\"($target_id)\"" }
| first | get index
)
let next_id_idx = (
$lines | enumerate | skip ($id_idx + 1)
| where { |l| ($l.item | str trim | str starts-with "id ") and ($l.item | str contains "=") and ($l.item | str contains "\"") }
| if ($in | is-empty) { [{ index: ($lines | length) }] } else { $in }
| first | get index
)
$lines | enumerate
| where { |l| $l.index > $id_idx and $l.index < $next_id_idx }
| where { |l| $l.item | str trim | str starts-with $"($field) " }
| first | get index
}
# ─── state / next / validate / audit ─────────────────────────────────────────
export def "state" [] {
load-state | get dimensions | each { |dim|
let cur = $dim.states | where id == $dim.current_state | first
{ dimension: $dim.id, current: $dim.current_state, desired: $dim.desired_state, tension: $cur.tension, horizon: $dim.horizon }
}
}
export def "next" [] {
load-state | get dimensions | each { |dim|
let active = $dim.transitions | where from == $dim.current_state
if ($active | is-empty) {
{ dimension: $dim.id, from: $dim.current_state, to: "", condition: "no active transition", blocker: "", catalyst: "", horizon: "" }
} else {
let tr = $active | first
{ dimension: $dim.id, from: $tr.from, to: $tr.to, condition: $tr.condition, blocker: $tr.blocker, catalyst: $tr.catalyst, horizon: $tr.horizon }
}
}
}
export def "validate" [decision: string] {
let core = load-core
let words = ($decision | str downcase | split row " " | where { |w| ($w | str length) > 3 })
let affected = ($core.nodes | each { |n|
let text = $"($n.id) ($n.name) ($n.description)" | str downcase
if ($words | any { |w| $text =~ $w }) { $n } else { null }
} | compact)
{
decision: $decision,
core_axioms: ($core.nodes | where { |n| $n.id in $core.core } | select id level name),
invariants_at_risk: ($affected | where invariant == true | select id level name),
tensions_touched: ($affected | where level == "Tension" | select id name),
practices_touched: ($affected | where level == "Practice" | select id name),
verdict: "manual review required — if any invariant is contradicted, justification is required",
}
}
export def "audit" [] {
let core = load-core
let nodes = $core.nodes
let edges = $core.edges
let practice_ids = ($nodes | where level == "Practice" | get id)
let tensions = ($nodes | where level == "Tension" | each { |t|
let linked = ($edges | where kind == "ManifestsIn" | where { |e| $e.from == $t.id or $e.to == $t.id }
| each { |e| let peer = if $e.from == $t.id { $e.to } else { $e.from }; if $peer in $practice_ids { $peer } else { null } } | compact)
{ tension: $t.id, name: $t.name, practice_count: ($linked | length), practices: ($linked | str join ", "), status: (if ($linked | is-empty) { "no practice" } else { "ok" }) }
})
let axioms = ($nodes | where level == "Axiom" | each { |a|
let manifests = ($edges | where from == $a.id and kind == "ManifestsIn" | each { |e| $nodes | where id == $e.to | first })
{ axiom: $a.id, name: $a.name, total_links: ($manifests | length), project_links: ($manifests | where level == "Project" | length), status: (if ($manifests | where level == "Project" | is-empty) { "no project link" } else { "ok" }) }
})
{ tensions: $tensions, axioms: $axioms, active_gap: $core.tension_without_practice }
}
# ─── projects ────────────────────────────────────────────────────────────────
export def "projects" [] {
load-core | get nodes | where level == "Project" | select id name description
}
export def "projects show" [id: string] {
let nodes = load-core | get nodes
let r = if ($id =~ '^[0-9]+$') {
let projects = ($nodes | where level == "Project")
let idx = ($id | into int)
if $idx >= ($projects | length) { print --stderr $"projects show: index out of range ($idx) — total ($projects | length)"; exit 1 }
$projects | skip $idx | first
} else {
let found = ($nodes | where id == $id and level == "Project")
if ($found | is-empty) { print --stderr $"projects show: not found — ($id)"; exit 1 }
$found | first
}
print $"id: ($r.id)"
print $"name: ($r.name)"
print $"description: ($r.description)"
if ($r.artifact_paths? | default [] | is-not-empty) {
print ""
print "artifacts:"
$r.artifact_paths | each { |p| print $" ($p)" }
}
render-links ($r.links? | default [])
}
# ─── career ───────────────────────────────────────────────────────────────────
export def "career skills" [--tier: string = ""] {
let items = load-career | get skills
let filtered = if ($tier | is-empty) { $items } else { $items | where tier == $tier }
$filtered | sort-by --reverse proficiency | select name tier proficiency years
}
export def "career talks" [--status: string = ""] {
let items = load-career | get talks
let filtered = if ($status | is-empty) { $items } else { $items | where status == $status }
$filtered | select id title event date status
}
export def "career publications" [--featured] {
let items = load-career | get publications | sort-by sort_order
let filtered = if $featured { $items | where featured == true } else { $items }
$filtered | select project_node tagline status version featured
}
export def "career positioning" [] {
load-career | get positioning | select name core_message target
}
# ─── content ──────────────────────────────────────────────────────────────────
export def "content" [--status: string = ""] {
let items = load-personal | get contents
let filtered = if ($status | is-empty) { $items } else { $items | where status == $status }
$filtered | select id title kind status
}
# ─── opportunities ────────────────────────────────────────────────────────────
export def "opportunities" [--status: string = ""] {
let items = load-personal | get opportunities
let filtered = if ($status | is-empty) { $items } else { $items | where status == $status }
$filtered | select id name kind status deadline
}
export def "opportunities show" [id: string] {
let items = load-personal | get opportunities
let r = if ($id =~ '^[0-9]+$') {
let idx = ($id | into int)
if $idx >= ($items | length) { print --stderr $"opportunities show: index out of range ($idx) — total ($items | length)"; exit 1 }
$items | skip $idx | first
} else {
let found = ($items | where id == $id)
if ($found | is-empty) { print --stderr $"opportunities show: not found — ($id)"; exit 1 }
$found | first
}
print $"id: ($r.id)"
print $"name: ($r.name)"
print $"kind: ($r.kind)"
print $"status: ($r.status)"
print $"deadline: ($r.deadline)"
render-links ($r.links? | default [])
if ($r.fit_signals | is-not-empty) { print ""; print $"fit_signals: ($r.fit_signals | str join ', ')" }
if ($r.linked_nodes | is-not-empty) { print ""; print $"nodes: ($r.linked_nodes | str join ', ')" }
print ""; print "note:"; print $" ($r.note)"
}
export def "opportunities update" [id: string, --status: string] {
let valid = ["Watching" "Evaluating" "Active" "Submitted" "Closed"]
if ($status | is-empty) { print --stderr "opportunities update: --status required"; exit 1 }
if not ($status in $valid) { print --stderr $"opportunities update: invalid status '($status)' — valid: ($valid | str join ', ')"; exit 1 }
let items = load-personal | get opportunities
if ($items | where id == $id | is-empty) { print --stderr $"opportunities update: not found — ($id)"; exit 1 }
let path = $"(project-root)/.ontology/personal.ncl"
let lines = open --raw $path | lines
let status_idx = (ncl-block-field-idx $lines $id "status")
let new_status = ($lines | get $status_idx | str replace --regex `'\w+` $"'($status)")
let patched = ($lines | enumerate | each { |l| if $l.index == $status_idx { $new_status } else { $l.item } } | str join "\n")
$patched | save --force $path
ncl-validate $path
print $"($id): status → '($status)"
}
# ─── cfp ──────────────────────────────────────────────────────────────────────
export def "cfp" [--stage: string = ""] {
let items = load-backlog | get cfp
let filtered = if ($stage | is-empty) { $items } else { $items | where stage == $stage }
$filtered | select id name stage deadline sessionize_session_id next_action
}
export def "cfp show" [id: string] {
let items = load-backlog | get cfp
let r = if ($id =~ '^[0-9]+$') {
let idx = ($id | into int)
if $idx >= ($items | length) { print --stderr $"cfp show: index out of range ($idx) — total ($items | length)"; exit 1 }
$items | skip $idx | first
} else {
let found = ($items | where id == $id)
if ($found | is-empty) { print --stderr $"cfp show: not found — ($id)"; exit 1 }
$found | first
}
print $"id: ($r.id)"
print $"name: ($r.name)"
print $"stage: ($r.stage)"
print $"deadline: ($r.deadline)"
print $"opportunity_id: ($r.opportunity_id)"
print $"sessionize_session_id: ($r.sessionize_session_id)"
print $"related_mode: ($r.related_mode)"
print $"created: ($r.created)"
print $"updated: ($r.updated)"
render-links ($r.links? | default [])
print ""; print "next_action:"; print $" ($r.next_action)"
print ""; print "note:"; print $" ($r.note)"
}
export def "cfp update" [id: string, --stage: string] {
let valid = ["Watching" "Evaluating" "Drafting" "Submitted" "Accepted" "Declined" "Delivered"]
if ($stage | is-empty) { print --stderr "cfp update: --stage required"; exit 1 }
if not ($stage in $valid) { print --stderr $"cfp update: invalid stage '($stage)' — valid: ($valid | str join ', ')"; exit 1 }
let items = load-backlog | get cfp
if ($items | where id == $id | is-empty) { print --stderr $"cfp update: not found — ($id)"; exit 1 }
let today = (date now | format date "%Y-%m-%d")
let path = $"(project-root)/reflection/backlog.ncl"
let lines = open --raw $path | lines
let stage_idx = (ncl-block-field-idx $lines $id "stage")
let updated_idx = (ncl-block-field-idx $lines $id "updated")
let new_stage = ($lines | get $stage_idx | str replace --regex `'\w+` $"'($stage)")
let new_updated = ($lines | get $updated_idx | str replace --regex `"[0-9-]+"` $"\"($today)\"")
let patched = ($lines | enumerate | each { |l|
if $l.index == $stage_idx { $new_stage } else if $l.index == $updated_idx { $new_updated } else { $l.item }
} | str join "\n")
$patched | save --force $path
ncl-validate $path
print $"($id): stage → '($stage) | updated → ($today)"
}
# ─── help + main ──────────────────────────────────────────────────────────────
def show-help [] {
print "Personal Ontology — ontoref domain extension"
print ""
print "USAGE"
print " ore personal <command> [args]"
print ""
print "COMMANDS"
print " projects Project portfolio (Project-level nodes)"
print " projects show <id|idx> Full detail of a project node"
print " state Current FSM position across all dimensions"
print " next Next valid transition per dimension"
print " validate <decision> Check against ontological invariants"
print " audit Coherence audit: gaps in coverage"
print " career skills [--tier] Skills by proficiency"
print " career talks [--status] Talks filtered by status"
print " career publications [--featured] Publication cards"
print " career positioning Positioning strategies"
print " content [--status] Content pipeline"
print " opportunities [--status] Opportunities"
print " opportunities show <id> Full opportunity detail"
print " opportunities update <id> Update opportunity status"
print " cfp [--stage] CFP pipeline"
print " cfp show <id> Full CFP detail"
print " cfp update <id> --stage <s> Update CFP stage"
}
def main [
...args: string
--tier: string = ""
--status: string = ""
--stage: string = ""
--featured
] {
if ($args | is-empty) or ($args | first) == "help" { show-help; return }
let sub = ($args | str join " ")
match $sub {
"state" => { state }
"next" => { next }
"audit" => { audit }
"projects" => { projects }
"projects show" => { print --stderr "usage: personal projects show <id|idx>"; exit 1 }
_ if ($sub | str starts-with "projects show ") => { projects show ($args | get 2) }
_ if ($sub | str starts-with "projects ") => { projects show ($args | get 1) }
_ if ($sub | str starts-with "validate ") => { validate ($args | skip 1 | str join " ") }
"career skills" => { career skills --tier $tier }
"career talks" => { career talks --status $status }
"career publications" => { if $featured { career publications --featured } else { career publications } }
"career positioning" => { career positioning }
"content" => { content --status $status }
"opportunities" => { opportunities --status $status }
"opportunities show" => { print --stderr "usage: personal opportunities show <id|idx>"; exit 1 }
_ if ($sub | str starts-with "opportunities show ") => { opportunities show ($args | get 2) }
_ if ($sub | str starts-with "opportunities update ") => { opportunities update ($args | get 2) --status $status }
_ if ($sub | str starts-with "opportunities ") => { opportunities show ($args | get 1) }
"cfp" => { cfp --stage $stage }
"cfp show" => { print --stderr "usage: personal cfp show <id|idx>"; exit 1 }
_ if ($sub | str starts-with "cfp show ") => { cfp show ($args | get 2) }
_ if ($sub | str starts-with "cfp update ") => { cfp update ($args | get 2) --stage $stage }
_ if ($sub | str starts-with "cfp ") => { cfp show ($args | get 1) }
_ => { print $"Unknown command: ($sub)"; show-help }
}
}

View file

@ -0,0 +1,35 @@
let s = import "../schema.ncl" in
s.Domain & {
id = "personal",
name = "Personal Ontology",
repo_kinds = ["PersonalOntology"],
description = "Career history, CFP pipeline, opportunities, and content tracking for PersonalOntology projects.",
required_extensions = ["career", "personal"],
short_alias = "personal",
commands = [
{ id = "projects", description = "Project portfolio — Project-level nodes from core.ncl" },
{ id = "projects show <id|idx>", description = "Full detail of a project node" },
{ id = "state", description = "Current FSM position across all dimensions" },
{ id = "next", description = "Next valid transition per dimension with blocker/catalyst" },
{ id = "validate <decision>", description = "Check a decision against ontological invariants" },
{ id = "audit", description = "Coherence audit: tensions without practices, axioms without project links" },
{ id = "career skills [--tier]", description = "Skills sorted by proficiency, filtered by Expert|Advanced|Intermediate|Foundational" },
{ id = "career talks [--status]", description = "Talks filtered by Idea|Proposed|Accepted|Delivered|Archived" },
{ id = "career publications", description = "Publication cards sorted by order, --featured for featured only" },
{ id = "career positioning", description = "Positioning strategies with core message" },
{ id = "content [--status]", description = "Content pipeline filtered by Idea|Draft|Review|Published|Archived" },
{ id = "opportunities [--status]", description = "Opportunities filtered by Watching|Evaluating|Active|Submitted|Closed" },
{ id = "opportunities show <id>", description = "Full detail of a single opportunity" },
{ id = "opportunities update <id>", description = "Update opportunity status in .ontology/personal.ncl" },
{ id = "cfp [--stage]", description = "CFP pipeline filtered by Watching|Evaluating|Drafting|Submitted|Accepted|Declined|Delivered" },
{ id = "cfp show <id>", description = "Full detail of a single CFP item" },
{ id = "cfp update <id> --stage <s>", description = "Update CFP stage in reflection/backlog.ncl" },
],
pages = [
{ id = "personal", route = "/<slug>/personal", title = "Personal (Content · Opportunities · CFP)", template = "personal", nav_label = "Personal" },
{ id = "career", route = "/<slug>/career", title = "Career (Skills · Experience · Talks)", template = "career", nav_label = "Career" },
],
}

View file

@ -0,0 +1 @@
PersonalOntology

View file

@ -0,0 +1,220 @@
#!/usr/bin/env nu
# domains/provisioning/commands.nu — Provisioning domain CLI for ontoref.
# Dispatched by the ontoref bash wrapper when repo_kind in {DevWorkspace, Mixed}:
# ore provisioning <command> [args]
#
# Works for both DevWorkspace (workspace-level) and Mixed (platform-level) projects.
# Commands that require project-specific files (card.ncl, reflection/backlog.ncl)
# degrade gracefully when those files are absent.
def project-root [] {
$env.ONTOREF_PROJECT_ROOT? | default (pwd | path expand)
}
# ─── Loaders ──────────────────────────────────────────────────────────────────
def load-state [] { ^nickel export $"(project-root)/.ontology/state.ncl" | from json }
def load-core [] { ^nickel export $"(project-root)/.ontology/core.ncl" | from json }
def load-gate [] { ^nickel export $"(project-root)/.ontology/gate.ncl" | from json }
def load-connections [] { ^nickel export $"(project-root)/.ontology/connections.ncl" | from json }
def load-manifest [] { ^nickel export $"(project-root)/.ontology/manifest.ncl" | from json }
def load-card [] {
let path = $"(project-root)/card.ncl"
if ($path | path exists) { ^nickel export $path | from json } else { null }
}
def load-backlog [] {
let path = $"(project-root)/reflection/backlog.ncl"
if ($path | path exists) { ^nickel export $path | from json } else { { items: [] } }
}
# ─── state / next / validate ──────────────────────────────────────────────────
export def "state" [] {
load-state | get dimensions | each { |dim|
{ dimension: $dim.id, current: $dim.current_state, desired: $dim.desired_state, horizon: $dim.horizon }
}
}
export def "next" [] {
load-state | get dimensions | each { |dim|
let active = $dim.transitions | where from == $dim.current_state
if ($active | is-empty) {
{ dimension: $dim.id, from: $dim.current_state, to: "", condition: "no active transition", blocker: "", catalyst: "" }
} else {
let tr = $active | first
{ dimension: $dim.id, from: $tr.from, to: $tr.to, condition: $tr.condition, blocker: $tr.blocker, catalyst: $tr.catalyst }
}
}
}
export def "validate" [decision: string] {
let core = load-core
let words = ($decision | str downcase | split row " " | where { |w| ($w | str length) > 3 })
let affected = ($core.nodes | each { |n|
let text = $"($n.id) ($n.name) ($n.description)" | str downcase
if ($words | any { |w| $text =~ $w }) { $n } else { null }
} | compact)
{
decision: $decision,
invariants_at_risk: ($affected | where invariant == true | select id level name),
tensions_touched: ($affected | where level == "Tension" | select id name),
practices_touched: ($affected | where level == "Practice" | select id name),
verdict: "manual review required — if any invariant is contradicted, justification is required",
}
}
# ─── connections / gates ──────────────────────────────────────────────────────
export def "connections" [] {
let c = load-connections
if ($c.upstream? | default [] | is-not-empty) {
print ""
print "UPSTREAM"
$c.upstream | each { |u|
print $" ($u.project) kind=($u.kind) via=($u.via)"
if ($u.node? | default "" | is-not-empty) { print $" node: ($u.node)" }
print $" ($u.note)"
}
}
if ($c.downstream? | default [] | is-not-empty) {
print ""
print "DOWNSTREAM"
$c.downstream | each { |d|
print $" ($d.project) kind=($d.kind) via=($d.via)"
if ($d.node? | default "" | is-not-empty) { print $" node: ($d.node)" }
print $" ($d.note)"
}
}
if ($c.peers? | default [] | is-not-empty) {
print ""
print "PEERS"
$c.peers | each { |p| print $" ($p.project) kind=($p.kind) via=($p.via)" }
}
}
export def "gates" [] {
load-gate | get membranes | each { |m|
{
membrane: $m.id,
name: $m.name,
active: $m.active,
permeability: $m.permeability,
condition: ($m.opening_condition.description? | default ""),
}
}
}
# ─── card (DevWorkspace only) ─────────────────────────────────────────────────
export def "card" [] {
let c = load-card
if ($c | is-empty) {
print "No card.ncl found — this command is only available for DevWorkspace projects."
return
}
print $"id: ($c.id)"
print $"name: ($c.name)"
print $"tagline: ($c.tagline)"
print $"status: ($c.status)"
print $"version: ($c.version)"
print $"started: ($c.started_at)"
if ($c.tags? | default [] | is-not-empty) { print $"tags: ($c.tags | str join ', ')" }
if ($c.tools? | default [] | is-not-empty) { print $"tools: ($c.tools | str join ', ')" }
if ($c.features? | default [] | is-not-empty) {
print ""
print "features:"
$c.features | each { |f| print $" - ($f)" }
}
}
# ─── capabilities (manifest.capabilities) ────────────────────────────────────
export def "capabilities" [] {
let caps = load-manifest | get capabilities? | default []
if ($caps | is-empty) { print "No capabilities declared in manifest."; return }
$caps | select id name summary
}
# ─── backlog (platform only) ──────────────────────────────────────────────────
export def "backlog" [--priority: string = ""] {
let items = load-backlog | get items
if ($items | is-empty) {
print "No backlog.ncl found — this command is only available for platform projects."
return
}
let filtered = if ($priority | is-empty) { $items } else { $items | where priority == $priority }
$filtered | each { |i|
{
id: $i.id,
priority: $i.priority,
blocked_by: ($i.blocked_by | str join ", "),
}
}
}
export def "backlog show" [id: string] {
let items = load-backlog | get items
if ($items | is-empty) { error make { msg: "No backlog.ncl found for this project." } }
let item = $items | where id == $id
if ($item | is-empty) { error make { msg: $"Backlog item not found: ($id)" } }
let r = $item | first
print $"id: ($r.id)"
print $"priority: ($r.priority)"
print ""
print "description:"
print $" ($r.description)"
if ($r.blocked_by? | default [] | is-not-empty) {
print ""
print $"blocked_by: ($r.blocked_by | str join ', ')"
}
if ($r.related_nodes? | default [] | is-not-empty) {
print $"nodes: ($r.related_nodes | str join ', ')"
}
}
# ─── help + main ──────────────────────────────────────────────────────────────
def show-help [] {
print "Provisioning — ontoref domain extension"
print ""
print "USAGE"
print " ore provisioning <command> [args]"
print ""
print "COMMANDS (all repo_kinds)"
print " state Current FSM position across all dimensions"
print " next Next valid transitions with blockers/catalysts"
print " validate <decision> Check against ontological invariants"
print " connections Upstream/downstream project dependency graph"
print " gates Membrane status and opening conditions"
print " capabilities Platform capabilities from manifest"
print ""
print "COMMANDS (DevWorkspace only)"
print " card Workspace card: identity, clusters, status"
print ""
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"
}
def main [
...args: string
--priority: string = ""
] {
if ($args | is-empty) or ($args | first) == "help" { show-help; return }
let sub = ($args | str join " ")
match $sub {
"state" => { state }
"next" => { next }
"connections" => { connections }
"gates" => { gates }
"card" => { card }
"capabilities" => { capabilities }
"backlog" => { backlog --priority $priority }
_ if ($sub | str starts-with "backlog show ") => { backlog show ($args | get 2) }
_ if ($sub | str starts-with "validate ") => { validate ($args | skip 1 | str join " ") }
_ => { print $"Unknown command: ($sub)"; show-help }
}
}

View file

@ -0,0 +1,26 @@
let s = import "../schema.ncl" in
s.Domain & {
id = "provisioning",
name = "Provisioning",
repo_kinds = ["DevWorkspace", "Mixed"],
description = "Infrastructure workspace and platform commands for DevWorkspace and Mixed projects.",
short_alias = "prov",
required_extensions = [],
commands = [
{ id = "state", description = "Current FSM position across all dimensions" },
{ id = "next", description = "Next valid transitions with blockers and catalysts" },
{ id = "validate <decision>", description = "Check a decision against ontological invariants" },
{ id = "connections", description = "Upstream and downstream project dependency graph" },
{ id = "gates", description = "Membrane status and opening conditions" },
{ id = "card", description = "Workspace card — identity, clusters, status (DevWorkspace only)" },
{ 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" },
],
pages = [
{ id = "provisioning", route = "/<slug>/provisioning", title = "Provisioning (State · Connections · Gates)", template = "provisioning", nav_label = "Provisioning" },
],
}

View file

@ -0,0 +1,2 @@
DevWorkspace
Mixed

42
domains/schema.ncl Normal file
View file

@ -0,0 +1,42 @@
# domains/schema.ncl — contract for ontoref domain extension manifests.
#
# A domain is a self-contained extension loaded by ontoref when the project's
# repo_kind matches one of the domain's declared repo_kinds.
# Domains live in $ONTOREF_ROOT/domains/{id}/ and provide:
# - commands.nu → CLI commands dispatched by the bash wrapper
# - domain.ncl → this contract: declared commands, pages, requirements
# - repo_kinds.txt → plain-text list of matching repo_kind values (bash-readable)
let domain_command_type = {
id | String, # e.g. "cfp list", "career skills --tier"
description | String,
usage | String | default = "",
examples | Array String | default = [],
} in
let domain_page_type = {
id | String, # "cfp", "career"
route | String, # "/personal/cfp"
title | String,
template | String, # filename stem in domain's pages/ dir
nav_label | String | default = "",
} in
let domain_type = {
id | String,
name | String,
repo_kinds | Array String, # must match values in manifest.ncl RepoKind enum
description | String | default = "",
commands | Array domain_command_type | default = [],
pages | Array domain_page_type | default = [],
# .ontology/*.ncl stems that must exist for commands to work
required_extensions | Array String | default = [],
commands_script | String | default = "commands.nu",
short_alias | String | default = "",
} in
{
DomainCommand = domain_command_type,
DomainPage = domain_page_type,
Domain = domain_type,
}

View file

@ -40,8 +40,10 @@ def main [
| each { |l| $l | str trim }
| where { |l| ($l | str contains "import \"") }
| each { |l|
$l | parse --regex 'import\s+"(?P<path>[^"]+)"' | get path | first
let parsed = ($l | parse --regex 'import\s+"(?P<path>[^"]+)"')
if ($parsed | is-empty) { null } else { $parsed | get path | first }
}
| compact
| flatten
)
@ -83,9 +85,9 @@ def main [
if not ($p | path exists) {
let project_root = ($p | str replace --regex '(/\.ontoref/project\.ncl)$' '')
if not ($project_root | path exists) {
print --stderr $" (ansi yellow)WARN(ansi reset) removing missing project (root deleted): ($project_root)"
print --stderr $" (ansi yellow)WARN(ansi reset) removing missing project \(root deleted\): ($project_root)"
} else {
print --stderr $" (ansi yellow)WARN(ansi reset) removing invalid project (project.ncl missing): ($p)"
print --stderr $" (ansi yellow)WARN(ansi reset) removing invalid project \(project.ncl missing\): ($p)"
}
null
} else {

View file

@ -135,7 +135,87 @@ def main [] {
}
print $"✓ reflection ($reflection_dest)/ updated=($refl_updated) unchanged=($refl_skipped)"
# ── 3c. CLI templates (project.ncl, ontoref-config.ncl, ontology/ stubs) ──
# ── 3c. Domain extensions (domains/) ─────────────────────────────────────
# Domain commands are dispatched by the bash wrapper at runtime from $ONTOREF_ROOT/domains/.
let domains_src = $"($repo_root)/domains"
let domains_dest = $"($data_dir)/domains"
if ($domains_src | path exists) {
mkdir $domains_dest
mut dom_updated = 0
mut dom_skipped = 0
for src_file in (glob $"($domains_src)/**/*" | where { |f| ($f | path type) == "file" }) {
let rel = ($src_file | str replace $"($domains_src)/" "")
let dest_file = $"($domains_dest)/($rel)"
let dest_parent = ($dest_file | path dirname)
mkdir $dest_parent
let needs_update = if ($dest_file | path exists) {
(open --raw $src_file | hash sha256) != (open --raw $dest_file | hash sha256)
} else {
true
}
if $needs_update {
cp $src_file $dest_file
$dom_updated = $dom_updated + 1
} else {
$dom_skipped = $dom_skipped + 1
}
}
print $"✓ domains ($domains_dest)/ updated=($dom_updated) unchanged=($dom_skipped)"
# Build aliases.txt and install short_alias bin wrappers for each domain.
mut alias_lines = []
for domain_dir in (ls $domains_src | where type == "dir" | get name) {
let domain_ncl = $"($domain_dir)/domain.ncl"
if not ($domain_ncl | path exists) { continue }
let domain_data = (do { ^nickel export $domain_ncl } | complete)
if $domain_data.exit_code != 0 { continue }
let d = ($domain_data.stdout | from json)
let alias = ($d.short_alias? | default "")
let domain_id = ($d.id? | default "")
if ($alias | is-empty) { continue }
# aliases.txt entry — only when alias differs from domain id (e.g. prov → provisioning)
# skipped when alias == domain_id (e.g. personal → personal): ore already handles it natively
if $alias != $domain_id {
$alias_lines = ($alias_lines | append $"($alias)=($domain_id)")
}
# standalone bin wrapper — always created when short_alias is set,
# even when alias == domain_id, so `personal state` works without `ore`
let alias_dest = $"($bin_dir)/($alias)"
let alias_body = $"#!/bin/bash\nexec ontoref ($domain_id) \"$@\"\n"
let needs_update = if ($alias_dest | path exists) {
($alias_body | hash sha256) != (open --raw $alias_dest | hash sha256)
} else { true }
if $needs_update {
$alias_body | save --force $alias_dest
chmod +x $alias_dest
print $"✓ alias ($alias_dest) → ontoref ($domain_id)"
} else {
print $"— alias ($alias) unchanged"
}
}
# Write consolidated aliases.txt to the installed domains dir
let aliases_dest = $"($domains_dest)/aliases.txt"
if ($alias_lines | is-not-empty) {
let aliases_body = ($alias_lines | str join "\n")
let needs_update = if ($aliases_dest | path exists) {
($aliases_body | hash sha256) != (open --raw $aliases_dest | hash sha256)
} else { true }
if $needs_update {
$aliases_body | save --force $aliases_dest
print $"✓ domain-aliases ($aliases_dest)"
} else {
print $"— domain-aliases unchanged"
}
}
} else {
print $" (ansi yellow)warn(ansi reset) domains/ not found at ($domains_src)"
}
# ── 3d. CLI templates (project.ncl, ontoref-config.ncl, ontology/ stubs) ──
# `ontoref setup` reads from $ONTOREF_ROOT/templates/ — copy the repo-level
# templates/ tree so the installed CLI works without the source repo present.
let cli_templates_src = $"($repo_root)/templates"
@ -167,6 +247,34 @@ def main [] {
print $" (ansi yellow)warn(ansi reset) templates/ not found at ($cli_templates_src)"
}
# ── 3e. Ontology defaults + schemas (data dir) ───────────────────────────
# Consumer projects import "ontology/defaults/state.ncl", "defaults/manifest.ncl", etc.
# These must be resolvable from $ONTOREF_ROOT (the data dir).
# Structure: $data_dir/ontology/{defaults,schemas}/
# The bash wrapper includes $ONTOREF_ROOT in NICKEL_IMPORT_PATH, so:
# import "ontology/defaults/state.ncl" → $data_dir/ontology/defaults/state.ncl ✓
# import "defaults/state.ncl" → $data_dir/ontology/defaults/state.ncl ✓ (via $ONTOREF_ROOT/ontology in nickel-import-path)
let ontology_src = $"($repo_root)/ontology"
let ontology_dest = $"($data_dir)/ontology"
if ($ontology_src | path exists) {
mkdir $ontology_dest
mut ont_updated = 0
mut ont_skipped = 0
for src_file in (glob $"($ontology_src)/**/*.ncl" | where { |f| ($f | path type) == "file" }) {
let rel = ($src_file | str replace $"($ontology_src)/" "")
let dest_file = $"($ontology_dest)/($rel)"
let dest_parent = ($dest_file | path dirname)
mkdir $dest_parent
let needs_update = if ($dest_file | path exists) {
(open --raw $src_file | hash sha256) != (open --raw $dest_file | hash sha256)
} else {
true
}
if $needs_update { cp $src_file $dest_file; $ont_updated = $ont_updated + 1 } else { $ont_skipped = $ont_skipped + 1 }
}
print $"✓ ontology ($ontology_dest)/ updated=($ont_updated) unchanged=($ont_skipped)"
}
# ── 4. UI assets (data dir) ────────────────────────────────────────────────
let templates_src = $"($repo_root)/crates/ontoref-daemon/templates"
let public_src = $"($repo_root)/crates/ontoref-daemon/public"
@ -302,7 +410,7 @@ def main [] {
let mode_tag = if $is_dev { "dev" } else { "service" }
print $"\ninstalled mode=($mode_tag) platform=($platform)"
print $" bin ($bin_dir)/ontoref, ontoref-daemon, ontoref-daemon.bin"
print $" data ($data_dir)/"
print $" data ($data_dir)/ \(reflection/, domains/, templates/, ...\)"
print $" config ($config_dir)/"
print ""
print " next: nu install/config-setup.nu"

View file

@ -145,7 +145,11 @@ load_ontoref_env() {
# Installed schemas dir provides ontoref-project.ncl and other protocol schemas.
local installed_schemas="${HOME}/.config/ontoref/schemas"
local fallback_paths="${ONTOREF_PROJECT_ROOT}:${ONTOREF_PROJECT_ROOT}/.ontology:${ONTOREF_PROJECT_ROOT}/adrs:${installed_schemas}:${ONTOREF_ROOT}"
# $ONTOREF_ROOT/ontology is included so consumer projects can resolve:
# import "defaults/state.ncl" → $ONTOREF_ROOT/ontology/defaults/state.ncl
# import "schemas/manifest.ncl" → $ONTOREF_ROOT/ontology/schemas/manifest.ncl
local ontoref_ontology="${ONTOREF_ROOT}/ontology"
local fallback_paths="${ONTOREF_PROJECT_ROOT}:${ONTOREF_PROJECT_ROOT}/.ontology:${ONTOREF_PROJECT_ROOT}/adrs:${installed_schemas}:${ontoref_ontology}:${ONTOREF_ROOT}"
if [[ ! -f "${CONFIG_NCL}" ]]; then
export NICKEL_IMPORT_PATH="${fallback_paths}"
@ -160,7 +164,7 @@ load_ontoref_env() {
fi
if [[ -n "${raw_paths}" ]]; then
export NICKEL_IMPORT_PATH="${raw_paths}:${installed_schemas}:${ONTOREF_ROOT}"
export NICKEL_IMPORT_PATH="${raw_paths}:${installed_schemas}:${ontoref_ontology}:${ONTOREF_ROOT}"
else
export NICKEL_IMPORT_PATH="${fallback_paths}"
fi
@ -170,6 +174,22 @@ if [[ -z "${NICKEL_IMPORT_PATH:-}" ]]; then
load_ontoref_env
fi
# ── Export ONTOREF_DOMAIN_ROOT from domain_origin.path ────────────────────────
# If the project declares domain_origin in its manifest, expose the framework path
# so NCL extension files can import from: import "%{env.ONTOREF_DOMAIN_ROOT}/.ontology/..."
# Uses grep (no nickel penalty) — path line expected to be a simple string literal.
if [[ -z "${ONTOREF_DOMAIN_ROOT:-}" ]]; then
_manifest="${ONTOREF_PROJECT_ROOT}/.ontology/manifest.ncl"
if grep -q "domain_origin" "${_manifest}" 2>/dev/null; then
_domain_path="$(grep -A5 "domain_origin" "${_manifest}" | grep 'path\s*=' | head -1 | sed 's/.*=\s*"\(.*\)".*/\1/')"
if [[ -n "${_domain_path}" && -d "${_domain_path}" ]]; then
export ONTOREF_DOMAIN_ROOT="${_domain_path}"
fi
unset _domain_path
fi
unset _manifest
fi
# ── Advisory locking (mkdir-based, POSIX-atomic) ──────────────────────────────
determine_lock() {
@ -350,6 +370,86 @@ _copy_to_clipboard() {
echo " ✓ Copied to clipboard" >&2
}
# ── Domain extension dispatch ─────────────────────────────────────────────────
# If the first arg matches a domain id and the project's repo_kind is in that
# domain's repo_kinds.txt, delegate to the domain's commands.nu directly.
_dispatch_domain() {
local first_arg="${REMAINING_ARGS[0]:-}"
[[ -z "$first_arg" ]] && return 1
# Resolve short alias → domain id (e.g. prov → provisioning, jpl → personal).
local aliases_file="${ONTOREF_ROOT}/domains/aliases.txt"
if [[ -f "$aliases_file" ]]; then
local resolved
resolved="$(grep "^${first_arg}=" "$aliases_file" 2>/dev/null | cut -d= -f2)" || true
if [[ -n "$resolved" ]]; then
first_arg="$resolved"
REMAINING_ARGS=("$first_arg" "${REMAINING_ARGS[@]:1}")
fi
fi
local domain_dir="${ONTOREF_ROOT}/domains/${first_arg}"
# Not a known domain ID or alias — fall through to Nu dispatcher as normal command.
[[ ! -d "$domain_dir" ]] && return 1
# From here on, the arg IS a domain name. All failures become diagnostics, not fall-throughs.
local repo_kinds_file="${domain_dir}/repo_kinds.txt"
if [[ ! -f "$repo_kinds_file" ]]; then
echo "ontoref: domain '${first_arg}' is missing repo_kinds.txt — reinstall ontoref" >&2
exit 1
fi
local manifest="${ONTOREF_PROJECT_ROOT}/.ontology/manifest.ncl"
if [[ ! -f "$manifest" ]]; then
echo "" >&2
echo " ontoref: domain '${first_arg}' requires a project with .ontology/manifest.ncl" >&2
echo " current project: ${ONTOREF_PROJECT_ROOT}" >&2
echo "" >&2
exit 1
fi
# Extract repo_kind directly from the NCL source — no nickel export needed.
# The field is always a literal enum tag: repo_kind = 'SomeName,
# This avoids import-path resolution failures for manifests that import schemas.
local repo_kind=""
repo_kind="$(grep -oE "repo_kind\s*=\s*'[A-Za-z_]+" "$manifest" 2>/dev/null | grep -oE "'[A-Za-z_]+$" | tr -d "'")" || true
if [[ -z "$repo_kind" ]]; then
echo "" >&2
echo " ontoref: domain '${first_arg}' is not available for this project" >&2
echo " reason: repo_kind is not set in ${manifest}" >&2
echo "" >&2
exit 1
fi
if ! grep -qx "$repo_kind" "$repo_kinds_file" 2>/dev/null; then
local required
required="$(tr '\n' ' ' < "$repo_kinds_file" | sed 's/ $//')"
echo "" >&2
echo " ontoref: domain '${first_arg}' is not available for this project" >&2
echo " requires repo_kind: ${required}" >&2
echo " current repo_kind: ${repo_kind}" >&2
echo "" >&2
exit 1
fi
local domain_cmd="${domain_dir}/commands.nu"
if [[ ! -f "$domain_cmd" ]]; then
echo "ontoref: domain '${first_arg}' commands.nu not found — reinstall ontoref" >&2
exit 1
fi
local domain_args=("${REMAINING_ARGS[@]:1}")
nu "$domain_cmd" "${domain_args[@]+"${domain_args[@]}"}"
return 0
}
if _dispatch_domain; then
exit 0
fi
# ── Delegate to Nushell dispatcher ────────────────────────────────────────────
LOCK_RESOURCE="$(determine_lock)"

View file

@ -4,5 +4,7 @@ import 'justfiles/dev.just'
import 'justfiles/ci.just'
import 'justfiles/assets.just'
project_root := justfile_directory()
default:
@just --list

View file

@ -1,6 +1,7 @@
let content = import "content.ncl" in
let repo_kind_type = [|
'Framework,
'DevWorkspace,
'PublishedCrate,
'Service,
@ -284,6 +285,56 @@ let critical_dep_type = {
mitigation | String | default = "",
} in
# ── Domain ontology layer ───────────────────────────────────────────────────
# A project may have two distinct ontological layers:
# 1. Self-ontology (.ontology/) — the project describing itself: state, ADRs, practices.
# Present in any project using the ontoref protocol.
# 2. Domain ontology — schemas and contracts the project defines for others to consume
# (domain_provides), or schemas the project imports from an upstream framework
# (domain_origin). These may be NCL-based or expressed in code (Rust traits, etc.).
#
# A project that only has self-ontology omits both fields.
# A project that is a framework defines domain_provides.
# A project that is an implementation defines domain_origin.
# Both fields are optional and independent.
let domain_ontology_kind_type = [|
'Ncl, # domain schemas are NCL contracts (importable via NICKEL_IMPORT_PATH)
'RustTrait, # domain contracts are Rust traits / type-system boundaries
'Mixed, # combination of NCL contracts and code-level contracts
'Implicit, # no formal schema; the domain is implicit in conventions and documentation
|] in
let domain_provides_type = {
# Canonical identifier for the domain this project defines.
id | String,
# Human-readable domain name.
name | String,
# Where the domain schemas / contracts live within this project.
schema_path | String | default = "ontology/schemas/",
# repo_kind expected for implementation projects consuming this domain.
impl_repo_kind | String | default = "",
# How the domain is expressed: NCL, RustTrait, Mixed, or Implicit.
kind | domain_ontology_kind_type | default = 'Ncl,
description | String | default = "",
} in
let domain_origin_type = {
# Identifier matching the upstream framework's domain_provides.id.
id | String,
# Human-readable name of the upstream framework / core project.
name | String,
# Local filesystem path to the upstream framework project root.
# Used to resolve NICKEL_IMPORT_PATH entries and CLI navigation.
path | String | default = "",
# Which schema stems from the upstream domain are imported/extended.
# Empty means the project inherits the full domain surface.
extends | Array String | default = [],
# If true, this project's self-ontology (.ontology/) IS the domain implementation.
# If false, the domain schemas are consumed but the self-ontology is independent.
integrated | Bool | default = true,
} in
# ── Root manifest ───────────────────────────────────────────────────────────
let manifest_type = {
@ -322,6 +373,11 @@ let manifest_type = {
requirements | Array requirement_type | default = [],
# External dependencies with documented failure blast radius.
critical_deps | Array critical_dep_type | default = [],
# Domain ontology layer — optional, independent of self-ontology (.ontology/).
# Set when this project IS a domain framework defining schemas for others.
domain_provides | domain_provides_type | optional,
# Set when this project IS an implementation extending an upstream domain framework.
domain_origin | domain_origin_type | optional,
} in
{
@ -351,4 +407,7 @@ let manifest_type = {
Capability = capability_type,
Requirement = requirement_type,
CriticalDep = critical_dep_type,
DomainOntologyKind = domain_ontology_kind_type,
DomainProvides = domain_provides_type,
DomainOrigin = domain_origin_type,
}

View file

@ -0,0 +1,47 @@
#!/usr/bin/env nu
# reflection/bin/init-repo.nu — VCS-aware repository initialization.
#
# Called by the new_project mode init_repo step.
# Detects existing VCS state and initializes accordingly:
# jj detected → jj git init --colocate (jj-native with git interop)
# git detected → commit initial empty commit if needed
# none detected → default to jj colocated (preferred for Radicle)
#
# Usage: nu reflection/bin/init-repo.nu <project_dir>
def main [project_dir: path]: nothing -> nothing {
let dir = ($project_dir | path expand)
if not ($dir | path exists) {
mkdir $dir
}
let has_jj = ($dir | path join ".jj" | path exists)
let has_git = ($dir | path join ".git" | path exists)
if $has_jj {
# Already a jj repo — nothing to do
print $" ($dir) already a jj repo"
return
}
if $has_git {
# Existing git repo — ensure at least one commit exists for worktree ops
let log = do { ^git -C $dir log --oneline -1 } | complete
if $log.exit_code != 0 or ($log.stdout | str trim | is-empty) {
std fs write-all $"($dir)/README" ""
do { ^git -C $dir add README } | complete | ignore
do { ^git -C $dir commit --allow-empty -m "chore: initial commit" } | complete | ignore
}
print $" ($dir) git repo ready"
return
}
# No VCS — initialize jj colocated (creates both .jj/ and .git/)
let r = do { ^jj git init --colocate $dir } | complete
if $r.exit_code != 0 {
error make { msg: $"init-repo: jj git init failed: ($r.stderr)" }
}
print $" ($dir) initialized as jj colocated repo"
}

View file

@ -0,0 +1,164 @@
#!/usr/bin/env nu
# reflection/bin/jjw-ncl-merge.nu — 3-way NCL merge driver for jj resolve.
#
# jj calls this as:
# nu jjw-ncl-merge.nu <left> <right> <base> <output>
#
# Register as a jj merge tool in ~/.config/jj/config.toml:
# [merge-tools.ncl]
# program = "nu"
# args = ["/path/to/reflection/bin/jjw-ncl-merge.nu", "$left", "$right", "$base", "$output"]
#
# Usage from jjw:
# jj resolve --tool ncl -- .ontology/core.ncl
#
# Strategy (two-layer):
# 1. git merge-file — 3-way text merge. Handles disjoint record additions
# without parsing NCL: side A adds node_x, side B adds node_y, both
# added to the same record → clean text merge.
# 2. nickel export — validates the text-merged result is legal NCL.
# Catches pathological cases where text merge succeeded but produced
# structurally invalid NCL (e.g., duplicate field names in different
# positions, mismatched let-bindings).
#
# Exit 0: $output written with merged content.
# Exit 1: auto-merge not possible — jj will fall back to manual resolution.
#
# Requires: git, nickel in PATH.
# ONTOREF_ROOT or ONTOREF_PROJECT_ROOT must be set (provides --import-path
# so nickel can locate schema.ncl and other imports).
const ONLY_ONTOLOGY_FILES = [
"core.ncl",
"state.ncl",
"gate.ncl",
"manifest.ncl",
]
def main [
left: path, # jj "ours" side (current workspace change)
right: path, # jj "theirs" side (the other side being merged)
base: path, # jj common ancestor
output: path, # jj expects the resolved content here
]: nothing -> nothing {
let left_name = ($left | path basename)
let right_name = ($right | path basename)
# ── Verify both sides are valid NCL before attempting merge ──────────────
let import_path = resolve-import-path $left
let check_left = (validate-ncl $left $import_path)
let check_right = (validate-ncl $right $import_path)
if not $check_left.ok {
error make {
msg: $"left side is not valid NCL:\n($check_left.err)"
}
}
if not $check_right.ok {
error make {
msg: $"right side is not valid NCL:\n($check_right.err)"
}
}
# ── Layer 1: git merge-file (3-way text merge) ───────────────────────────
# --stdout writes result to stdout; none of the input files are modified.
# Exits 0 = clean, 1 = conflict markers present, >1 = error.
let merge_r = (do {
^git merge-file --stdout ($left) ($base) ($right)
} | complete)
if $merge_r.exit_code > 1 {
error make {
msg: $"git merge-file error (exit ($merge_r.exit_code)): ($merge_r.stderr | str trim)"
}
}
if $merge_r.exit_code == 1 {
let conflicts = (count-conflict-markers $merge_r.stdout)
error make {
msg: $"text-level merge has ($conflicts) conflict(s) — manual resolution required.\nUse: jj resolve -r @ -- <file>"
}
}
let merged_text = $merge_r.stdout
# ── Layer 2: validate merged NCL ─────────────────────────────────────────
# Write to a temp file so nickel can read it; defer ensures cleanup even
# if nickel export throws or the process is interrupted.
let tmp_merged = (mktemp --suffix ".ncl")
defer { rm -f $tmp_merged }
$merged_text | save --force $tmp_merged
let validate = (validate-ncl $tmp_merged $import_path)
if not $validate.ok {
error make {
msg: $"text merge succeeded but result is invalid NCL:\n($validate.err)\nManual resolution required."
}
}
# ── Write output ─────────────────────────────────────────────────────────
$merged_text | save --force $output
print $" NCL auto-merge: ($left_name) ← ($right_name) [text+NCL clean]"
}
# Resolve the nickel --import-path from the file location or env.
#
# jj places its temp merge files somewhere under .jj/rewrite/ — they retain
# the original filename but the directory is different. We resolve the import
# path from env vars (set by ontoref sessions) or fall back to cwd.
def resolve-import-path [file: path]: nothing -> string {
let from_env = (
$env.ONTOREF_PROJECT_ROOT? | default (
$env.ONTOREF_ROOT? | default ""
)
)
if ($from_env | is-not-empty) { return $from_env }
# Best-effort: walk up from the file's real path looking for .ontology/
let mut dir = ($file | path expand | path dirname)
for _ in 0..8 {
if ($dir | path join ".ontology" | path exists) { return $dir }
let parent = ($dir | path dirname)
if $parent == $dir { break } # reached filesystem root
$dir = $parent
}
pwd | path expand
}
# Run nickel export on a file, return {ok: bool, err: string}.
def validate-ncl [file: path, import_path: string]: nothing -> record {
let r = (do {
^nickel export --import-path $import_path $file
} | complete)
if $r.exit_code == 0 {
{ ok: true, err: "" }
} else {
{ ok: false, err: ($r.stderr | str trim) }
}
}
# Count jj/git conflict markers in merged text.
def count-conflict-markers [text: string]: nothing -> int {
$text | lines | where { |l| ($l | str starts-with "<<<<<<<") } | length
}
#[test]
def test-conflict-marker-count [] {
let text = "<<<<<<< left\nfoo\n=======\nbar\n>>>>>>> right\n"
assert equal (count-conflict-markers $text) 1
}
#[test]
def test-conflict-marker-count-zero [] {
let text = "let s = import \"schema.ncl\" in\n{ node_a = {} }\n"
assert equal (count-conflict-markers $text) 0
}

323
reflection/bin/jjw.nu Normal file
View file

@ -0,0 +1,323 @@
#!/usr/bin/env nu
# reflection/bin/jjw.nu — jj + ontoref + rad orchestration for agent workspaces.
#
# Wraps three CLIs (jj, ontoref, rad) into a coherent agent lifecycle:
# jjw agent create → workspace + ontoref run
# jjw agent status → list active workspaces and their run state
# jjw agent step → report a step in the ontoref run
# jjw agent publish → validate + push + open Radicle patch
# jjw agent merge → merge commit + advance main + cleanup
# jjw agent discard → abandon workspace and cancel run
#
# Requires: jj >= 0.40, ontoref in PATH, rad >= 1.0 (optional for publish)
#
# NCL auto-merge for .ontology/ conflicts:
# Register jjw-ncl-merge.nu as a jj merge tool in ~/.config/jj/config.toml:
# [merge-tools.ncl]
# program = "nu"
# args = ["{ONTOREF_ROOT}/reflection/bin/jjw-ncl-merge.nu", "$left", "$right", "$base", "$output"]
# Then resolve: jj resolve --tool ncl -- .ontology/core.ncl
#
# .ontoref-run metadata file lives in each workspace dir:
# .agents/<run_id>/.ontoref-run
#
# ONTOREF_ROOT must be set (done by `use env.nu *` in ontoref sessions).
# When called standalone, set ONTOREF_ROOT to the ontoref installation path.
const ONTOREF_RUN_FILE = ".ontoref-run"
const AGENTS_DIR = ".agents"
# ── Helpers ──────────────────────────────────────────────────────────────────
def project-root []: nothing -> string {
let pr = ($env.ONTOREF_PROJECT_ROOT? | default "")
if ($pr | is-not-empty) { $pr } else { pwd | path expand }
}
def agents-dir []: nothing -> string {
$"(project-root)/($AGENTS_DIR)"
}
def workspace-path [run_id: string]: nothing -> string {
$"(agents-dir)/($run_id)"
}
def run-file [run_id: string]: nothing -> string {
$"(workspace-path $run_id)/($ONTOREF_RUN_FILE)"
}
def ontoref-bin []: nothing -> string {
let root = ($env.ONTOREF_ROOT? | default "")
if ($root | is-not-empty) {
let p = $"($root)/ontoref"
if ($p | path exists) { return $p }
}
# Fall back to PATH
let which_r = do { ^which ontoref } | complete
if $which_r.exit_code == 0 { $which_r.stdout | str trim } else { "ontoref" }
}
def jj [args: list<string>]: nothing -> string {
let root = (project-root)
let r = do { ^jj --no-pager --repository $root ...$args } | complete
if $r.exit_code != 0 {
error make { msg: $"jj ($args | str join ' '): ($r.stderr | str trim)" }
}
$r.stdout
}
def onto [args: list<string>]: nothing -> string {
let bin = (ontoref-bin)
let r = do { ^nu $bin ...$args } | complete
if $r.exit_code != 0 {
error make { msg: $"ontoref ($args | str join ' '): ($r.stderr | str trim)" }
}
$r.stdout
}
def read-run-file [run_id: string]: nothing -> record {
let f = (run-file $run_id)
if not ($f | path exists) {
error make { msg: $"No .ontoref-run for workspace '($run_id)'" }
}
open $f | from json
}
def get-change-id [run_id: string]: nothing -> string {
let ws_name = $run_id
jj ["log", "--no-graph", "-r", $"($ws_name)@", "-T", "change_id ++ \"\\n\""] | str trim
}
def has-rad-remote []: nothing -> bool {
let r = do { ^jj --no-pager git remote list } | complete
if $r.exit_code != 0 { return false }
$r.stdout | lines | any { |l| ($l | str trim | str starts-with "rad") }
}
# ── Commands ─────────────────────────────────────────────────────────────────
# Create a new agent workspace tied to an ontoref run.
export def "agent create" [
agent_id: string, # Logical agent identifier (e.g. "architect-01")
--task (-t): string = "", # Backlog task ID to associate
--mode (-m): string = "sync-ontology",
]: nothing -> nothing {
let root = (project-root)
let agents = (agents-dir)
mkdir $agents
# Start ontoref run — get run_id from JSON output
let run_json = (onto ["run", "start", $mode, "--task", $task, "--fmt", "json"])
let run_data = ($run_json | from json)
let run_id = $run_data.run_id
let ws_path = (workspace-path $run_id)
let ws_path_str = $ws_path
# jj workspace add — workspace name = run_id (unique UUID-like string)
jj ["workspace", "add", $ws_path_str, "--name", $run_id] | ignore
let change_id = (get-change-id $run_id)
# Write .ontoref-run metadata
let meta = {
run_id: $run_id,
agent_id: $agent_id,
mode: $mode,
task: $task,
workspace_name: $run_id,
workspace_path: $ws_path_str,
change_id: $change_id,
created_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ"),
remote: (if (has-rad-remote) { "rad" } else { "origin" }),
}
$meta | to json | save --force (run-file $run_id)
print $" workspace: ($ws_path_str)"
print $" run_id: ($run_id)"
print $" change_id: ($change_id)"
}
# List active agent workspaces and their ontoref run state.
export def "agent status" [
agent_id?: string, # Filter by agent_id (optional)
]: nothing -> nothing {
let agents = (agents-dir)
if not ($agents | path exists) {
print " no active agent workspaces"
return
}
let entries = (ls $agents | where type == "dir")
if ($entries | is-empty) {
print " no active agent workspaces"
return
}
let rows = $entries | each { |e|
let run_id = ($e.name | path basename)
let rf = $"($e.name)/($ONTOREF_RUN_FILE)"
if not ($rf | path exists) { return null }
let meta = (open $rf | from json)
if ($agent_id | is-not-empty) and ($meta.agent_id != $agent_id) { return null }
let change_id = (
do { jj ["log", "--no-graph", "-r", $"($run_id)@", "-T", "change_id ++ \"\\n\""] }
| complete | if $in.exit_code == 0 { $in.stdout | str trim } else { "unknown" }
)
{
run_id: $run_id,
agent_id: $meta.agent_id,
mode: $meta.mode,
task: $meta.task,
change_id: $change_id,
created: $meta.created_at,
}
} | where { |r| $r != null }
if ($rows | is-empty) {
print $" no workspaces for agent ($agent_id? | default "any")"
} else {
print ($rows | table)
}
}
# Report a step in the ontoref run for an agent workspace.
export def "agent step" [
run_id: string, # Workspace / run ID
step_id: string, # Step to report
--status (-s): string = "", # pass | fail | skip
]: nothing -> nothing {
if ($status | is-empty) or not ($status in ["pass", "fail", "skip"]) {
error make { msg: $"--status required: pass | fail | skip (got: '($status)')" }
}
let meta = (read-run-file $run_id)
onto ["step", "report", $meta.mode, $step_id, "--status", $status] | ignore
print $" ($run_id) step ($step_id) → ($status)"
}
# Validate, push, and open a Radicle patch (or git push) for an agent workspace.
export def "agent publish" [
run_id: string,
]: nothing -> nothing {
let meta = (read-run-file $run_id)
# 1. Ontology drift check
onto ["sync", "diff", "--fail-on-drift"] | ignore
# 2. Constraint validation
onto ["validate", "check-all"] | ignore
# 3. Verify all mode steps are reported
onto ["mode", "complete", $meta.mode] | ignore
# 4. Set commit description
let desc = $"agent(($meta.agent_id)): ($meta.task)"
jj ["describe", "-m", $desc, "-r", $meta.change_id] | ignore
# 5. Create/update bookmark — `set` is idempotent (safe on re-push after amend)
let bookmark = $"agent/($meta.agent_id)/($run_id)"
jj ["bookmark", "set", $bookmark, "-r", $meta.change_id] | ignore
# 6. Push
let remote = $meta.remote
jj ["git", "push", "--remote", $remote, "--bookmark", $bookmark] | ignore
# 7. Open Radicle patch only if rad remote is configured
if $remote == "rad" {
let r = do { ^rad patch open --title $desc } | complete
if $r.exit_code != 0 {
print $" warning: rad patch open failed: ($r.stderr | str trim)"
} else {
print $" patch opened on Radicle"
}
} else {
print $" pushed to ($remote) — no Radicle remote, skipped rad patch open"
let modified = (jj ["diff", "--name-only", "-r", $meta.change_id])
if ($modified | lines | any { |l| $l | str starts-with ".ontology/" }) {
print $" WARNING: workspace modified .ontology/ files but remote is git-only."
print $" merge may be rejected by deny_ontology_writes on the server."
}
}
print $" published: ($bookmark) → ($remote)"
}
# Merge workspace into main, advance bookmark, and clean up.
export def "agent merge" [
run_id: string,
]: nothing -> nothing {
let meta = (read-run-file $run_id)
let change_id = $meta.change_id
# 1. Create merge commit (no-edit — doesn't touch working copies)
jj ["new", "--no-edit", "main", $change_id] | ignore
# 2. Find the merge commit: heads of change_id's descendants, excluding
# change_id itself. `heads(change_id::)` alone can match change_id if it
# is a head in the workspace — `~ $change_id` excludes the original commit.
let merge_revset = $"heads(($change_id)::) ~ ($change_id)"
# 3. Check for conflicts — only block on .ontology/ conflicts
let conflict_out = (jj ["log", "--no-graph", "-r", $merge_revset,
"-T", "if(conflict, \"CONFLICT\", \"CLEAN\") ++ \"\\n\""])
if ($conflict_out | str trim) == "CONFLICT" {
let files_out = (
do { jj ["diff", "--name-only", "-r", $merge_revset] }
| complete | get -o result | default ""
)
let onto_conflicts = ($files_out | lines | where { |l| $l | str starts-with ".ontology/" })
if ($onto_conflicts | is-not-empty) {
# Abandon the conflicted merge commit before aborting
do { jj ["abandon", $merge_revset] } | complete | ignore
error make {
msg: $".ontology/ conflicts require NCL merge resolution: ($onto_conflicts | str join ', ')"
}
}
print $" WARNING: merge commit has non-ontology conflicts (stored as jj data)"
print $" use `jj resolve -r ($merge_revset)` in workspace to clean up"
}
# 4. Advance main bookmark to the merge commit
jj ["bookmark", "set", "main", "-r", $merge_revset] | ignore
# 5. Dispatch VCS event so ontoref syncs
let hooks = $"($env.ONTOREF_ROOT? | default '')/reflection/hooks/git-event.nu"
if ($hooks | path exists) {
do { ^nu -c $"use ($hooks) *; on-vcs-event 'post-merge'" } | complete | ignore
}
# 6. Forget workspace and remove directory
do { ^jj --no-pager workspace forget $run_id } | complete | ignore
let ws_path = (workspace-path $run_id)
if ($ws_path | path exists) {
rm -rf $ws_path
}
print $" merged ($run_id) into main, workspace removed"
}
# Abandon an agent workspace without merging. Cancels the ontoref run.
export def "agent discard" [
run_id: string,
]: nothing -> nothing {
# Forget from jj
do { ^jj --no-pager workspace forget $run_id } | complete | ignore
# Remove workspace directory
let ws_path = (workspace-path $run_id)
if ($ws_path | path exists) {
rm -rf $ws_path
}
# Cancel the ontoref run (best-effort — run cancel may not exist in all versions)
let r = do { onto ["run", "cancel", $run_id] } | complete
if $r.exit_code != 0 {
print $" advisory: ontoref run cancel returned non-zero (run may already be complete)"
}
print $" discarded workspace ($run_id)"
}

View file

@ -144,6 +144,46 @@ def "main help" [...args: string] {
fmt-cmd $"($cmd) help search" "ontology search + bookmarks (NCL-persisted)"
fmt-cmd $"($cmd) help qa" "Q&A knowledge base: query, add, list"
fmt-cmd $"($cmd) help log" "action audit trail, follow, filter"
# Active domain extension (conditional on project repo_kind)
let proj_root = ($env.ONTOREF_PROJECT_ROOT? | default "")
let ore_root = ($env.ONTOREF_ROOT? | default "")
if ($proj_root | is-not-empty) and ($ore_root | is-not-empty) {
let manifest = $"($proj_root)/.ontology/manifest.ncl"
if ($manifest | path exists) {
let rk_result = (do { ^nickel export $manifest } | complete)
if $rk_result.exit_code == 0 {
let rk = ($rk_result.stdout | from json | get repo_kind? | default "")
if ($rk | is-not-empty) {
let domains_dir = $"($ore_root)/domains"
if ($domains_dir | path exists) {
let matched = (
ls $domains_dir | where type == "dir" | get name
| each { |d|
let rk_file = $"($d)/repo_kinds.txt"
if ($rk_file | path exists) and ((open --raw $rk_file | lines | any { |l| ($l | str trim) == $rk })) { $d | path basename } else { null }
}
| compact
| if ($in | is-empty) { null } else { $in | first }
)
if ($matched | is-not-empty) {
let d_result = (do { ^nickel export $"($domains_dir)/($matched)/domain.ncl" } | complete)
if $d_result.exit_code == 0 {
let d = ($d_result.stdout | from json)
let alias = ($d.short_alias? | default "")
let alias_hint = if ($alias | is-not-empty) { $" \(alias: ($alias)\)" } else { "" }
print ""
fmt-section $"DOMAIN EXTENSION ($d.name)($alias_hint)"
print ""
fmt-cmd $"($cmd) ($matched) <command>" $"($d.description)"
fmt-cmd $"($cmd) help ($matched)" "show domain command reference"
}
}
}
}
}
}
}
print ""
fmt-section "QUICK REFERENCE"
@ -186,7 +226,7 @@ def "main help" [...args: string] {
print $" (ansi cyan)mf(ansi reset) → manifest (ansi cyan)dg(ansi reset) → diagram (ansi cyan)md(ansi reset) → mode (ansi cyan)st(ansi reset) → status"
print $" (ansi cyan)fm(ansi reset) → form (ansi cyan)s(ansi reset) → search (ansi cyan)ru(ansi reset) → run \(mode\) (ansi cyan)sv(ansi reset) → services"
print $" (ansi cyan)nv(ansi reset) → nats (ansi cyan)q(ansi reset) → qa query (ansi cyan)f(ansi reset) → search \(alias\) (ansi cyan)df(ansi reset) → describe diff"
print $" (ansi cyan)da(ansi reset) → describe api (ansi cyan)dst(ansi reset) → describe state (ansi cyan)dws(ansi reset) → describe workspace"
print $" (ansi cyan)da(ansi reset) → describe api (ansi cyan)dst(ansi reset) → describe state (ansi cyan)dws(ansi reset) → describe workspace (ansi cyan)ddom(ansi reset) → describe domain"
print ""
print $" (ansi dark_gray)Tip: any group accepts(ansi reset) (ansi cyan)h(ansi reset) (ansi dark_gray)for help,(ansi reset) (ansi cyan)?(ansi reset) (ansi dark_gray)for interactive selector, or bare for picker(ansi reset)"
print $" (ansi dark_gray)Any command:(ansi reset) (ansi cyan)--fmt|-f(ansi reset) (ansi dark_gray)text*|json|yaml|toml|md(ansi reset) · (ansi cyan)--clip(ansi reset) (ansi dark_gray)copy output to clipboard(ansi reset)"
@ -428,6 +468,10 @@ def "main sync audit" [--fmt (-f): string = "", --strict, --quick] {
sync audit --fmt $f --strict=$strict --quick=$quick
}
def "main sync watch" [] { log-action "sync watch" "read"; sync watch }
def "main sync manifest-check" [--strict] {
log-action "sync manifest-check" "read"
sync manifest-check --strict=$strict
}
# ── Coder ─────────────────────────────────────────────────────────────────────
@ -587,6 +631,12 @@ def "main describe guides" [--fmt (-f): string = "", --actor: string = ""] {
describe guides --fmt $fmt --actor $actor
}
def "main describe domain" [--fmt (-f): string = ""] {
log-action "describe domain" "read"
let f = (resolve-fmt $fmt [text json])
describe domain --fmt $f
}
# ── Diagram ───────────────────────────────────────────────────────────────────
def "main diagram" [] {
@ -749,11 +799,14 @@ def "main d workspace" [--fmt (-f): string = "", --actor: string = ""] { main de
def "main d ws" [--fmt (-f): string = "", --actor: string = ""] { main describe workspace --fmt $fmt --actor $actor }
def "main d guides" [--fmt (-f): string = "", --actor: string = ""] { main describe guides --fmt $fmt --actor $actor }
def "main d g" [--fmt (-f): string = "", --actor: string = ""] { main describe guides --fmt $fmt --actor $actor }
def "main d domain" [--fmt (-f): string = ""] { main describe domain --fmt $fmt }
def "main d dom" [--fmt (-f): string = ""] { main describe domain --fmt $fmt }
def "main df" [--fmt (-f): string = "", --file: string = ""] { main describe diff --fmt $fmt --file $file }
def "main da" [--actor: string = "", --tag: string = "", --auth: string = "", --fmt (-f): string = ""] { main describe api --actor $actor --tag $tag --auth $auth --fmt $fmt }
def "main dst" [id?: string, --fmt (-f): string = "", --actor: string = ""] { main describe state $id --fmt $fmt --actor $actor }
def "main dws" [--fmt (-f): string = "", --actor: string = ""] { main describe workspace --fmt $fmt --actor $actor }
def "main ddom" [--fmt (-f): string = ""] { main describe domain --fmt $fmt }
def "main bkl" [action?: string] { main backlog $action }
def "main bkl help" [] { help-group "backlog" }

View file

@ -1,39 +1,52 @@
#!/usr/bin/env nu
# reflection/hooks/git-event.nu — called by post-merge and post-checkout hooks.
# reflection/hooks/git-event.nu — called by git post-merge and post-checkout hooks.
#
# Delegates entirely to the ontoref dispatcher so all mode detection,
# transition, and sync logic lives in one place.
# Never fails the git operation — all errors are best-effort.
# For jj repos, this hook is never installed (jj has no hook system).
# The jjw wrapper calls `on-vcs-event` directly after relevant jj operations.
def main [event: string = ""] {
let ontoref_root = ($env.ONTOREF_ROOT? | default "")
if ($ontoref_root | is-empty) { return }
# Core event handler — callable from both the git hook and the jjw wrapper.
#
# Triggers: mode detection, project-context regeneration, daemon sync.
# Never fails the calling operation — all steps are best-effort.
export def "on-vcs-event" [event_type: string = ""]: nothing -> nothing {
let ontoref_root = ($env.ONTOREF_ROOT? | default "")
if ($ontoref_root | is-empty) { return }
let wrapper = $"($ontoref_root)/ontoref"
if not ($wrapper | path exists) { return }
let wrapper = $"($ontoref_root)/ontoref"
if not ($wrapper | path exists) { return }
# Trigger mode detection + transition. Sync runs inside on-enter-daemon if mode changed.
do { ^nu $wrapper mode-detect } | complete | null
# Trigger mode detection + transition. Sync runs inside on-enter-daemon if mode changed.
do { ^nu $wrapper mode-detect } | complete | null
# Regenerate static project context for Claude Code sessions.
let context_file = $"($ontoref_root)/.claude/project-context.md"
if ($context_file | path parent | path exists) {
let r = (do { ^nu $wrapper describe project --fmt text } | complete)
if $r.exit_code == 0 {
$r.stdout | save --force $context_file
# Regenerate static project context for Claude Code sessions.
let context_file = $"($ontoref_root)/.claude/project-context.md"
if ($context_file | path parent | path exists) {
let r = (do { ^nu $wrapper describe project --fmt text } | complete)
if $r.exit_code == 0 {
$r.stdout | save --force $context_file
}
}
}
# If daemon mode is active, push ontology for current git event.
let lock_file = $"($ontoref_root)/.ontoref/mode.lock"
if not ($lock_file | path exists) { return }
# If daemon mode is active, push ontology for current VCS event.
let lock_file = $"($ontoref_root)/.ontoref/mode.lock"
if not ($lock_file | path exists) { return }
let lock = (do { open $lock_file | from json } | complete | get -o stdout | default { mode: "local" })
if $lock.mode == "daemon" {
let store = $"($ontoref_root)/reflection/modules/store.nu"
let r = (do { ^nu -c $"use ($store) *; store sync-push" } | complete)
if $r.exit_code == 0 {
print $" (ansi green)ontoref(ansi reset) ontology synced to daemon"
let lock = (
do { open $lock_file | from json } | complete
| get -o stdout
| default { mode: "local" }
)
if $lock.mode == "daemon" {
let store = $"($ontoref_root)/reflection/modules/store.nu"
let r = (do { ^nu -c $"use ($store) *; store sync-push" } | complete)
if $r.exit_code == 0 {
print $" (ansi green)ontoref(ansi reset) ontology synced to daemon"
}
}
}
}
# Git hook entry point — called by post-merge / post-checkout.
def main [event: string = ""]: nothing -> nothing {
on-vcs-event $event
}

View file

@ -3,9 +3,9 @@
slug = "just-convention",
description = "Justfile validates against the canonical module convention",
check = {
tag = "NuCmd",
cmd = "cd $env.ONTOREF_PROJECT_ROOT; $env.ONTOREF_ACTOR = \"agent\"; let r = (do { ^ontoref validate justfile --fmt json } | complete); if $r.exit_code != 0 { exit 1 }; let result = ($r.stdout | from json); if $result.ok { exit 0 } else { exit 1 }",
expect_exit = 0,
tag = "Grep",
pattern = "project_root\\s*:=",
paths = ["justfile"],
},
instructions = "
Run the validator to see what is missing:

View file

@ -16,7 +16,7 @@ Add to .pre-commit-config.yaml in the local hooks section:
- id: manifest-coverage
name: Manifest capability completeness
entry: bash -c 'ONTOREF_ROOT=\"$(pwd)\" ONTOREF_PROJECT_ROOT=\"$(pwd)\" nu --no-config-file -c \"use ./reflection/modules/sync.nu *; sync manifest-check\"'
entry: bash -c 'ONTOREF_PROJECT_ROOT=\"$(pwd)\" ontoref sync manifest-check'
language: system
files: (\\.ontology/|reflection/modes/|reflection/forms/).*\\.ncl$
pass_filenames: false
@ -30,8 +30,8 @@ It blocks on Hard failures (no capabilities declared at all) and warns on Soft i
Add to .claude/hooks/session-context.sh before the final line:
# quick manifest/ontology health check
HEALTH_OUT=$(cd \"$REPO_ROOT\" && ONTOREF_ROOT=\"$REPO_ROOT\" ONTOREF_PROJECT_ROOT=\"$REPO_ROOT\" \\
nu --no-config-file -c 'use ./reflection/modules/sync.nu *; sync manifest-check' 2>/dev/null || true)
HEALTH_OUT=$(cd \"$REPO_ROOT\" && ONTOREF_PROJECT_ROOT=\"$REPO_ROOT\" \\
ontoref sync manifest-check 2>/dev/null || true)
if [[ -n \"$HEALTH_OUT\" ]]; then
echo \"Manifest Coverage\"

View file

@ -0,0 +1,26 @@
{
id = "0013",
slug = "domain-extension-install",
description = "Domain extension system requires ontoref >= session-2026-04-06. The installer copies domains/ to $data_dir and generates bin wrappers for short aliases (personal, prov, fw). Consumer projects with matching repo_kind gain ore <domain-id> commands and ore help <domain-id> reference.",
check = {
tag = "NuCmd",
cmd = "let ore_root = ($env.ONTOREF_ROOT? | default \"\"); if ($ore_root | is-empty) { exit 1 }; let domains = $\"($ore_root)/domains\"; if not ($domains | path exists) { exit 1 }; let has_personal = ($\"($domains)/personal/commands.nu\" | path exists); let has_prov = ($\"($domains)/provisioning/commands.nu\" | path exists); let has_fw = ($\"($domains)/framework/commands.nu\" | path exists); if $has_personal and $has_prov and $has_fw { exit 0 } else { exit 1 }",
expect_exit = 0,
},
instructions = "
Reinstall ontoref to get the domain extension system:
nu install/install.nu
This copies domains/ to $ONTOREF_ROOT/domains/ and creates:
- $HOME/.local/bin/personal → ontoref personal (PersonalOntology)
- $HOME/.local/bin/prov → ontoref provisioning (DevWorkspace, Mixed)
- $HOME/.local/bin/fw → ontoref framework (Library, Service, Tool)
- $ONTOREF_ROOT/domains/aliases.txt (prov=provisioning, fw=framework)
After reinstall, verify with:
ore help personal # PersonalOntology projects
ore help provisioning # DevWorkspace / Mixed projects
ore help framework # Library / Service / Tool projects
",
}

View file

@ -53,8 +53,8 @@ let s = import "../schema.ncl" in
id = "extract-branch",
action = "Get current branch name for PR title fallback",
actor = 'Agent,
cmd = "git rev-parse --abbrev-ref HEAD",
verify = "git rev-parse --abbrev-ref HEAD | grep -qv 'main\\|master'",
cmd = "nu -c 'use {ontoref_dir}/reflection/modules/vcs.nu; vcs current-ref'",
verify = "nu -c 'use {ontoref_dir}/reflection/modules/vcs.nu; let ref = (vcs current-ref); if $ref in [\"main\", \"master\"] { exit 1 }'",
depends_on = [{ step = "check-auth", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
},

View file

@ -40,7 +40,7 @@ d.make_mode String {
{
id = "resolve_career_trajectory",
action = "From state.ncl career dimension, extract current_state → desired_state trajectory and its active blockers/catalysts. This becomes the 'why now' and 'where I am going' section of the application.",
cmd = "nickel export .ontology/state.ncl | from json | get dimensions | where { |d| $d.id == 'career }",
cmd = "nickel export .ontology/state.ncl | from json | get dimensions | where { |d| $d.id == \"career\" }",
actor = 'Agent,
depends_on = [{ step = "select_narrative_nodes" }],
on_error = { strategy = 'Continue },

View file

@ -26,7 +26,7 @@ let s = import "../schema.ncl" in
id = "init_repo",
action = "initialize_git_repository",
actor = 'Both,
cmd = "git -C {project_dir} init && git -C {project_dir} commit --allow-empty -m 'chore: initial commit'",
cmd = "nu {ontoref_dir}/reflection/bin/init-repo.nu {project_dir}",
depends_on = [],
on_error = { strategy = 'Stop },
},

View file

@ -20,7 +20,7 @@ d.make_mode String {
{
id = "select_projects",
action = "From core.ncl Project nodes, select those relevant to the target context. Relevance is determined by: (1) node pole alignment with context (Yang for engineering roles, Yin for research/creative), (2) artifact_paths showing real artifacts, (3) edges showing which practices they validate.",
cmd = "nickel export .ontology/core.ncl | from json | get nodes | where { |n| $n.level == 'Project }",
cmd = "nickel export .ontology/core.ncl | from json | get nodes | where { |n| $n.level == \"Project\" }",
actor = 'Both,
depends_on = [{ step = "resolve_context" }],
on_error = { strategy = 'Stop },
@ -35,7 +35,7 @@ d.make_mode String {
{
id = "resolve_career_state",
action = "Export state.ncl and read the career dimension: current_state, desired_state, and active transitions. This informs the CV narrative arc — what you are moving toward, not just what you have done.",
cmd = "nickel export .ontology/state.ncl | from json | get dimensions | where { |d| $d.id == 'career }",
cmd = "nickel export .ontology/state.ncl | from json | get dimensions | where { |d| $d.id == \"career\" }",
actor = 'Agent,
depends_on = [{ step = "resolve_context" }],
on_error = { strategy = 'Continue },

View file

@ -21,7 +21,7 @@ d.make_mode String {
{
id = "resolve_conference",
action = "Load the target Opportunity node from .ontology/personal.ncl. Note: name, deadline, fit_signals, and note field. The fit_signals should map to gate.ncl signal types that are currently active.",
cmd = "nickel export .ontology/personal.ncl | from json | get opportunities | where { |o| $o.kind == 'Conference }",
cmd = "nickel export .ontology/personal.ncl | from json | get opportunities | where { |o| $o.kind == \"Conference\" }",
actor = 'Agent,
depends_on = [{ step = "resolve_talk_node" }],
on_error = { strategy = 'Stop },

View file

@ -3,6 +3,7 @@
use env.nu *
use store.nu [daemon-export-safe]
use ./vcs.nu
export def "backlog list" [
--status: string = "",
@ -302,7 +303,7 @@ def backlog-set-status [id: string, new_status: string] {
let tc = do { ^nickel typecheck $file } | complete
if $tc.exit_code != 0 {
print " error: typecheck failed — reverting"
do { ^git checkout -- $file } | complete | ignore
vcs restore-file $file
return
}
print $" ($id) → ($new_status)"

View file

@ -40,6 +40,7 @@ export def nickel-import-path [root: string]: nothing -> string {
}
use ../modules/store.nu [daemon-export-safe]
use ./vcs.nu
# Centralized output dispatcher for all describe commands.
# Handles text (via render callback), json, yaml, toml, table.
@ -114,6 +115,7 @@ export def "describe capabilities" [
let adrs = (collect-adr-summary $root)
let api_routes = (load-api-catalog-static $root $a)
let feature_flags = (collect-cargo-features $root)
let domain_extension = (resolve-domain-extension $root)
let data = {
project_flags: $project_flags,
@ -130,6 +132,7 @@ export def "describe capabilities" [
adrs: $adrs,
api_routes: $api_routes,
feature_flags: $feature_flags,
domain_extension: $domain_extension,
}
emit-output $data $f { || render-capabilities-text $data $a $root }
@ -1372,6 +1375,38 @@ export def "describe guides" [
migration_authoring: "ontoref migrate list | last — check last id, increment. Schema: { id, slug, description, check: {tag, ...}, instructions }. check.tag: FileExists | Grep | NuCmd. NuCmd must be valid Nushell — no bash operators.",
}
# Domain ontology layer — cross-project relationships from manifest
let d_orig = ($manifest.domain_origin? | default {})
let d_prov = ($manifest.domain_provides? | default {})
let has_orig_g = ($d_orig | is-not-empty) and ($d_orig.id? | is-not-empty)
let has_prov_g = ($d_prov | is-not-empty) and ($d_prov.id? | is-not-empty)
let impls_for_guides = if $has_prov_g {
find-domain-implementations ($d_prov.id? | default "")
} else { [] }
let orig_path_g = ($d_orig.path? | default "")
let fw_constraints_g = if $has_orig_g and ($orig_path_g | is-not-empty) {
load-framework-hard-constraints $orig_path_g
} else if $has_prov_g {
load-framework-hard-constraints $root
} else { [] }
let integrated_g = ($d_orig.integrated? | default true)
let contract_kind_g = ($d_prov.kind? | default ($d_orig.kind? | default "Ncl") | into string)
let advisory_g: string = if $has_orig_g and not $integrated_g {
$"EXTERNAL domain contract \(($contract_kind_g)\): .ontology/ is independent of the code-level contract. Verify all changes against framework_constraints before modifying the implementation layer."
} else if $has_orig_g and $integrated_g {
"INTEGRATED domain: this project's .ontology/ IS the domain implementation."
} else if $has_prov_g {
"FRAMEWORK: changes to the contract surface may affect registered implementations."
} else { "" }
let domain_layer = {
role: (if $has_prov_g { "framework" } else if $has_orig_g { "implementation" } else { "none" }),
domain_provides: $d_prov,
domain_origin: $d_orig,
implementations: $impls_for_guides,
framework_constraints: $fw_constraints_g,
agent_advisory: $advisory_g,
}
let data = {
identity: $identity,
axioms: $axioms,
@ -1391,6 +1426,7 @@ export def "describe guides" [
requirements: $manifest_requirements,
critical_deps: $manifest_critical_deps,
authoring_conventions: $authoring_conventions,
domain_layer: $domain_layer,
}
emit-output $data $f {||
@ -1484,6 +1520,178 @@ def render-api-text [data: record]: nothing -> nothing {
}
}
# ── describe domain ─────────────────────────────────────────────────────────────
# "What domain ontology relationships does this project participate in?"
# For framework projects: shows declared contract + all registered implementations.
# For implementation projects: shows the origin framework + navigation hints.
# Scans ~/.config/ontoref/projects.ncl to resolve cross-project relationships.
export def "describe domain" [
--fmt: string = "", # Output format: text* | json
--actor: string = "",
]: nothing -> nothing {
let root = (project-root)
let a = if ($actor | is-not-empty) { $actor } else { (actor-default) }
let f = if ($fmt | is-not-empty) { $fmt } else if $a == "agent" { "json" } else { "text" }
let manifest = (load-manifest-safe $root)
let d_orig = ($manifest.domain_origin? | default {})
let d_prov = ($manifest.domain_provides? | default {})
let has_orig = ($d_orig | is-not-empty) and ($d_orig.id? | is-not-empty)
let has_prov = ($d_prov | is-not-empty) and ($d_prov.id? | is-not-empty)
if not $has_orig and not $has_prov {
print $" (ansi yellow)No domain_origin or domain_provides declared in .ontology/manifest.ncl(ansi reset)"
return
}
let impls = if $has_prov {
find-domain-implementations ($d_prov.id? | default "")
} else { [] }
let orig_path = ($d_orig.path? | default "")
let origin_project: record = if $has_orig and ($orig_path | is-not-empty) and ($orig_path | path exists) {
let om = (load-manifest-safe $orig_path)
{
root: $orig_path,
name: ($om.name? | default ($d_orig.name? | default "")),
slug: ($om.slug? | default ($orig_path | path basename)),
}
} else { {} }
# Load framework hard constraints — essential for agents working on implementations.
# For framework projects: load from self. For implementations: load from origin.
let fw_constraints = if $has_orig and ($orig_path | is-not-empty) {
load-framework-hard-constraints $orig_path
} else if $has_prov {
load-framework-hard-constraints $root
} else { [] }
# Contract advisory for external (integrated = false) domain implementations.
# Agents need an explicit signal that .ontology/ is independent of the code contract.
let integrated = ($d_orig.integrated? | default true)
let contract_kind = ($d_prov.kind? | default ($d_orig.kind? | default "Ncl") | into string)
let agent_advisory: string = if $has_orig and not $integrated {
$"This project implements an EXTERNAL domain contract \(($contract_kind)\). The .ontology/ describes the project independently — it does NOT capture the code-level contract. Before modifying the implementation layer, cross-reference constraints from the framework ADRs listed in framework_constraints."
} else if $has_orig and $integrated {
"This project's .ontology/ IS the domain implementation (integrated = true). Ontology and implementation are the same layer."
} else if $has_prov {
"This project defines a domain contract. Changes to the contract surface may affect registered implementations."
} else { "" }
let data = {
role: (if $has_prov { "framework" } else { "implementation" }),
domain_provides: $d_prov,
domain_origin: $d_orig,
origin_project: $origin_project,
implementations: $impls,
framework_constraints: $fw_constraints,
agent_advisory: $agent_advisory,
}
emit-output $data $f { || render-domain-text $data $root }
}
def render-domain-text [data: record, root: string]: nothing -> nothing {
let sep = "──────────────────────────────────────────────────────────────────"
print ""
print "DOMAIN ONTOLOGY LAYER"
print "══════════════════════════════════════════════════════════════════"
# Agent advisory — always first when present
let advisory = ($data.agent_advisory? | default "")
if ($advisory | is-not-empty) {
print $"(ansi yellow_bold)AGENT ADVISORY(ansi reset)"
print $sep
print $" ($advisory)"
print ""
}
if $data.role == "framework" {
let prov = $data.domain_provides
let prov_id = ($prov.id? | default "")
let prov_nm = ($prov.name? | default "")
let prov_k = ($prov.kind? | default "Ncl" | into string)
print $" Role: (ansi cyan_bold)framework / provider(ansi reset)"
print $" Domain: (ansi white_bold)($prov_id)(ansi reset) ($prov_nm) [($prov_k)]"
let schema = ($prov.schema_path? | default "")
if ($schema | is-not-empty) { print $" Schemas: ($schema) ← contract surface for implementations" }
let impl_kind = ($prov.impl_repo_kind? | default "")
if ($impl_kind | is-not-empty) { print $" impl_for: ($impl_kind) repo_kind" }
let desc = ($prov.description? | default "")
if ($desc | is-not-empty) { print $" ($desc)" }
print ""
if ($data.implementations | is-empty) {
print $" (ansi dark_gray)No registered implementations found in ~/.config/ontoref/projects.ncl(ansi reset)"
} else {
print $"KNOWN IMPLEMENTATIONS ($data.implementations | length) registered"
print $sep
for impl in $data.implementations {
let integ = if $impl.integrated { "integrated" } else { $"(ansi yellow)external(ansi reset) — .ontology/ is independent of code contract" }
print $" (ansi green)($impl.slug)(ansi reset) ($impl.root)"
if ($impl.extends | is-not-empty) {
print $" extends: ($impl.extends | str join ', ')"
}
print $" type: ($integ)"
print $" navigate: ONTOREF_PROJECT_ROOT=($impl.root) ore describe project"
}
}
} else {
let orig = $data.domain_origin
let orig_id = ($orig.id? | default "")
let orig_nm = ($orig.name? | default "")
let integrated = ($orig.integrated? | default true)
let integ_str = if $integrated { "integrated" } else { $"(ansi yellow)external(ansi reset)" }
let orig_path = ($orig.path? | default "")
let ext = ($orig.extends? | default [])
print $" Role: (ansi cyan_bold)implementation(ansi reset)"
print $" Domain: (ansi white_bold)($orig_id)(ansi reset) ($orig_nm)"
print $" Type: ($integ_str)"
if ($ext | is-not-empty) {
print $" Extends: ($ext | str join ', ')"
}
print ""
print $"ORIGIN FRAMEWORK"
print $sep
if ($data.origin_project | is-empty) {
if ($orig_path | is-not-empty) {
print $" (ansi yellow)Path declared but not reachable:(ansi reset) ($orig_path)"
} else {
print $" (ansi dark_gray)No path declared — cannot resolve origin project(ansi reset)"
}
} else {
let op = $data.origin_project
print $" (ansi green)($op.slug)(ansi reset) ($op.root)"
print $" navigate: ONTOREF_PROJECT_ROOT=($op.root) ore describe project"
print $" navigate: ONTOREF_PROJECT_ROOT=($op.root) ore describe capabilities"
print $" navigate: ONTOREF_PROJECT_ROOT=($op.root) ore describe domain"
}
}
# Framework hard constraints — shown for both roles
let fc = ($data.framework_constraints? | default [])
if ($fc | is-not-empty) {
print ""
let src_label = if $data.role == "framework" { "HARD CONSTRAINTS (own ADRs)" } else { "FRAMEWORK HARD CONSTRAINTS (inherited)" }
print $src_label
print $sep
for c in $fc {
print $" [($c.adr_id)] ($c.adr_title)"
print $" ($c.description)"
if ($c.check_hint | is-not-empty) {
print $" Check: ($c.check_hint)"
}
}
}
print ""
}
# ── describe state ──────────────────────────────────────────────────────────────
# "What FSM dimensions exist and where are they currently?"
# Reads .ontology/state.ncl and prints each dimension with current/desired state,
@ -1804,12 +2012,12 @@ export def "describe diff" [
def diff-export-committed [rel_path: string, root: string]: nothing -> record {
let ip = (nickel-import-path $root)
let show = (do { ^git -C $root show $"HEAD:($rel_path)" } | complete)
if $show.exit_code != 0 { return {} }
let committed = (vcs show-committed $"($root)/($rel_path)")
if ($committed | is-empty) { return {} }
let mk = (do { ^mktemp } | complete)
if $mk.exit_code != 0 { return {} }
let tmp = ($mk.stdout | str trim)
$show.stdout | save --force $tmp
$committed | save --force $tmp
let r = (do { ^nickel export --format json --import-path $ip $tmp } | complete)
do { ^rm -f $tmp } | complete | ignore
if $r.exit_code != 0 { return {} }
@ -1907,6 +2115,8 @@ def collect-identity [root: string]: nothing -> record {
let manifest = (load-manifest-safe $root)
let kind = if ($manifest | is-not-empty) { $manifest.repo_kind? | default "" } else { "" }
let description = if ($manifest | is-not-empty) { $manifest.description? | default "" } else { "" }
let domain_origin = if ($manifest | is-not-empty) { $manifest.domain_origin? | default {} } else { {} }
let domain_provides = if ($manifest | is-not-empty) { $manifest.domain_provides? | default {} } else { {} }
{
name: $name,
@ -1918,6 +2128,8 @@ def collect-identity [root: string]: nothing -> record {
has_reflection: ($"($root)/reflection" | path exists),
has_manifest: ($"($root)/.ontology/manifest.ncl" | path exists),
has_coder: ($"($root)/.coder" | path exists),
domain_origin: $domain_origin,
domain_provides: $domain_provides,
}
}
@ -2148,7 +2360,7 @@ def scan-ontoref-commands []: nothing -> list<string> {
"describe project", "describe capabilities", "describe constraints",
"describe tools", "describe features", "describe impact", "describe why",
"describe guides", "describe diff", "describe api",
"describe state", "describe workspace",
"describe state", "describe workspace", "describe domain",
]
}
@ -2250,9 +2462,8 @@ def scan-dev-tools [root: string]: nothing -> list<record> {
}
def git-remote-slug [root: string]: nothing -> string {
let r = do { ^git -C $root remote get-url origin } | complete
if $r.exit_code != 0 { return "" }
let url = ($r.stdout | str trim)
let url = (vcs remote-url)
if ($url | is-empty) { return "" }
if ($url | str contains "@") {
# git@host:owner/repo.git
$url | split row ":" | last | str replace -r '\.git$' "" | str trim
@ -2290,7 +2501,7 @@ def scan-project-flags [root: string]: nothing -> record {
if $cfg != null { $cfg.nats_events?.enabled? | default false } else { false }
} else { false }
let has_git = ($"($root)/.git" | path exists)
let has_git = (vcs is-repo)
let git_slug = if $has_git { git-remote-slug $root } else { "" }
let has_git_remote = ($git_slug | is-not-empty)
@ -2499,6 +2710,88 @@ def load-manifest-safe [root: string]: nothing -> record {
daemon-export-safe $manifest --import-path $ip | default {}
}
# Scan ~/.config/ontoref/projects.ncl and return projects whose
# manifest.domain_origin.id matches domain_id.
# Text-parses project.ncl files to extract roots (no nickel subprocess).
# Only invokes nickel for projects whose raw manifest text contains the id.
def find-domain-implementations [domain_id: string]: nothing -> list<record> {
if ($domain_id | is-empty) { return [] }
let cfg = $"($env.HOME)/.config/ontoref/projects.ncl"
if not ($cfg | path exists) { return [] }
let proj_paths = (
open --raw $cfg
| lines
| each { |l| $l | str trim }
| where { |l| $l | str contains 'import "' }
| each { |l|
let m = ($l | parse --regex 'import\s+"(?P<p>[^"]+)"')
if ($m | is-empty) { null } else { $m | get p | first }
}
| compact
)
$proj_paths | each { |proj_ncl|
if not ($proj_ncl | path exists) { return null }
# Fast text extraction of root = "..." from project.ncl
let root_line = (
open --raw $proj_ncl
| lines
| where { |l| $l | str trim | str starts-with "root " }
| first
| default ""
)
if ($root_line | is-empty) { return null }
let root_m = ($root_line | parse --regex '"(?P<r>[^"]+)"')
if ($root_m | is-empty) { return null }
let root = ($root_m | get r | first)
if not ($root | path exists) { return null }
let manifest_path = $"($root)/.ontology/manifest.ncl"
if not ($manifest_path | path exists) { return null }
# Quick text pre-filter — avoids nickel for unrelated projects
let raw = (open --raw $manifest_path)
if not ($raw | str contains $"\"($domain_id)\"") { return null }
let ip = (nickel-import-path $root)
let mdata = (daemon-export-safe $manifest_path --import-path $ip | default {})
let orig_id = ($mdata.domain_origin? | default {} | get id? | default "")
if $orig_id != $domain_id { return null }
let orig = ($mdata.domain_origin? | default {})
{
root: $root,
slug: ($mdata.slug? | default ($root | path basename)),
name: ($mdata.name? | default ($root | path basename)),
integrated: ($orig.integrated? | default true),
extends: ($orig.extends? | default []),
}
} | compact
}
# Load hard constraints from Accepted ADRs at framework_root.
# Returns list of {adr_id, title, description, check_hint} for agent consumption.
def load-framework-hard-constraints [framework_root: string]: nothing -> list<record> {
if ($framework_root | is-empty) or not ($framework_root | path exists) { return [] }
let files = (glob $"($framework_root)/adrs/adr-*.ncl")
if ($files | is-empty) { return [] }
let ip = (nickel-import-path $framework_root)
$files | each { |f|
let adr = (daemon-export-safe $f --import-path $ip)
if $adr == null { return null }
if ($adr.status? | default "") != "Accepted" { return null }
let hard = ($adr.constraints? | default [] | where { |c| ($c.severity? | default "") == "Hard" })
if ($hard | is-empty) { return null }
$hard | each { |c| {
adr_id: ($adr.id? | default ""),
adr_title: ($adr.title? | default ""),
description: ($c.description? | default ($c.claim? | default "")),
check_hint: ($c.check_hint? | default ($c.id? | default "")),
}}
} | compact | flatten
}
def load-all-adrs [root: string]: nothing -> list<record> {
let files = (glob $"($root)/adrs/adr-*.ncl")
let ip = (nickel-import-path $root)
@ -2600,6 +2893,37 @@ def render-project-text [data: record, actor: string, root: string]: nothing ->
print $"Kind: ($data.identity.kind)"
}
# Domain ontology layer
let d_orig = ($data.identity.domain_origin? | default {})
let d_prov = ($data.identity.domain_provides? | default {})
if ($d_orig | is-not-empty) and ($d_orig.id? | is-not-empty) {
let orig_id = ($d_orig.id? | default "")
let orig_name = ($d_orig.name? | default "")
let orig_integ = if ($d_orig.integrated? | default true) { "integrated" } else { "external" }
print $"Domain: implements ($orig_id) — ($orig_name) [$orig_integ]"
let orig_path = ($d_orig.path? | default "")
if ($orig_path | is-not-empty) { print $" origin: ($orig_path)" }
let orig_ext = ($d_orig.extends? | default [])
if ($orig_ext | is-not-empty) { print $" extends: ($orig_ext | str join ', ')" }
}
if ($d_prov | is-not-empty) and ($d_prov.id? | is-not-empty) {
let prov_id = ($d_prov.id? | default "")
let prov_name = ($d_prov.name? | default "")
let prov_kind = ($d_prov.kind? | default "Ncl" | into string)
print $"Domain: provides ($prov_id) — ($prov_name) [$prov_kind]"
let prov_schema = ($d_prov.schema_path? | default "")
if ($prov_schema | is-not-empty) { print $" schemas: ($prov_schema)" }
let prov_impl = ($d_prov.impl_repo_kind? | default "")
if ($prov_impl | is-not-empty) { print $" impl repo_kind: ($prov_impl)" }
print $" → ore describe domain (list implementations)"
}
if ($d_orig | is-not-empty) and ($d_orig.id? | is-not-empty) {
let orig_path = ($d_orig.path? | default "")
if ($orig_path | is-not-empty) {
print $" → ONTOREF_PROJECT_ROOT=($orig_path) ore describe domain (framework details)"
}
}
# Systems present
let systems = [
(if $data.identity.has_ontology { "ontology" } else { null }),
@ -2678,6 +3002,38 @@ def render-project-text [data: record, actor: string, root: string]: nothing ->
}
}
def resolve-domain-extension [root: string]: nothing -> record {
let ontoref_root = ($env.ONTOREF_ROOT? | default "")
if ($ontoref_root | is-empty) { return {} }
let manifest_path = $"($root)/.ontology/manifest.ncl"
if not ($manifest_path | path exists) { return {} }
let rk_raw = (do { ^nickel export $manifest_path } | complete)
if $rk_raw.exit_code != 0 { return {} }
let repo_kind = ($rk_raw.stdout | from json | get repo_kind? | default "")
if ($repo_kind | is-empty) { return {} }
let domains_dir = $"($ontoref_root)/domains"
if not ($domains_dir | path exists) { return {} }
let matched = (
ls $domains_dir
| where type == "dir"
| get name
| each { |d|
let rk = $"($d)/repo_kinds.txt"
if ($rk | path exists) and ((open --raw $rk | lines | any { |l| ($l | str trim) == $repo_kind })) {
$d | path basename
} else { null }
}
| compact
| if ($in | is-empty) { null } else { $in | first }
)
if ($matched | is-empty) { return {} }
let domain_ncl = $"($domains_dir)/($matched)/domain.ncl"
if not ($domain_ncl | path exists) { return {} }
let raw = (do { ^nickel export $domain_ncl } | complete)
if $raw.exit_code != 0 { return {} }
$raw.stdout | from json | insert _domain_id $matched
}
def render-capabilities-text [data: record, actor: string, root: string]: nothing -> nothing {
print ""
print "CAPABILITIES"
@ -2869,6 +3225,66 @@ def render-capabilities-text [data: record, actor: string, root: string]: nothin
}
}
let dom = ($data.domain_extension? | default {})
if ($dom | is-not-empty) and ($dom.name? | is-not-empty) {
let domain_id = ($dom._domain_id? | default "")
let cmd_caller = ($env.ONTOREF_CALLER? | default "ontoref")
print ""
print $"DOMAIN EXTENSION: ($dom.name)"
print "──────────────────────────────────────────────────────────────────"
for c in ($dom.commands? | default []) {
print $" ($cmd_caller) ($domain_id) ($c.id) — ($c.description)"
}
if ($dom.pages? | default [] | is-not-empty) {
print ""
for p in ($dom.pages? | default []) {
print $" ($p.route) — ($p.title)"
}
}
}
# Domain ontology layer (manifest domain_origin / domain_provides)
let manifest = (load-manifest-safe $root)
let d_orig = ($manifest.domain_origin? | default {})
let d_prov = ($manifest.domain_provides? | default {})
let has_orig = ($d_orig | is-not-empty) and ($d_orig.id? | is-not-empty)
let has_prov = ($d_prov | is-not-empty) and ($d_prov.id? | is-not-empty)
let has_domain_layer = $has_orig or $has_prov
if $has_domain_layer {
print ""
print "DOMAIN ONTOLOGY LAYER"
print "──────────────────────────────────────────────────────────────────"
if ($d_orig | is-not-empty) and ($d_orig.id? | is-not-empty) {
let orig_id = ($d_orig.id? | default "")
let orig_name = ($d_orig.name? | default "")
let orig_integ = if ($d_orig.integrated? | default true) { "integrated" } else { "external" }
print $" origin: ($orig_id) ($orig_name) [($orig_integ)]"
let orig_path = ($d_orig.path? | default "")
if ($orig_path | is-not-empty) { print $" path: ($orig_path)" }
let orig_ext = ($d_orig.extends? | default [])
if ($orig_ext | is-not-empty) { print $" extends: ($orig_ext | str join ', ')" }
}
if ($d_orig | is-not-empty) and ($d_orig.id? | is-not-empty) {
let orig_path = ($d_orig.path? | default "")
if ($orig_path | is-not-empty) {
print $" navigate: ONTOREF_PROJECT_ROOT=($orig_path) ore describe domain"
}
}
if ($d_prov | is-not-empty) and ($d_prov.id? | is-not-empty) {
let prov_id = ($d_prov.id? | default "")
let prov_name = ($d_prov.name? | default "")
let prov_kind = ($d_prov.kind? | default "Ncl" | into string)
print $" provides: ($prov_id) ($prov_name) [($prov_kind)]"
let prov_schema = ($d_prov.schema_path? | default "")
if ($prov_schema | is-not-empty) { print $" schemas: ($prov_schema)" }
let prov_impl = ($d_prov.impl_repo_kind? | default "")
if ($prov_impl | is-not-empty) { print $" impl_for: ($prov_impl)" }
let prov_desc = ($d_prov.description? | default "")
if ($prov_desc | is-not-empty) { print $" ($prov_desc)" }
print $" navigate: ore describe domain (list implementations)"
}
}
print ""
}

View file

@ -50,7 +50,39 @@ def run-migration-check [check: record, root: string]: nothing -> record {
}
}
"NuCmd" => {
let r = do { nu -c $check.cmd } | complete
# KNOWN BUG: Including $ONTOREF_ROOT/ontology (the parent ontology dir) in
# NICKEL_IMPORT_PATH causes the installed ontoref binary to fail with
# "Not a directory" inside its nu subprocess. Subdirs (ontology/schemas,
# ontology/defaults) are safe. Root cause: uninvestigated interaction between
# nickel's directory traversal and nushell's path handling in the subprocess chain.
# Fix: build the import path explicitly, omitting $ONTOREF_ROOT/ontology.
# When root is a consumer project (not the ontoref root), include root/ontology
# so consumer-local ontology defaults (e.g. defaults/manifest.ncl) are resolvable.
# Excluded when root == ONTOREF_ROOT: that specific path triggers a "Not a directory"
# bug in the nu subprocess chain (see comment above).
let project_ontology = if $root != $env.ONTOREF_ROOT { [$"($root)/ontology"] } else { [] }
let safe_ip = (
[
$"($root)/.ontology"
$"($root)/adrs"
$"($root)/.ontoref/ontology/schemas"
$"($root)/.ontoref/adrs"
$root
$"($env.ONTOREF_ROOT)/ontology/schemas"
$"($env.ONTOREF_ROOT)/adrs"
$env.ONTOREF_ROOT
]
| append $project_ontology
| where { |p| $p | path exists }
| uniq
| str join ":"
)
let r = (do {
with-env {
NICKEL_IMPORT_PATH: $safe_ip
ONTOREF_PROJECT_ROOT: $root
} { nu --no-config-file -c $check.cmd }
} | complete)
let expected = ($check.expect_exit? | default 0)
{
passed: ($r.exit_code == $expected),

View file

@ -16,6 +16,7 @@
use env.nu *
use store.nu [daemon-available]
use ../nulib/shared.nu [project-root]
use ./vcs.nu
# ── Internal helpers ──────────────────────────────────────────────────────────
@ -72,22 +73,32 @@ def on-enter-local []: nothing -> nothing {
}
def install-hooks [mode: string]: nothing -> nothing {
let git_dir = (
do { ^git rev-parse --git-dir } | complete
| if $in.exit_code == 0 { $in.stdout | str trim } else { "" }
)
if ($git_dir | is-empty) { return }
match (vcs detect) {
"jj" => {
# jj has no hook system — hooks are dispatched by the jjw wrapper instead.
# The jjw wrapper calls `on-vcs-event` from git-event.nu after relevant operations.
print " advisory: jj repo detected — skipping hook installation (no jj hook system)"
},
"git" => {
let git_dir = (
do { ^git rev-parse --git-dir } | complete
| if $in.exit_code == 0 { $in.stdout | str trim } else { "" }
)
if ($git_dir | is-empty) { return }
let hook_body = if $mode == "daemon" {
$"#!/usr/bin/env bash\nnu \"($env.ONTOREF_ROOT)/reflection/hooks/git-event.nu\" \"$1\" 2>/dev/null || true\n"
} else {
"#!/usr/bin/env bash\n# ontoref local mode — no-op\nexit 0\n"
}
let hook_body = if $mode == "daemon" {
$"#!/usr/bin/env bash\nnu \"($env.ONTOREF_ROOT)/reflection/hooks/git-event.nu\" \"$1\" 2>/dev/null || true\n"
} else {
"#!/usr/bin/env bash\n# ontoref local mode — no-op\nexit 0\n"
}
for hook in ["post-merge" "post-checkout"] {
let path = $"($git_dir)/hooks/($hook)"
$hook_body | save -f $path
do { ^chmod +x $path } | complete | null
for hook in ["post-merge" "post-checkout"] {
let path = $"($git_dir)/hooks/($hook)"
$hook_body | save -f $path
do { ^chmod +x $path } | complete | null
}
},
_ => {} # no-op — no VCS detected
}
}

117
reflection/modules/vcs.nu Normal file
View file

@ -0,0 +1,117 @@
# VCS abstraction — detects jj or git from filesystem, exposes uniform operations.
#
# All ontoref modules should call these functions instead of hardcoding `git`
# commands. Detection is filesystem-based (no config, no env var): checks for
# `.jj/` (jj) and `.git/` (git) in ONTOREF_PROJECT_ROOT or ONTOREF_ROOT.
use ./env.nu
# Private: resolve project root following the same convention as other modules.
def project-root []: nothing -> string {
let pr = ($env.ONTOREF_PROJECT_ROOT? | default "")
if ($pr | is-not-empty) and ($pr != $env.ONTOREF_ROOT) { $pr } else { $env.ONTOREF_ROOT }
}
# Detect which VCS is active in the project root.
# Returns: "jj" | "git" | "none"
export def detect []: nothing -> string {
let root = (project-root)
let has_jj = ($root | path join ".jj" | path exists)
let has_git = ($root | path join ".git" | path exists)
if $has_jj { "jj" } else if $has_git { "git" } else { "none" }
}
# True when any VCS is active.
export def "is-repo" []: nothing -> bool {
(detect) != "none"
}
# Get file content at last-committed state.
#
# jj model: the working copy IS a commit (@). Reading from @- gives the parent
# commit — i.e., the state before any current working-copy edits.
# `jj file show` requires jj >= 0.40.
export def "show-committed" [file_path: string]: nothing -> string {
let root = (project-root)
match (detect) {
"jj" => {
let rel = ($file_path | path relative-to $root)
do { ^jj --no-pager --repository $root file show $rel -r "@-" } | complete
| if $in.exit_code == 0 { $in.stdout } else { "" }
},
"git" => {
let rel = ($file_path | path relative-to $root)
do { ^git -C $root show $"HEAD:($rel)" } | complete
| if $in.exit_code == 0 { $in.stdout } else { "" }
},
_ => { "" }
}
}
# Revert a file to its last-committed state.
#
# jj: `jj restore --from @-` undoes working-copy edits for the given file.
# git: `git checkout -- <file>` restores from HEAD.
export def "restore-file" [file_path: string]: nothing -> nothing {
let root = (project-root)
match (detect) {
"jj" => {
let rel = ($file_path | path relative-to $root)
do { ^jj --no-pager --repository $root restore --from "@-" -- $rel } | complete | ignore
},
"git" => {
do { ^git -C $root checkout -- $file_path } | complete | ignore
},
_ => {
error make { msg: $"vcs restore-file: no VCS at ($root)" }
}
}
}
# Get the remote origin URL.
export def "remote-url" []: nothing -> string {
let root = (project-root)
match (detect) {
"jj" => {
let r = do { ^jj --no-pager --repository $root git remote list } | complete
if $r.exit_code != 0 or ($r.stdout | str trim | is-empty) { return "" }
# Format: "<name> <url>" per line — extract URL from first remote
$r.stdout | lines | where { |l| ($l | str trim | is-not-empty) }
| first | split row "\t" | last | str trim
},
"git" => {
let r = do { ^git -C $root remote get-url origin } | complete
if $r.exit_code == 0 { $r.stdout | str trim } else { "" }
},
_ => { "" }
}
}
# Get the current branch or bookmark name.
#
# jj: returns the bookmark pointing to @, or "detached" if none.
# git: returns the branch name from rev-parse.
export def "current-ref" []: nothing -> string {
let root = (project-root)
match (detect) {
"jj" => {
let r = do {
^jj --no-pager --repository $root bookmark list --pointing-to "@"
-T 'name ++ "\n"'
} | complete
if $r.exit_code != 0 { return "detached" }
let names = ($r.stdout | lines | where { |l| ($l | str trim | is-not-empty) })
if ($names | is-empty) { "detached" } else { $names | first }
},
"git" => {
let r = do { ^git -C $root rev-parse --abbrev-ref HEAD } | complete
if $r.exit_code == 0 { $r.stdout | str trim } else { "unknown" }
},
_ => { "unknown" }
}
}
# True when the project root has a VCS remote configured.
export def "has-remote" []: nothing -> bool {
(remote-url) | is-not-empty
}

View file

@ -191,6 +191,7 @@ export def help-group [group: string] {
fmt-cmd $"($cmd) sync audit --quick" "fast audit (skip API surface)" -v 1
fmt-cmd $"($cmd) sync audit --fmt json" "structured output (for agents)" -v 1
fmt-cmd $"($cmd) sync watch" "bacon-based continuous drift detection" -v 1
fmt-cmd $"($cmd) sync manifest-check" "capability coverage: Hard gaps block, Soft gaps warn" -v 1
print ""
},
"coder" => {
@ -315,6 +316,11 @@ export def help-group [group: string] {
fmt-cmd $"($cmd) describe extensions" "list .ontology/*.ncl extensions (career, personal, …)" -v 1
fmt-cmd $"($cmd) describe extensions --dump <stem>" "dump a specific extension (e.g. --dump career)" -v 1
print ""
fmt-section "Domain ontology relationships"
print ""
fmt-cmd $"($cmd) describe domain" "framework/implementation role, origin, known implementations, agent advisory" -v 1
fmt-cmd $"($cmd) describe domain --fmt json" "machine-readable (default for --actor agent)" -v 1
print ""
fmt-aliases [
{ short: "d", long: "describe" },
{ short: "d s", long: "describe search <term>" },
@ -330,6 +336,8 @@ export def help-group [group: string] {
{ short: "d imp", long: "describe impact <id>" },
{ short: "d w", long: "describe why <id>" },
{ short: "d ext", long: "describe extensions" },
{ short: "d dom", long: "describe domain" },
{ short: "ddom", long: "describe domain (top-level shortcut)" },
]
},
"search" | "s" => {
@ -459,10 +467,30 @@ export def help-group [group: string] {
{ short: "mg p", long: "migrate pending" },
]
},
_ if (($"($env.ONTOREF_ROOT?)/domains/($group)/domain.ncl") | path exists) => {
let domain_ncl = $"($env.ONTOREF_ROOT)/domains/($group)/domain.ncl"
let d = (nickel export $domain_ncl | from json)
print ""
fmt-header $"($d.name | str upcase) (domain extension)"
fmt-sep
let cmd_prefix = ($env.ONTOREF_CALLER? | default "ontoref")
for c in $d.commands {
fmt-cmd $"($cmd_prefix) ($group) ($c.id)" $c.description
}
print ""
if ($d.pages | is-not-empty) {
fmt-section "WEB PAGES"
print ""
for p in $d.pages {
fmt-info $"($p.route) — ($p.title)"
}
print ""
}
},
_ => {
print $" (ansi red)Unknown group: ($group)(ansi reset)"
print ""
fmt-info "Available groups: check | form | mode | adr | register | backlog | config | sync | coder | manifest | describe | log | migrate"
fmt-info "Available groups: check | form | mode | adr | register | backlog | config | sync | coder | manifest | describe | log | migrate | <domain-id>"
print ""
},
}