diff --git a/.gitignore b/.gitignore index 89c9375..25c3324 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,8 @@ vendordiff.patch # Generated SBOM files SBOM.*.json *.sbom.json + +# UnoCSS build +assets/css/node_modules/ +assets/css/pnpm-lock.yaml +crates/ontoref-daemon/public/css/ontoref.css diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 0488308..3e43e95 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -88,6 +88,7 @@ "globs": [ "**/*.md", "!node_modules/**", + "!**/node_modules/**", "!target/**", "!.git/**", "!build/**", @@ -106,6 +107,7 @@ "assets/branding/**", "assets/logo_prompt.md", "node_modules/**", + "**/node_modules/**", "target/**", ".git/**", "build/**", diff --git a/.ontology/core.ncl b/.ontology/core.ncl index dc98f4a..ba168e1 100644 --- a/.ontology/core.ncl +++ b/.ontology/core.ncl @@ -82,9 +82,10 @@ let d = import "../ontology/defaults/core.ncl" in "adrs/adr-007-api-surface-discoverability-onto-api-proc-macro.ncl", "adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl", "adrs/adr-009-manifest-self-interrogation-layer-three-semantic-axes.ncl", + "adrs/adr-010-protocol-migration-system.ncl", "CHANGELOG.md", ], - adrs = ["adr-001", "adr-002", "adr-003", "adr-004", "adr-005", "adr-006", "adr-007", "adr-008", "adr-009"], + adrs = ["adr-001", "adr-002", "adr-003", "adr-004", "adr-005", "adr-006", "adr-007", "adr-008", "adr-009", "adr-010"], }, d.make_node { @@ -151,9 +152,27 @@ let d = import "../ontology/defaults/core.ncl" in "reflection/forms/adopt_ontoref.ncl", "reflection/templates/adopt_ontoref.nu.j2", "reflection/templates/update-ontology-prompt.md", + "reflection/migrations/", ], }, + d.make_node { + id = "protocol-migration-system", + name = "Protocol Migration System", + pole = 'Yang, + level = 'Practice, + description = "Progressive, ordered protocol migrations for consumer projects. Each migration is an NCL file in reflection/migrations/NNN-slug.ncl declaring id, slug, description, a typed check (FileExists | Grep | NuCmd), and instructions interpolated at runtime with project_root and project_name. Applied state is determined solely by whether the check passes — no state file, fully idempotent. NuCmd checks must be valid Nushell (no bash &&, $env.VAR not $VAR). Accessible via `ontoref migrate list/pending/show` and the interactive group dispatch. Narrows ADR instance checks to `adr-[0-9][0-9][0-9]-*.ncl` to exclude schema/template infrastructure files from pattern matching.", + invariant = false, + artifact_paths = [ + "reflection/migrations/", + "reflection/modules/migrate.nu", + "reflection/nulib/interactive.nu", + "reflection/nulib/help.nu", + "reflection/bin/ontoref.nu", + ], + adrs = ["adr-010"], + }, + d.make_node { id = "ontology-three-file-split", name = "Ontology Three-File Split", @@ -503,6 +522,16 @@ let d = import "../ontology/defaults/core.ncl" in { from = "manifest-self-description", to = "adr-lifecycle", kind = 'Complements, weight = 'Medium, note = "capabilities.adrs[] creates explicit typed links from capabilities to the ADRs that formalize them — the ADR→Node linkage pattern extended to the manifest layer." }, + # Protocol Migration System edges + { from = "protocol-migration-system", to = "adopt-ontoref-tooling", kind = 'ManifestsIn, weight = 'High, + note = "Migration system is the versioned upgrade surface for adopt-ontoref-tooling — new protocol features arrive as numbered migrations, not template rewrites." }, + { from = "protocol-migration-system", to = "adr-lifecycle", kind = 'Complements, weight = 'High, + note = "Each migration check can verify ADR-level constraints are met in consumer repos — migrations and ADRs are complementary protocol enforcement layers." }, + { from = "protocol-migration-system", to = "no-enforcement", kind = 'Complements, weight = 'Medium, + note = "Migrations are advisory: `migrate pending` reports state, never applies automatically. The actor decides when to apply." }, + { 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." }, + # Config Surface edges { from = "config-surface", to = "ontoref-daemon", kind = 'ManifestsIn, weight = 'High }, { from = "config-surface", to = "ontoref-ontology-crate", kind = 'DependsOn, weight = 'High, diff --git a/.ontology/manifest.ncl b/.ontology/manifest.ncl index d15abed..0d69b57 100644 --- a/.ontology/manifest.ncl +++ b/.ontology/manifest.ncl @@ -61,6 +61,33 @@ m.make_manifest { }, ], + templates = [ + m.make_template { + id = "project-full-adoption-prompt", + kind = 'AgentPrompt, + source_path = "reflection/templates/project-full-adoption-prompt.md", + description = "Master adoption prompt for new and existing projects: protocol infrastructure, ontology enrichment, config surface (nickel-validated-overrides + ConfigFields derive), API surface (#[onto_api]), and manifest self-interrogation (capabilities/requirements/critical_deps). Orchestrates all other templates.", + }, + m.make_template { + id = "update-ontology-prompt", + kind = 'AgentPrompt, + source_path = "reflection/templates/update-ontology-prompt.md", + description = "8-phase ontology enrichment prompt: core.ncl nodes/edges, state.ncl dimension transitions, manifest assets, connections, ADR check_hint migration. Called from Phase 2 of project-full-adoption-prompt.", + }, + m.make_template { + id = "manifest-self-interrogation-prompt", + kind = 'AgentPrompt, + source_path = "reflection/templates/manifest-self-interrogation-prompt.md", + description = "Focused prompt for populating capabilities[], requirements[], and critical_deps[] in manifest.ncl. Called from Phase 5 of project-full-adoption-prompt.", + }, + m.make_template { + id = "vendor-frontend-assets-prompt", + kind = 'AgentPrompt, + source_path = "reflection/templates/vendor-frontend-assets-prompt.md", + description = "Guide for vendoring frontend JS dependencies locally: directory layout (assets/vendor/), just recipe structure (assets.just with pinned version variables), Tera template integration, CDN asset verification steps, and agent execution checklist. Reusable across any ontoref-protocol project with a static-file-serving daemon.", + }, + ], + consumption_modes = [ m.make_consumption_mode { consumer = 'Developer, diff --git a/.ontology/state.ncl b/.ontology/state.ncl index 274d95c..0ac3689 100644 --- a/.ontology/state.ncl +++ b/.ontology/state.ncl @@ -24,8 +24,8 @@ 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 = "First external adoption.", - blocker = "ontoref.dev not yet published; no external consumers yet. Auth model complete. Install pipeline complete. Personal/career schema layer present; content modes operational. Nu 0.111 compat fixed (ADR-006). Protocol v2 complete: manifest.ncl + connections.ncl templates, update_ontoref mode, API catalog via #[onto_api], describe diff, describe api, per-file versioning. Config surface complete (ADR-008): typed DaemonNclConfig, #[derive(ConfigFields)] inventory coherence registry, NCL contracts (LogConfig/DaemonConfig in .ontoref/contracts.ncl), override-layer mutation API, multi-consumer manifest schema. Manifest self-interrogation layer complete (ADR-009): capability_type, requirement_type (env_target: Production/Development/Both, kind: Tool/Service/EnvVar/Infrastructure), critical_dep_type — describe requirements new subcommand, describe guides extended. Syntaxis syntaxis-ontology crate has pending ES→EN migration errors.", + 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) all complete.", + blocker = "ontoref.dev not yet published.", horizon = 'Months, }, ], diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5f85508..133679f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -111,23 +111,25 @@ repos: hooks: - id: check-added-large-files args: ['--maxkb=1000'] - exclude: ^assets/presentation/ + exclude: (^assets/presentation/|node_modules/) - id: check-case-conflict + exclude: node_modules/ - id: check-merge-conflict + exclude: node_modules/ - id: check-toml - exclude: ^assets/presentation/ + exclude: (^assets/presentation/|node_modules/) - id: check-yaml - exclude: ^(\.woodpecker/|assets/presentation/) + exclude: (^\.woodpecker/|^assets/presentation/|node_modules/) - id: end-of-file-fixer - exclude: ^assets/presentation/ + exclude: (^assets/presentation/|node_modules/) - id: trailing-whitespace - exclude: (\.md$|^assets/presentation/) + exclude: (\.md$|^assets/presentation/|node_modules/) - id: mixed-line-ending - exclude: ^assets/presentation/ + exclude: (^assets/presentation/|node_modules/) diff --git a/CHANGELOG.md b/CHANGELOG.md index 056177e..be9723e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,59 @@ ADRs referenced below live in `adrs/` as typed Nickel records. ## [Unreleased] +### Protocol Migration System — progressive NCL checks for consumer project upgrades (ADR-010) + +Replaces the template-prompt approach with an ordered, idempotent migration system. Applied state +determined by check result alone — no state file. 6 migrations shipped; runtime ships +`migrate list/pending/show` with interactive group dispatch. + +#### `reflection/migrations/` — 6 ordered migrations + +- `0001-ontology-infrastructure` — `.ontology/manifest.ncl` and `connections.ncl` present. +- `0002-adr-typed-checks` — no `check_hint` in ADR instance files (`adr-[0-9][0-9][0-9]-*.ncl`); + check narrowed from `adrs/` broad scan to exclude schema/template infrastructure files. +- `0003-manifest-self-interrogation` — `capabilities[]` and `requirements[]` non-empty in manifest.ncl. +- `0004-just-convention` — justfile validates against canonical module convention (pending in this repo — documented gap). +- `0005-mode-step-schema` — all reflection mode steps declare `actor`, `on_error`, `depends_on`. +- `0006-claude-agent-entrypoint` — `Agent Entry-Point Protocol` section present in `.claude/CLAUDE.md`. + +All NuCmd checks rewritten from bash to valid Nushell: `&&` removed, `$env.VAR` replacing `$VAR`, +no bash-style redirects. Grep checks on ADR files use `adr-[0-9][0-9][0-9]-*.ncl` glob. + +#### `reflection/modules/migrate.nu` — new module + +- `migrate list [--fmt] [--actor]` — all migrations with applied/pending status; JSON for agents. +- `migrate pending [--fmt] [--actor]` — pending only. +- `migrate show [--fmt]` — runtime-interpolated instructions; accepts short ids (`002` → `0002`). +- Applied state: `run-migration-check` dispatches over `FileExists | Grep | NuCmd`. +- No state file — idempotent by construction. + +#### `reflection/nulib/interactive.nu` + `help.nu` — `migrate` group wired + +- `group-command-info` — `migrate` case added (list, pending, show). +- `run-group-command` — `migrate` dispatch added. +- `help-group` — `migrate` help section added; fallback "Available groups" updated. + +#### `reflection/bin/ontoref.nu` — shims + aliases + +- `main migrate`, `main migrate list/pending/show` added. +- Short aliases: `mg`, `mg l`, `mg p`. + +#### `reflection/schemas/justfile-convention.ncl` — export fix + +- Removed `Module` and `ModuleSystem` from the exported record (open contract fields with no default + value caused `nickel export` to fail). Both remain as `let` bindings for internal NCL use. + +#### on+re update + +| Artifact | Change | +|----------|--------| +| `adrs/adr-010-...ncl` | Created — protocol migration system, progressive NCL checks | +| `.ontology/core.ncl` | `protocol-migration-system` node added; `adopt-ontoref-tooling` artifacts updated; `adr-lifecycle` updated with ADR-010; 4 new edges | +| `.ontology/state.ncl` | `protocol-maturity` catalyst updated (10 consumers, all features complete); blocker narrowed to `ontoref.dev not yet published` | + +--- + ### Manifest Self-Interrogation Layer — capabilities, requirements, critical deps (ADR-009) Three new typed arrays in `manifest_type` answering operational self-knowledge queries distinct from diff --git a/README.md b/README.md index e7f720e..7bff453 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,14 @@ gate — Rust structs are contract-trusted readers with `#[serde(default)]`. Ontoref demonstrates the pattern on itself: `.ontoref/contracts.ncl` applies `LogConfig` and `DaemonConfig` contracts to `.ontoref/config.ncl`. ([ADR-008](adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl)) +**Protocol Migration System** — protocol upgrades for consumer projects expressed as ordered NCL files +in `reflection/migrations/NNN-slug.ncl`. Each migration declares a typed check (`FileExists | Grep | +NuCmd`) whose result IS the applied state — no state file, fully idempotent. `migrate list` shows all +migrations with applied/pending status; `migrate pending` lists only what is missing; `migrate show ` +renders runtime-interpolated instructions (project_root and project_name auto-detected). NuCmd checks are +valid Nushell (no bash `&&`, `$env.VAR` not `$VAR`). Grep checks targeting ADR files scope to +`adr-[0-9][0-9][0-9]-*.ncl` to exclude schema/template infrastructure files. ([ADR-010](adrs/adr-010-protocol-migration-system.ncl)) + **Manifest Self-Interrogation** — `manifest_type` gains three typed arrays that answer self-knowledge queries agents and operators need on cold start: `capabilities[]` (what the project does, why it was built, how it works — with explicit `nodes[]` and `adrs[]` cross-references into the DAG), diff --git a/adrs/adr-010-protocol-migration-system.ncl b/adrs/adr-010-protocol-migration-system.ncl new file mode 100644 index 0000000..720746f --- /dev/null +++ b/adrs/adr-010-protocol-migration-system.ncl @@ -0,0 +1,84 @@ +let d = import "adr-defaults.ncl" in + +d.make_adr { + id = "adr-010", + title = "Protocol Migration System — Progressive NCL Checks for Consumer Project Upgrades", + status = 'Accepted, + date = "2026-03-28", + + context = "As the ontoref protocol evolved (manifest.ncl self-interrogation, typed ADR checks, CLAUDE.md agent entry-point, justfile convention), the adoption tooling relied on static prompt templates with manual {placeholder} substitution. An agent or developer adopting ontoref had no machine-queryable way to know which protocol features were missing from their project, nor how to apply them in a safe, ordered sequence. The template approach produced four separate documents that drifted out of sync with the actual protocol state and required human judgement to determine which ones applied. There was no idempotency guarantee and no check mechanism — a project that had already applied a change would re-read instructions that no longer applied.", + + decision = "Protocol upgrades for consumer projects are expressed as ordered NCL migration files in reflection/migrations/NNN-slug.ncl. Each migration declares: id (zero-padded 4-digit string), slug, description, a typed check record (FileExists | Grep | NuCmd), and an instructions string interpolated at runtime with project_root and project_name. Applied state is determined solely by whether the check passes — there is no state file. This makes migrations fully idempotent: running `migrate list` on an already-compliant project shows all applied with no side effects. NuCmd checks must be valid Nushell (no bash &&, $env.VAR not $VAR, no bash redirects). Grep checks targeting ADR files must use the glob pattern adrs/adr-[0-9][0-9][0-9]-*.ncl to exclude infrastructure files (adr-schema.ncl, adr-constraints.ncl, _template.ncl) that legitimately contain deprecated field names as schema definitions. The system is exposed via `ontoref migrate list`, `migrate pending`, and `migrate show ` — wired into the interactive group dispatch and help system. Migrations are advisory: the system reports state, never applies changes automatically.", + + rationale = [ + { + claim = "Check-as-source-of-truth eliminates state file drift", + detail = "Any state file recording 'migration 0003 applied' becomes stale the moment someone reverts a change, changes branches, or clones a fresh repo. The check IS the state: if the condition is satisfied, the migration is applied; if not, it is pending. This is the same principle used by database migration tools that check for a schema version column — except here the 'column' is a Nushell assertion over the project's file system. No synchronization required.", + }, + { + claim = "Typed checks (FileExists | Grep | NuCmd) cover the full protocol surface", + detail = "FileExists covers structural requirements (.ontology/manifest.ncl present). Grep covers content requirements (pattern present or absent in specific files). NuCmd covers semantic requirements that require evaluation — nickel export succeeds, capabilities[] is non-empty, justfile validates. The three types compose the full assertion space without requiring a general-purpose script language in the migration definition itself.", + }, + { + claim = "Ordered numbering enables dependency reasoning without a dependency graph", + detail = "Migration 0003 (manifest self-interrogation) requires migration 0001 (manifest.ncl present) to have been applied. Rather than declaring explicit depends_on edges (which require a DAG evaluator), the numeric ordering encodes the implicit prerequisite sequence. An agent applying pending migrations in order will always satisfy prerequisites before dependent checks.", + }, + ], + + consequences = { + positive = [ + "`migrate pending` gives agents and developers a single authoritative list of what is missing — no manual comparison against protocol documentation", + "Migrations are idempotent and safe to re-run: `migrate list` on a fully-adopted project is a no-op read", + "Instructions are interpolated at runtime with project_root and project_name — no manual placeholder substitution", + "New protocol features arrive as numbered migrations without touching existing template files", + "NuCmd checks encode the same typed check logic used by ADR constraints in validate.nu — consistent assertion model across the protocol", + ], + negative = [ + "NuCmd checks must be single-line Nushell (nu -c) — complex multi-step checks become dense; readability degrades for non-trivial assertions", + "Grep checks require knowing which files to exclude (infrastructure vs instance files); the adr-[0-9][0-9][0-9]-*.ncl pattern is a convention that authors must follow", + "Migration ordering encodes implicit dependencies — a migration that genuinely depends on two prior migrations has no way to express that formally beyond numeric sequence", + ], + }, + + alternatives_considered = [ + { + option = "Single monolithic adoption prompt template with {placeholder} substitution", + why_rejected = "Produced four separate documents (project-full-adoption-prompt.md, update-ontology-prompt.md, manifest-self-interrogation-prompt.md, vendor-frontend-assets-prompt.md) that drifted out of sync. Required manual judgement to determine which applied to a given project. No idempotency, no machine-queryable state, no ordered application guarantee. Each new protocol feature required updating multiple templates.", + }, + { + option = "State file recording applied migration IDs", + why_rejected = "State files become stale on branch switches, cherry-picks, and fresh clones. They require commit discipline to keep in sync. A project where someone manually applied the changes without running the migration tool would show the migration as pending despite being satisfied — false negatives. The check-as-truth model has no false negatives by construction.", + }, + { + option = "Jinja2/j2 templating for instruction rendering", + why_rejected = "The ontoref runtime already runs Nushell for all automation. Adding a j2 dependency for template rendering introduces a new tool to install, configure, and maintain. Runtime string interpolation in Nushell (str replace --all) is sufficient for the two substitution values needed (project_root, project_name) and keeps the migration runner dependency-free.", + }, + ], + + constraints = [ + { + id = "nucmd-checks-must-be-nushell", + claim = "NuCmd check cmd fields must be valid Nushell — no bash operators (&&, ||, 2>/dev/null), no $VARNAME (must be $env.VARNAME)", + scope = "reflection/migrations/*.ncl (any migration with tag = 'NuCmd)", + severity = 'Hard, + check = { tag = 'Grep, pattern = "&&|\\$[A-Z_]+[^)]", paths = ["reflection/migrations/"], must_be_empty = true }, + rationale = "The migration runner executes checks via `nu -c $check.cmd`. Bash syntax in a Nu script produces parser errors that surface as false-negative check results — the migration appears pending due to a runner error, not because the condition is unmet.", + }, + { + id = "grep-checks-use-instance-glob", + claim = "Grep checks targeting ADR files must scope to adrs/adr-[0-9][0-9][0-9]-*.ncl, not adrs/ or adrs/adr-*.ncl", + scope = "reflection/migrations/*.ncl (any migration with tag = 'Grep and paths containing 'adrs')", + severity = 'Soft, + check = { tag = 'Grep, pattern = "\"adrs/\"", paths = ["reflection/migrations/"], must_be_empty = true }, + rationale = "adr-schema.ncl, adr-constraints.ncl, adr-defaults.ncl, and _template.ncl are infrastructure files that legitimately contain deprecated field names as schema definitions. Scanning all of adrs/ produces false positives in ontoref's own repo and in any consumer project that vendors the ADR schema files.", + }, + ], + + related_adrs = ["adr-001", "adr-006"], + + ontology_check = { + decision_string = "Protocol migrations expressed as ordered NCL files with typed idempotent checks; applied state determined by check result not state file; NuCmd checks must be valid Nushell; Grep checks on ADR files must use instance-only glob", + invariants_at_risk = ["no-enforcement", "self-describing"], + verdict = 'Safe, + }, +} diff --git a/api-catalog.json b/api-catalog.json new file mode 100644 index 0000000..19c813f --- /dev/null +++ b/api-catalog.json @@ -0,0 +1,964 @@ +[ + { + "method": "GET", + "path": "/actors", + "description": "List all registered actor sessions with their last-seen timestamp and pending notification count", + "auth": "viewer", + "actors": [ + "developer", + "admin" + ], + "params": [ + { + "name": "project", + "kind": "string", + "constraint": "optional", + "description": "Filter by project slug" + } + ], + "tags": [ + "actors" + ], + "feature": "" + }, + { + "method": "POST", + "path": "/actors/register", + "description": "Register an actor session and receive a bearer token for subsequent calls", + "auth": "none", + "actors": [ + "agent", + "developer", + "ci" + ], + "params": [ + { + "name": "actor", + "kind": "string", + "constraint": "required", + "description": "Actor type (agent|developer|ci|admin)" + }, + { + "name": "project", + "kind": "string", + "constraint": "optional", + "description": "Project slug to associate with" + }, + { + "name": "label", + "kind": "string", + "constraint": "optional", + "description": "Human label for audit trail" + } + ], + "tags": [ + "actors", + "auth" + ], + "feature": "" + }, + { + "method": "DELETE", + "path": "/actors/{token}", + "description": "Deregister an actor session and invalidate its bearer token", + "auth": "none", + "actors": [ + "agent", + "developer", + "ci" + ], + "params": [], + "tags": [ + "actors", + "auth" + ], + "feature": "" + }, + { + "method": "POST", + "path": "/actors/{token}/profile", + "description": "Update actor profile metadata: display name, role, and custom context fields", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [], + "tags": [ + "actors" + ], + "feature": "" + }, + { + "method": "POST", + "path": "/actors/{token}/touch", + "description": "Extend actor session TTL; prevents the session from expiring due to inactivity", + "auth": "none", + "actors": [ + "agent", + "developer", + "ci" + ], + "params": [], + "tags": [ + "actors" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/adr/{id}", + "description": "Read a single ADR by id, exported from NCL as structured JSON", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug (defaults to primary)" + } + ], + "tags": [ + "adrs" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/api/catalog", + "description": "Full catalog of daemon HTTP endpoints with metadata: auth, actors, params, tags", + "auth": "none", + "actors": [ + "agent", + "developer", + "ci", + "admin" + ], + "params": [], + "tags": [ + "meta", + "catalog" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/backlog-json", + "description": "Export the project backlog as structured JSON from reflection/backlog.ncl", + "auth": "viewer", + "actors": [ + "developer", + "agent" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug (defaults to primary)" + } + ], + "tags": [ + "backlog" + ], + "feature": "" + }, + { + "method": "POST", + "path": "/cache/invalidate", + "description": "Invalidate one or all NCL cache entries, forcing re-export on next request", + "auth": "admin", + "actors": [ + "developer", + "admin" + ], + "params": [ + { + "name": "file", + "kind": "string", + "constraint": "optional", + "description": "Specific file path to invalidate (omit to invalidate all)" + } + ], + "tags": [ + "cache" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/cache/stats", + "description": "NCL export cache statistics: entry count, hit/miss counters", + "auth": "viewer", + "actors": [ + "developer", + "admin" + ], + "params": [], + "tags": [ + "cache", + "meta" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/config/cross-project", + "description": "Compare config surfaces across all registered projects: shared values, conflicts, coverage gaps", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [], + "tags": [ + "config" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/describe/actor-init", + "description": "Minimal onboarding payload for a new actor session: what to register as and what to do first", + "auth": "none", + "actors": [ + "agent" + ], + "params": [ + { + "name": "actor", + "kind": "string", + "constraint": "optional", + "description": "Actor type to onboard as" + }, + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug" + } + ], + "tags": [ + "describe", + "actors" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/describe/capabilities", + "description": "Available reflection modes, just recipes, Claude capabilities and CI tools for the project", + "auth": "none", + "actors": [ + "agent", + "developer", + "ci" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug (defaults to primary)" + } + ], + "tags": [ + "describe" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/describe/connections", + "description": "Cross-project connection declarations: upstream, downstream, peers with addressing", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug (defaults to primary)" + } + ], + "tags": [ + "describe", + "federation" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/describe/guides", + "description": "Complete operational context for an actor: identity, axioms, practices, constraints, gate state, modes, actor policy, connections, content assets", + "auth": "none", + "actors": [ + "agent", + "developer", + "ci" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug (defaults to primary)" + }, + { + "name": "actor", + "kind": "string", + "constraint": "optional", + "description": "Actor context filters the policy (agent|developer|ci|admin)" + } + ], + "tags": [ + "describe", + "guides" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/describe/project", + "description": "Project self-description: identity, axioms, tensions, practices, gates, ADRs, dimensions", + "auth": "none", + "actors": [ + "agent", + "developer", + "ci", + "admin" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug (defaults to primary)" + } + ], + "tags": [ + "describe", + "ontology" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/graph/impact", + "description": "BFS impact graph from an ontology node; optionally traverses cross-project connections", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "node", + "kind": "string", + "constraint": "required", + "description": "Ontology node id to start from" + }, + { + "name": "depth", + "kind": "u32", + "constraint": "default=2", + "description": "Max BFS hops (capped at 5)" + }, + { + "name": "include_external", + "kind": "bool", + "constraint": "default=false", + "description": "Follow connections.ncl to external projects" + }, + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug (defaults to primary)" + } + ], + "tags": [ + "graph", + "federation" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/graph/node/{id}", + "description": "Resolve a single ontology node by id from the local cache (used by federation)", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug (defaults to primary)" + } + ], + "tags": [ + "graph", + "federation" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/health", + "description": "Daemon health check: uptime, version, feature flags, active projects", + "auth": "none", + "actors": [ + "agent", + "developer", + "ci", + "admin" + ], + "params": [], + "tags": [ + "meta" + ], + "feature": "" + }, + { + "method": "POST", + "path": "/nickel/export", + "description": "Export a Nickel file to JSON, using the cache when the file is unchanged", + "auth": "viewer", + "actors": [ + "developer", + "agent" + ], + "params": [ + { + "name": "file", + "kind": "string", + "constraint": "required", + "description": "Absolute path to the .ncl file to export" + }, + { + "name": "import_path", + "kind": "string", + "constraint": "optional", + "description": "NICKEL_IMPORT_PATH override" + } + ], + "tags": [ + "nickel", + "cache" + ], + "feature": "" + }, + { + "method": "POST", + "path": "/notifications/ack", + "description": "Acknowledge one or more notifications; removes them from the pending queue", + "auth": "none", + "actors": [ + "agent", + "developer", + "ci" + ], + "params": [ + { + "name": "token", + "kind": "string", + "constraint": "required", + "description": "Actor bearer token" + }, + { + "name": "ids", + "kind": "string", + "constraint": "required", + "description": "Comma-separated notification ids to acknowledge" + } + ], + "tags": [ + "notifications" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/notifications/pending", + "description": "Poll pending notifications for an actor; optionally marks them as seen", + "auth": "none", + "actors": [ + "agent", + "developer", + "ci" + ], + "params": [ + { + "name": "token", + "kind": "string", + "constraint": "required", + "description": "Actor bearer token" + }, + { + "name": "project", + "kind": "string", + "constraint": "optional", + "description": "Project slug filter" + }, + { + "name": "check_only", + "kind": "bool", + "constraint": "default=false", + "description": "Return count without marking seen" + } + ], + "tags": [ + "notifications" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/notifications/stream", + "description": "SSE push stream: actor subscribes once and receives notification events as they occur", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "token", + "kind": "string", + "constraint": "required", + "description": "Actor bearer token" + }, + { + "name": "project", + "kind": "string", + "constraint": "optional", + "description": "Project slug filter" + } + ], + "tags": [ + "notifications", + "sse" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/ontology", + "description": "List available ontology extension files beyond core, state, gate, manifest", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug (defaults to primary)" + } + ], + "tags": [ + "ontology" + ], + "feature": "" + }, + { + "method": "POST", + "path": "/ontology/changed", + "description": "Git hook endpoint: actor signs a file-change event it caused to suppress self-notification", + "auth": "viewer", + "actors": [ + "developer", + "ci" + ], + "params": [ + { + "name": "token", + "kind": "string", + "constraint": "required", + "description": "Actor bearer token" + }, + { + "name": "files", + "kind": "string", + "constraint": "required", + "description": "JSON array of changed file paths" + } + ], + "tags": [ + "ontology", + "notifications" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/ontology/{file}", + "description": "Export a specific ontology extension file to JSON", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug (defaults to primary)" + } + ], + "tags": [ + "ontology" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/projects", + "description": "List all registered projects with slug, root, push_only flag and import path", + "auth": "admin", + "actors": [ + "admin" + ], + "params": [], + "tags": [ + "projects", + "registry" + ], + "feature": "" + }, + { + "method": "POST", + "path": "/projects", + "description": "Register a new project at runtime without daemon restart", + "auth": "admin", + "actors": [ + "admin" + ], + "params": [], + "tags": [ + "projects", + "registry" + ], + "feature": "" + }, + { + "method": "DELETE", + "path": "/projects/{slug}", + "description": "Deregister a project and stop its file watcher", + "auth": "admin", + "actors": [ + "admin" + ], + "params": [], + "tags": [ + "projects", + "registry" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/projects/{slug}/config", + "description": "Full config export for a registered project (merged with any active overrides)", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "required", + "description": "Project slug" + } + ], + "tags": [ + "config" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/projects/{slug}/config/coherence", + "description": "Multi-consumer coherence report: unclaimed NCL fields, consumer field mismatches", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "required", + "description": "Project slug" + }, + { + "name": "section", + "kind": "string", + "constraint": "optional", + "description": "Filter to one section" + } + ], + "tags": [ + "config" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/projects/{slug}/config/quickref", + "description": "Generated config documentation with rationales, override history, and coherence status", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "required", + "description": "Project slug" + }, + { + "name": "section", + "kind": "string", + "constraint": "optional", + "description": "Filter to one section" + }, + { + "name": "format", + "kind": "string", + "constraint": "optional", + "description": "Output format (json|markdown)" + } + ], + "tags": [ + "config" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/projects/{slug}/config/schema", + "description": "Config surface schema: sections with descriptions, rationales, contracts, and declared consumers", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "required", + "description": "Project slug" + } + ], + "tags": [ + "config" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/projects/{slug}/config/{section}", + "description": "Values for a single config section (from the merged NCL export)", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "required", + "description": "Project slug" + }, + { + "name": "section", + "kind": "string", + "constraint": "required", + "description": "Section id" + } + ], + "tags": [ + "config" + ], + "feature": "" + }, + { + "method": "PUT", + "path": "/projects/{slug}/config/{section}", + "description": "Mutate a config section via the override layer. dry_run=true (default) returns the proposed change without writing.", + "auth": "admin", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "required", + "description": "Project slug" + }, + { + "name": "section", + "kind": "string", + "constraint": "required", + "description": "Section id" + } + ], + "tags": [ + "config" + ], + "feature": "" + }, + { + "method": "PUT", + "path": "/projects/{slug}/keys", + "description": "Hot-rotate credentials for a project; invalidates all existing actor and UI sessions", + "auth": "admin", + "actors": [ + "admin" + ], + "params": [], + "tags": [ + "projects", + "auth" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/projects/{slug}/ontology/versions", + "description": "Per-file ontology change counters for a project; incremented on every cache invalidation", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [], + "tags": [ + "projects", + "ontology", + "cache" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/qa-json", + "description": "Export the Q&A knowledge store as structured JSON from reflection/qa.ncl", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug (defaults to primary)" + } + ], + "tags": [ + "qa" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/search", + "description": "Full-text search over ontology nodes, ADRs, practices and Q&A entries", + "auth": "none", + "actors": [ + "agent", + "developer" + ], + "params": [ + { + "name": "q", + "kind": "string", + "constraint": "required", + "description": "Search query string" + }, + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug (ui feature only)" + } + ], + "tags": [ + "search" + ], + "feature": "" + }, + { + "method": "POST", + "path": "/sync", + "description": "Push-based sync: remote projects POST their NCL export JSON here to update the daemon cache", + "auth": "viewer", + "actors": [ + "ci", + "agent" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "required", + "description": "Project slug from Authorization header context" + } + ], + "tags": [ + "sync", + "federation" + ], + "feature": "" + }, + { + "method": "GET", + "path": "/validate/adrs", + "description": "Execute typed ADR constraint checks and return per-constraint pass/fail results", + "auth": "viewer", + "actors": [ + "developer", + "ci", + "agent" + ], + "params": [ + { + "name": "slug", + "kind": "string", + "constraint": "optional", + "description": "Project slug (defaults to primary)" + } + ], + "tags": [ + "validate", + "adrs" + ], + "feature": "" + } +] diff --git a/assets/css/package.json b/assets/css/package.json new file mode 100644 index 0000000..11a1ada --- /dev/null +++ b/assets/css/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "type": "module", + "packageManager": "pnpm@10.7.0", + "scripts": { + "build": "unocss '../../crates/ontoref-daemon/templates/**/*.html' -o ../../crates/ontoref-daemon/public/css/ontoref.css --minify", + "watch": "unocss '../../crates/ontoref-daemon/templates/**/*.html' -o ../../crates/ontoref-daemon/public/css/ontoref.css --watch" + }, + "devDependencies": { + "@unocss/cli": "^66.3.2", + "unocss": "^66.3.2", + "unocss-preset-daisy": "^7.0.0" + } +} diff --git a/assets/css/tailwind.config.js b/assets/css/tailwind.config.js new file mode 100644 index 0000000..8767b2a --- /dev/null +++ b/assets/css/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + '../../crates/ontoref-daemon/templates/**/*.html', + ], + plugins: [require('daisyui')], + daisyui: { + themes: ['dark', 'light'], + logs: false, + }, +}; diff --git a/assets/css/uno.config.js b/assets/css/uno.config.js new file mode 100644 index 0000000..b842d9f --- /dev/null +++ b/assets/css/uno.config.js @@ -0,0 +1,58 @@ +import { defineConfig, presetUno, transformerDirectives, transformerVariantGroup } from 'unocss' +import { presetDaisy } from 'unocss-preset-daisy' +import { readFileSync } from 'fs' +import { createRequire } from 'module' + +const require = createRequire(import.meta.url) +const daisyuiThemes = readFileSync(require.resolve('daisyui/dist/themes.css'), 'utf-8') + +const basePreflight = ` +*,::before,::after{box-sizing:border-box} +html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";line-height:1.5;-webkit-text-size-adjust:100%;tab-size:4} +body{margin:0;padding:0;line-height:inherit} +a{color:inherit;text-decoration:inherit} +img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle} +img,video{max-width:100%;height:auto} +h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit} +ol,ul{list-style:none;margin:0;padding:0} +button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0} +button,select{text-transform:none} +` + +// DaisyUI v3 sets --btn-text-case:uppercase per [data-theme=*] in themes.css and +// drives text-transform via that variable in component styles. Both come after +// preflights, so !important is the only reliable escape. +// svg sizing is handled in base.html via higher-specificity (.btn svg.w-N) rules. +const daisyV3Overrides = ` +.btn{text-transform:none!important;letter-spacing:normal!important} +` + +export default defineConfig({ + preflights: [ + { getCSS: () => basePreflight }, + { getCSS: () => daisyuiThemes }, + { getCSS: () => daisyV3Overrides }, + ], + content: { + filesystem: [ + '../../crates/ontoref-daemon/templates/**/*.html', + ], + }, + presets: [ + presetUno(), + presetDaisy({ themes: ['dark', 'light'] }), + ], + transformers: [ + transformerDirectives(), + transformerVariantGroup(), + ], + safelist: [ + // DaisyUI component classes assembled dynamically in JS (authBadge, statusBadge) + 'badge', 'badge-xs', 'badge-ghost', 'badge-info', 'badge-error', + 'badge-success', 'badge-warning', 'badge-neutral', 'badge-lg', 'badge-outline', + 'loading', 'loading-spinner', 'loading-sm', + // Utility classes assembled dynamically in JS + 'font-mono', 'hidden', 'line-through', + 'text-orange-400', 'text-cyan-400', 'text-purple-400', 'text-yellow-400', + ], +}) diff --git a/assets/vendor/cytoscape-navigator.js b/assets/vendor/cytoscape-navigator.js new file mode 100644 index 0000000..0f4d960 --- /dev/null +++ b/assets/vendor/cytoscape-navigator.js @@ -0,0 +1,960 @@ +;(function(){ 'use strict'; + + var defaults = { + container: false // can be a HTML or jQuery element or jQuery selector + , viewLiveFramerate: 0 // set false to update graph pan only on drag end; set 0 to do it instantly; set a number (frames per second) to update not more than N times per second + , dblClickDelay: 200 // milliseconds + , removeCustomContainer: true // destroy the container specified by user on plugin destroy + , rerenderDelay: 500 // ms to throttle rerender updates to the panzoom for performance + }; + + var debounce = (function(){ + /** + * lodash 3.1.1 (Custom Build) + * Build: `lodash modern modularize exports="npm" -o ./` + * Copyright 2012-2015 The Dojo Foundation + * Based on Underscore.js 1.8.3 + * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ + /** Used as the `TypeError` message for "Functions" methods. */ + var FUNC_ERROR_TEXT = 'Expected a function'; + + /* Native method references for those with the same name as other `lodash` methods. */ + var nativeMax = Math.max, + nativeNow = Date.now; + + /** + * Gets the number of milliseconds that have elapsed since the Unix epoch + * (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @category Date + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => logs the number of milliseconds it took for the deferred function to be invoked + */ + var now = nativeNow || function() { + return new Date().getTime(); + }; + + /** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed invocations. Provide an options object to indicate that `func` + * should be invoked on the leading and/or trailing edge of the `wait` timeout. + * Subsequent calls to the debounced function return the result of the last + * `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked + * on the trailing edge of the timeout only if the the debounced function is + * invoked more than once during the `wait` timeout. + * + * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options] The options object. + * @param {boolean} [options.leading=false] Specify invoking on the leading + * edge of the timeout. + * @param {number} [options.maxWait] The maximum time `func` is allowed to be + * delayed before it's invoked. + * @param {boolean} [options.trailing=true] Specify invoking on the trailing + * edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // avoid costly calculations while the window size is in flux + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // invoke `sendMail` when the click event is fired, debouncing subsequent calls + * jQuery('#postbox').on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // ensure `batchLog` is invoked once after 1 second of debounced calls + * var source = new EventSource('/stream'); + * jQuery(source).on('message', _.debounce(batchLog, 250, { + * 'maxWait': 1000 + * })); + * + * // cancel a debounced call + * var todoChanges = _.debounce(batchLog, 1000); + * Object.observe(models.todo, todoChanges); + * + * Object.observe(models, function(changes) { + * if (_.find(changes, { 'user': 'todo', 'type': 'delete'})) { + * todoChanges.cancel(); + * } + * }, ['delete']); + * + * // ...at some point `models.todo` is changed + * models.todo.completed = true; + * + * // ...before 1 second has passed `models.todo` is deleted + * // which cancels the debounced `todoChanges` call + * delete models.todo; + */ + function debounce(func, wait, options) { + var args, + maxTimeoutId, + result, + stamp, + thisArg, + timeoutId, + trailingCall, + lastCalled = 0, + maxWait = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = wait < 0 ? 0 : (+wait || 0); + if (options === true) { + var leading = true; + trailing = false; + } else if (isObject(options)) { + leading = !!options.leading; + maxWait = 'maxWait' in options && nativeMax(+options.maxWait || 0, wait); + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function cancel() { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (maxTimeoutId) { + clearTimeout(maxTimeoutId); + } + lastCalled = 0; + maxTimeoutId = timeoutId = trailingCall = undefined; + } + + function complete(isCalled, id) { + if (id) { + clearTimeout(id); + } + maxTimeoutId = timeoutId = trailingCall = undefined; + if (isCalled) { + lastCalled = now(); + result = func.apply(thisArg, args); + if (!timeoutId && !maxTimeoutId) { + args = thisArg = undefined; + } + } + } + + function delayed() { + var remaining = wait - (now() - stamp); + if (remaining <= 0 || remaining > wait) { + complete(trailingCall, maxTimeoutId); + } else { + timeoutId = setTimeout(delayed, remaining); + } + } + + function maxDelayed() { + complete(trailing, timeoutId); + } + + function debounced() { + args = arguments; + stamp = now(); + thisArg = this; + trailingCall = trailing && (timeoutId || !leading); + + if (maxWait === false) { + var leadingCall = leading && !timeoutId; + } else { + if (!maxTimeoutId && !leading) { + lastCalled = stamp; + } + var remaining = maxWait - (stamp - lastCalled), + isCalled = remaining <= 0 || remaining > maxWait; + + if (isCalled) { + if (maxTimeoutId) { + maxTimeoutId = clearTimeout(maxTimeoutId); + } + lastCalled = stamp; + result = func.apply(thisArg, args); + } + else if (!maxTimeoutId) { + maxTimeoutId = setTimeout(maxDelayed, remaining); + } + } + if (isCalled && timeoutId) { + timeoutId = clearTimeout(timeoutId); + } + else if (!timeoutId && wait !== maxWait) { + timeoutId = setTimeout(delayed, wait); + } + if (leadingCall) { + isCalled = true; + result = func.apply(thisArg, args); + } + if (isCalled && !timeoutId && !maxTimeoutId) { + args = thisArg = undefined; + } + return result; + } + debounced.cancel = cancel; + return debounced; + } + + /** + * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`. + * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(1); + * // => false + */ + function isObject(value) { + // Avoid a V8 JIT bug in Chrome 19-20. + // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + return debounce; + + })(); + + // ported lodash throttle function + var throttle = function( func, wait, options ){ + var leading = true, + trailing = true; + + if( options === false ){ + leading = false; + } else if( typeof options === typeof {} ){ + leading = 'leading' in options ? options.leading : leading; + trailing = 'trailing' in options ? options.trailing : trailing; + } + options = options || {}; + options.leading = leading; + options.maxWait = wait; + options.trailing = trailing; + + return debounce( func, wait, options ); + }; + + var Navigator = function ( element, options ) { + this._init(element, options) + }; + + var extend = function() { + for(var i = 1; i < arguments.length; i++) { + for(var key in arguments[i]) { + if(arguments[i].hasOwnProperty(key)) { + arguments[0][key] = arguments[i][key]; + } + } + } + return arguments[0]; + }; + + var wid = function(elem) { + return elem.getBoundingClientRect().width; + }; + + var hei = function(elem) { + return elem.getBoundingClientRect().height; + }; + + Navigator.prototype = { + + constructor: Navigator + + /**************************** + Main functions + ****************************/ + + , bb: function(){ + var bb = this.cy.elements().boundingBox() + + if( bb.w === 0 || bb.h === 0 ){ + return { + x1: 0, + x2: Infinity, + y1: 0, + y2: Infinity, + w: Infinity, + h: Infinity + } // => hide interactive overlay + } + + return bb + } + + , _addCyListener: function(events, handler){ + this._cyListeners.push({ + events: events, + handler: handler + }) + + this.cy.on(events, handler) + } + + , _removeCyListeners: function(){ + var cy = this.cy + + this._cyListeners.forEach(function(l){ + cy.off(l.events, l.handler) + }) + + cy.offRender(this._onRenderHandler) + } + + , _init: function ( cy, options ) { + this._cyListeners = [] + + this.$element = cy.container() + this.options = extend({}, defaults, options) + + this.cy = cy + + // Cache bounding box + this.boundingBox = this.bb() + + // Cache sizes + this.width = wid(this.$element); + this.height = hei(this.$element) + + // Init components + this._initPanel() + this._initThumbnail() + this._initView() + this._initOverlay() + } + + , destroy: function () { + this._removeEventsHandling(); + + // If container is not created by navigator and its removal is prohibited + if (this.options.container && !this.options.removeCustomContainer) { + this.$panel.innerHTML = ''; + } else { + this.$panel.parentElement.removeChild(this.$panel); + } + } + + /**************************** + Navigator elements functions + ****************************/ + + /* + * Used inner attributes + * + * w {number} width + * h {number} height + */ + , _initPanel: function () { + var options = this.options + if(options.container && typeof options.container === 'string' && options.container.length > 0) { + // to not break users which gives a jquery string selector + if (options.container.indexOf('#') !== -1) { + this.$panel = document.getElementById(options.container.replace('#', '')); + } else { + this.$panel = document.getElementsByClassName(options.container.replace('.', ''))[0]; + } + } else { + this.$panel = document.createElement('div'); + this.$panel.className = 'cytoscape-navigator'; + document.body.appendChild(this.$panel); + } + this._setupPanel() + this._addCyListener('resize', this.resize.bind(this)) + } + + , _setupPanel: function () { + // Cache sizes + this.panelWidth = wid(this.$panel); + this.panelHeight = hei(this.$panel); + } + + /* + * Used inner attributes + * + * zoom {number} + * pan {object} - {x: 0, y: 0} + */ + , _initThumbnail: function () { + // Create thumbnail + this.$thumbnail = document.createElement('img'); + + // Add thumbnail canvas to the DOM + this.$panel.appendChild(this.$thumbnail); + + // Setup thumbnail + this._setupThumbnailSizes() + this._setupThumbnail() + } + + , _setupThumbnail: function () { + this._updateThumbnailImage() + } + + , _setupThumbnailSizes: function () { + // Update bounding box cache + this.boundingBox = this.bb() + + this.thumbnailZoom = Math.min(this.panelHeight / this.boundingBox.h, this.panelWidth / this.boundingBox.w) + + // Used on thumbnail generation + this.thumbnailPan = { + x: (this.panelWidth - this.thumbnailZoom * (this.boundingBox.x1 + this.boundingBox.x2))/2 + , y: (this.panelHeight - this.thumbnailZoom * (this.boundingBox.y1 + this.boundingBox.y2))/2 + } + } + + // If bounding box has changed then update sizes + // Otherwise just update the thumbnail + , _checkThumbnailSizesAndUpdate: function () { + // Cache previous values + var _zoom = this.thumbnailZoom + , _pan_x = this.thumbnailPan.x + , _pan_y = this.thumbnailPan.y + + this._setupThumbnailSizes() + + if (_zoom != this.thumbnailZoom || _pan_x != this.thumbnailPan.x || _pan_y != this.thumbnailPan.y) { + this._setupThumbnail() + this._setupView() + } else { + this._updateThumbnailImage() + } + } + + /* + * Used inner attributes + * + * w {number} width + * h {number} height + * x {number} + * y {number} + * borderWidth {number} + * locked {boolean} + */ + , _initView: function () { + this.$view = document.createElement('div'); + this.$view.className = 'cytoscape-navigatorView'; + this.$panel.appendChild(this.$view) + // Compute borders + this.viewBorderTop = parseInt(this.$view.style['border-top-width'], 10) || 0; + this.viewBorderRight = parseInt(this.$view.style['border-right-width'], 10) || 0; + this.viewBorderBottom = parseInt(this.$view.style['border-bottom-width'], 10) || 0; + this.viewBorderLeft = parseInt(this.$view.style['border-left-width'], 10) || 0; + + // Abstract borders + this.viewBorderHorizontal = this.viewBorderLeft + this.viewBorderRight + this.viewBorderVertical = this.viewBorderTop + this.viewBorderBottom + + this._setupView() + + // Hook graph zoom and pan + this._addCyListener('zoom pan', this._setupView.bind(this)) + } + + , _setupView: function () { + if (this.viewLocked) + return + + var cyZoom = this.cy.zoom() + , cyPan = this.cy.pan() + + // Horizontal computation + this.viewW = this.width / cyZoom * this.thumbnailZoom + this.viewX = -cyPan.x * this.viewW / this.width + this.thumbnailPan.x - this.viewBorderLeft + + // Vertical computation + this.viewH = this.height / cyZoom * this.thumbnailZoom + this.viewY = -cyPan.y * this.viewH / this.height + this.thumbnailPan.y - this.viewBorderTop + + // CSS view + this.$view.style['width'] = this.viewW + 'px'; + this.$view.style['height'] = this.viewH + 'px'; + this.$view.style['position'] = 'absolute'; + this.$view.style['left'] = this.viewX + 'px'; + this.$view.style['top'] = this.viewY + 'px'; + } + + /* + * Used inner attributes + * + * timeout {number} used to keep stable frame rate + * lastMoveStartTime {number} + * inMovement {boolean} + * hookPoint {object} {x: 0, y: 0} + */ + , _initOverlay: function () { + // Used to capture mouse events + this.$overlay = document.createElement('div'); + this.$overlay.className = 'cytoscape-navigatorOverlay'; + + // Add overlay to the DOM + this.$panel.appendChild(this.$overlay) + + // Init some attributes + this.overlayHookPointX = 0; + this.overlayHookPointY = 0; + + // Listen for events + this._initEventsHandling() + } + + /**************************** + Event handling functions + ****************************/ + + , resize: function () { + // Cache sizes + this.width = wid(this.$element); + this.height = hei(this.$element); + this._thumbnailSetup = false + this._setupPanel() + this._checkThumbnailSizesAndUpdate() + this._setupView() + } + + , _initEventsHandling: function () { + var that = this + , eventsLocal = [ + // Mouse events + 'mousedown' + , 'mousewheel' + , 'DOMMouseScroll' // Mozilla specific event + // Touch events + , 'touchstart' + ] + , eventsGlobal = [ + 'mouseup' + , 'mouseout' + , 'mousemove' + // Touch events + , 'touchmove' + , 'touchend' + ] + + // handle events and stop their propagation + var overlayListener = function (ev) { + // Touch events + if (ev.type == 'touchstart') { + // Will count as middle of View + Object.defineProperty(ev, 'offsetX', { + value: that.viewX + that.viewW / 2, + writable: true + }); + Object.defineProperty(ev, 'offsetY', { + value: that.viewY + that.viewH / 2, + writable: true + }); + } + + // Normalize offset for browsers which do not provide that value + if (ev.offsetX === undefined || ev.offsetY === undefined) { + var rect = ev.target.getBoundingClientRect(); + var targetOffset = { + top: rect.top + window.scrollY, + left: rect.left + window.scrollX, + }; + Object.defineProperty(ev, 'offsetX', { + value: ev.pageX - targetOffset.left, + writable: true + }); + Object.defineProperty(ev, 'offsetY', { + value: ev.pageY - targetOffset.top, + writable: true + }); + } + + if (ev.type == 'mousedown' || ev.type == 'touchstart') { + that._eventMoveStart(ev) + } else if (ev.type == 'mousewheel' || ev.type == 'DOMMouseScroll') { + that._eventZoom(ev) + } + + // Prevent default and propagation + // Don't use peventPropagation as it breaks mouse events + return false; + }; + + // Hook global events + var globalListener = function (ev) { + + // Do not make any computations if it is has no effect on Navigator + if (!that.overlayInMovement) + return; + + // Touch events + if (ev.type == 'touchend') { + // Will count as middle of View + Object.defineProperty(ev, 'offsetX', { + value: that.viewX + that.viewW / 2, + writable: true + }); + Object.defineProperty(ev, 'offsetY', { + value: that.viewY + that.viewH / 2, + writable: true + }); + } else if (ev.type == 'touchmove') { + // Hack - we take in account only first touch + Object.defineProperty(ev, 'pageX', { + value: ev.originalEvent.touches[0].pageX, + writable: true + }); + Object.defineProperty(ev, 'pageY', { + value: ev.originalEvent.touches[0].pageY, + writable: true + }); + } + + // Normalize offset for browsers which do not provide that value + if (ev.offsetX === undefined || ev.offsetY === undefined) { + var rect = ev.target.getBoundingClientRect(); + var targetOffset = { + top: rect.top + window.scrollY, + left: rect.left + window.scrollX, + }; + Object.defineProperty(ev, 'offsetX', { + value: ev.pageX - targetOffset.left, + writable: true + }); + Object.defineProperty(ev, 'offsetY', { + value: ev.pageY - targetOffset.top, + writable: true + }); + } + + // Translate global events into local coordinates + if (ev.target !== that.$overlay) { + var rect = ev.target.getBoundingClientRect(); + var rect2 = that.$overlay.getBoundingClientRect(); + var targetOffset = { + top: rect.top + window.scrollY, + left: rect.left + window.scrollX, + }; + var overlayOffset = { + top: rect2.top + window.scrollY, + left: rect2.left + window.scrollX, + }; + + if(targetOffset && overlayOffset) { + Object.defineProperty(ev, 'offsetX', { + value: ev.offsetX - overlayOffset.left + targetOffset.left, + writable: true + }); + Object.defineProperty(ev, 'offsetY', { + value: ev.offsetY - overlayOffset.top + targetOffset.top, + writable: true + }); + } else { + return false; + } + } + + if (ev.type == 'mousemove' || ev.type == 'touchmove') { + that._eventMove(ev) + } else if (ev.type == 'mouseup' || ev.type == 'touchend') { + that._eventMoveEnd(ev) + } + + // Prevent default and propagation + // Don't use peventPropagation as it breaks mouse events + return false; + }; + + for (var i = 0; i < eventsLocal.length; i++) { + this.$overlay.addEventListener(eventsLocal[i], overlayListener, false); + } + + for (var i = 0; i < eventsGlobal.length; i++) { + window.addEventListener(eventsGlobal[i], globalListener, false); + } + + this._removeEventsHandling = function(){ + + for (var i = 0; i < eventsLocal.length; i++) { + this.$overlay.removeEventListener(eventsLocal[i], overlayListener); + } + + for (var i = 0; i < eventsGlobal.length; i++) { + window.removeEventListener(eventsGlobal[i], globalListener); + } + } + } + + , _eventMoveStart: function (ev) { + var now = new Date().getTime() + + // Check if it was double click + if (this.overlayLastMoveStartTime + && this.overlayLastMoveStartTime + this.options.dblClickDelay > now) { + // Reset lastMoveStartTime + this.overlayLastMoveStartTime = 0 + // Enable View in order to move it to the center + this.overlayInMovement = true + + // Set hook point as View center + this.overlayHookPointX = this.viewW / 2 + this.overlayHookPointY = this.viewH / 2 + + // Move View to start point + if (this.options.viewLiveFramerate !== false) { + this._eventMove({ + offsetX: this.panelWidth / 2 + , offsetY: this.panelHeight / 2 + }) + } else { + this._eventMoveEnd({ + offsetX: this.panelWidth / 2 + , offsetY: this.panelHeight / 2 + }) + } + + // View should be inactive as we don't want to move it right after double click + this.overlayInMovement = false + } + // This is a single click + // Take care as single click happens before double click 2 times + else { + this.overlayLastMoveStartTime = now + this.overlayInMovement = true + // Lock view moving caused by cy events + this.viewLocked = true + + // if event started in View + if (ev.offsetX >= this.viewX && ev.offsetX <= this.viewX + this.viewW + && ev.offsetY >= this.viewY && ev.offsetY <= this.viewY + this.viewH + ) { + this.overlayHookPointX = ev.offsetX - this.viewX + this.overlayHookPointY = ev.offsetY - this.viewY + } + // if event started in Thumbnail (outside of View) + else { + // Set hook point as View center + this.overlayHookPointX = this.viewW / 2 + this.overlayHookPointY = this.viewH / 2 + + // Move View to start point + this._eventMove(ev) + } + } + } + + , _eventMove: function (ev) { + var that = this + + this._checkMousePosition(ev) + + // break if it is useless event + if (!this.overlayInMovement) { + return; + } + + // Update cache + this.viewX = ev.offsetX - this.overlayHookPointX + this.viewY = ev.offsetY - this.overlayHookPointY + + // Update view position + this.$view.style['left'] = this.viewX + 'px'; + this.$view.style['top'] = this.viewY + 'px'; + + // Move Cy + if (this.options.viewLiveFramerate !== false) { + // trigger instantly + if (this.options.viewLiveFramerate == 0) { + this._moveCy() + } + // trigger less often than frame rate + else if (!this.overlayTimeout) { + // Set a timeout for graph movement + this.overlayTimeout = setTimeout(function () { + that._moveCy() + that.overlayTimeout = false + }, 1000 / this.options.viewLiveFramerate) + } + } + } + + , _checkMousePosition: function (ev) { + // If mouse in over View + if(ev.offsetX > this.viewX && ev.offsetX < this.viewX + this.viewBorderHorizontal + this.viewW + && ev.offsetY > this.viewY && ev.offsetY < this.viewY + this.viewBorderVertical + this.viewH) { + this.$panel.classList.add('mouseover-view') + } else { + this.$panel.classList.remove('mouseover-view') + } + } + + , _eventMoveEnd: function (ev) { + // Unlock view changing caused by graph events + this.viewLocked = false + + // Remove class when mouse is not over Navigator + this.$panel.classList.remove('mouseover-view') + + if (!this.overlayInMovement) { + return; + } + + // Trigger one last move + this._eventMove(ev) + + // If mode is not live then move graph on drag end + if (this.options.viewLiveFramerate === false) { + this._moveCy() + } + + // Stop movement permission + this.overlayInMovement = false + } + + , _eventZoom: function (ev) { + var ev2 = extend({}, ev.originalEvent); + var delta = ev.wheelDeltaY / 1000 || ev.wheelDelta / 1000 || ev.detail / -32 || ev2.wheelDeltaY / 1000 || ev2.wheelDelta / 1000 || ev2.detail / -32; + var zoomRate = Math.pow(10, delta) + , mousePosition = { + left: ev.offsetX + , top: ev.offsetY + } + + if (this.cy.zoomingEnabled()) { + this._zoomCy(zoomRate, mousePosition) + } + } + + , _updateThumbnailImage: function () { + var that = this; + + if( this._thumbnailUpdating ){ + return; + } + + this._thumbnailUpdating = true; + + var render = function() { + that._checkThumbnailSizesAndUpdate(); + that._setupView(); + + var $img = that.$thumbnail; + var img = $img; + + var w = that.panelWidth; + var h = that.panelHeight; + var bb = that.boundingBox; + var zoom = Math.min( w/bb.w, h/bb.h ); + + var png = that.cy.png({ + full: true, + scale: zoom, + maxHeight: h, + maxWidth: w + }); + if( png.indexOf('image/png') < 0 ){ + img.removeAttribute( 'src' ); + } else { + img.setAttribute( 'src', png ); + } + + var translate = { + x: (w - zoom*( bb.w ))/2, + y: (h - zoom*( bb.h ))/2 + }; + + $img.style['position'] = 'absolute'; + $img.style['left'] = translate.x + 'px'; + $img.style['top'] = translate.y + 'px'; + + } + + this._onRenderHandler = throttle(render, that.options.rerenderDelay) + + this.cy.onRender( this._onRenderHandler ) + } + + /**************************** + Navigator view moving + ****************************/ + + , _moveCy: function () { + this.cy.pan({ + x: -(this.viewX + this.viewBorderLeft - this.thumbnailPan.x) * this.width / this.viewW + , y: -(this.viewY + this.viewBorderLeft - this.thumbnailPan.y) * this.height / this.viewH + }) + } + + /** + * Zooms graph. + * + * @this {cytoscapeNavigator} + * @param {number} zoomRate The zoom rate value. 1 is 100%. + */ + , _zoomCy: function (zoomRate, zoomCenterRaw) { + var zoomCenter + , isZoomCenterInView = false + + zoomCenter = { + x: this.width / 2 + , y: this.height / 2 + }; + + this.cy.zoom({ + level: this.cy.zoom() * zoomRate + , renderedPosition: zoomCenter + }) + } + } + + // registers the extension on a cytoscape lib ref + var register = function( cytoscape ){ + + if (!cytoscape){ return; } // can't register if cytoscape unspecified + + cytoscape( 'core', 'navigator', function( options ){ + var cy = this; + + return new Navigator( cy, options ); + } ); + + }; + + if (typeof module !== 'undefined' && module.exports) { // expose as a commonjs module + module.exports = function( cytoscape ){ + register( cytoscape ); + }; + } else if (typeof define !== 'undefined' && define.amd) { // expose as an amd/requirejs module + define('cytoscape-navigator', function(){ + return register; + }); + } + + if (typeof cytoscape !== 'undefined') { // expose to global cytoscape (i.e. window.cytoscape) + register(cytoscape); + } + +})(); diff --git a/assets/web/index.html b/assets/web/index.html index 2810c1d..f59b1a5 100644 --- a/assets/web/index.html +++ b/assets/web/index.html @@ -1 +1 @@ - Ontoref
Architecture
Protocol + Runtime · v0.1.0
Ontoref — Self-Describing Ontology and Reflection Protocol

Structure that remembers why.

Self-Describing Protocol for
Evolving Systems

Ontology + Reflection + Daemon + MCP — encode what a system IS (invariants, tensions, constraints) and where it IS GOING (state dimensions, transition conditions, membranes) in machine-queryable directed acyclic graphs. Software projects, personal operational systems, agent contexts — same three files, same protocol. First-class web UI (12 pages), MCP server (19 tools), live session sharing. One protocol for developers, agents, CI, and individuals.
Protocol + Runtime. Zero enforcement.

The 7 Problems It Solves

01

Decisions Without Memory

  • Architectural choices made in chat, forgotten after rotation
  • No machine-queryable source of why something exists
  • ADRs as typed Nickel: invariants, constraints, supersession chain
  • Hard constraints enforced at every operation
02

Invisible Configuration Drift

  • Configs change outside any review cycle
  • No audit trail linking change to PR or ADR
  • Rollback requires manual file archaeology
  • Sealed profiles: sha256 hash, full history, verified rollback
03

Agents Without Context

  • LLMs start each session with zero project knowledge
  • Same mistakes, same questions, no accumulation across operations
  • Actor registry tracks each session token, type, current mode, last seen — persisted to disk
  • MCP tools give agents direct DAG read/write: nodes, ADRs, backlog, Q&A
  • Composed tasks shared via daemon — multiple actors see the same operational context live
04

Scattered Project Knowledge

  • Guidelines in wikis, patterns in docs, decisions in Slack
  • No single source queryable by humans, agents, and CI equally
  • .ontology/ separates three orthogonal concerns: core.ncl (what IS) · state.ncl (where we ARE vs want to BE) · gate.ncl (when READY to cross a boundary)
  • reflection/ reads all three and answers self-knowledge queries — an agent understands the project without reading code, only by consulting the declarative graph
05

Protocol Fragmentation

  • Each project re-invents its own conventions
  • No shared contract for how operations are defined and executed
  • Reflection modes: typed DAG contracts for any workflow
  • One protocol adopted per-project, without enforcing uniformity
06

Knowledge Lost Between Sessions

  • Q&A answered in one session forgotten by the next
  • Agent re-asks questions already answered in previous sessions
  • Q&A Knowledge Store: typed NCL, git-versioned, persists across browser resets
  • Notification barrier surfaces drift to agents proactively — pre_commit, drift, ontology_drift signals block until acknowledged
07

Decisions Without a Map

  • Personal and professional decisions made against implicit, unverifiable assumptions
  • No queryable model of what you never compromise
  • No structured way to ask: does this opportunity violate who I am?
  • ontoref as personal operational ontology — same core/state/gate files applied to life, career, and ecosystem dimensions
  • jpl validate "accept offer" → invariants_at_risk, relevant edges, verdict

Ontology & Reflection — Yin and Yang

Yin — The Ontology Layer

What must be true

  • Invariants — axioms that cannot change without a new ADR
  • Tensions — structural conflicts the project navigates, never resolves
  • Practices — confirmed patterns with artifact paths to real files and declared ADR validators
  • Gates — membranes controlling readiness thresholds
  • Dimensions — current vs desired state, with transition conditions
  • Q&A Knowledge Store — accumulated Q&A persisted to NCL, git-versioned, queryable by any actor

Yang — The Reflection Layer

How things move and change

  • Modes — typed DAG workflow contracts (preconditions, steps, postconditions)
  • Forms — parameter collection driving modes
  • ADR lifecycle — Proposed → Accepted → Superseded, with constraint history
  • Actors — developer / agent / CI, same protocol, different capabilities
  • Config seals — sha256-sealed profiles, drift detection, rollback
  • Quick Actions — runnable shortcuts over modes; configured in .ontoref/config.ncl
  • Passive Drift Observer — watches code changes, emits ontology_drift notifications with missing/stale/drift/broken counts
Ontology without Reflection = correct but static. Perfect invariants with no operations = dead documentation.
Reflection without Ontology = fluid but unanchored. Workflows that forget what they protect.

The protocol lives in coexistence.

DECLARATIVE LAYER · Nickel
.ontology/ · adrs/ · reflection/schemas/
Strong types, contracts, enums. Fails at definition time, not at runtime.
OPERATIONAL LAYER · Nushell
adr · register · config · backlog · forms · describe
Typed pipelines over structured data. No text streams.
ENTRY POINT · Bash → Nu
ontoref · actor detection · advisory locking · ONTOREF_IMPORT_PATH
Single entry point per project. Detects actor (developer/agent/CI), acquires lock, dispatches to correct Nu module.
KNOWLEDGE GRAPH · .ontology/
nodes · invariants · tensions · gates · dimensions · states
The project knows what it knows. Actor-agnostic. Machine-queryable via nickel export.
RUNTIME LAYER · Rust + axum
ontoref-daemon · ontoref-ontology · ontoref-reflection · search engine · notification barrier · SurrealDB (optional)
Optional persistent daemon. NCL export cache, HTTP UI (12 pages), MCP server (19 tools), actor registry, notification store, search engine, SurrealDB persistence. Never a protocol requirement.
ADOPTION LAYER · Per-project
.ontoref/config.ncl · ontoref CLI · adopt_ontoref mode
Each project maintains its own .ontology/ data. Ontoref provides the schemas, modules, and migration scripts. Zero lock-in.

Crates & Tooling

🧩

ontoref-ontology

  • Load and query .ontology/ NCL files as typed Rust structs
  • Node, Edge, Dimension, Gate, Membrane types — Node carries artifact_paths and adrs, both serde(default)
  • Graph traversal: callers, callees, impact queries
  • Invariant extraction and constraint validation
  • Zero stratumiops dependencies — minimal adoption surface (ADR-001)
🔄

ontoref-reflection

  • Execute reflection modes as typed NCL DAG contracts
  • Step execution with dependency resolution
  • ADR lifecycle: Proposed → Accepted → Superseded
  • Config seal and rollback operations
  • stratum-graph + stratum-state required; platform-nats feature-gated
📜

Nushell Modules

  • store.nu — SurrealDB-backed cache with NCL export
  • sync.nu — ontology code synchronization
  • describe.nu — actor-aware project self-knowledge
  • coder.nu — structured session records
  • 16 modules total — one per operational domain
⚙️

Nickel Schemas

  • Core ontology types: Node, Edge, Pole, AbstractionLevel
  • State machine types: Dimension, Transition, Gate, Membrane
  • ADR schema: Constraint, Severity, Status, supersession
  • Reflection schema: Mode, Step, OnError, Dependency
🖥️

ontoref-daemon · HTTP & UI

  • HTTP UI (axum + Tera): 12 pages — dashboard, graph, search, sessions, notifications, backlog, Q&A, actions, modes, compose, manage/login, manage/logout
  • Graph node detail panel: artifacts, connections, and ADR validators — each ADR is a clickable link that opens the full record via GET /api/adr/{id}
  • Actor registry (DashMap): token, type (developer / agent / CI), registered_at, last_seen, current_mode — serializable snapshot
  • Notification barrier: pre_commit · drift · ontology_drift — pre-commit hook polls & blocks on ack
  • Compose / live sharing: mode forms rendered interactively, ./ontoref dispatched server-side, shared across actors
  • File watcher (notify): passive drift observer, no polling

ontoref-daemon · MCP & Data

  • MCP server: stdio + streamable-HTTP, 19 tools — nodes, ADRs, modes, backlog, Q&A, sessions, search, notifications
  • Search engine: full-text across nodes / ADRs / reflection modes — returns kind · id · title · snippet · score
  • SurrealDB persistence (optional --db): actor sessions, seeded ontology tables, search index, notification history — fail-open
  • NCL export cache: avoids repeated nickel export on unchanged files
  • db + nats feature flags — builds standalone with --no-default-features

Adopt in Any Project

ontoref setup wires up any new or existing project — idempotent scaffold with optional auth key bootstrap.

stratumiopsMaster orchestration repo
vaporaAI agent orchestration
kogralKnowledge graph + MCP
syntaxisProject orchestration
provisioningDeclarative IaC
your-projectAny codebase
# Onboard a new project (idempotent; kind: Service by default)
ontorefsetup
ontorefsetup --kind Library
ontorefsetup --gen-keys ["admin:dev" "viewer:ci"]# bootstrap auth keys once

# Query the project self-knowledge
ontoref describe project
ontoref describe constraints
ontoref describe impact ontology-node-id

# ADR lifecycle
ontoref adr new --title "Adopt Nickel for configuration"
ontoref adr list --status Accepted

Daemon & MCP — Runtime Intelligence Layer

ontoref-daemon is an optional persistent process. It caches NCL exports, serves 12 UI pages, exposes 19 MCP tools, maintains an actor registry, stores notifications, indexes everything for search, and optionally persists to SurrealDB. Auth is opt-in: all surfaces (CLI, UI, MCP) exchange a project key for a UUID v4 session token via POST /sessions; CLI injects ONTOREF_TOKEN as Bearer automatically. It never changes the protocol — it accelerates and shares access to it. Configured via ~/.config/ontoref/config.ncl (Nickel, type-checked); edit interactively with ontoref config-edit. Started via NCL pipe bootstrap: ontoref-daemon-boot.

The Web UI — 12 Pages
localhost:7421/ui/{slug}/
DashboardGraphSearchSessionsNotifBacklogQ&AActionsModesCompose
/Dashboardproject overview, actor count, cache stats, notification count, backlog summary
/graphGraphCytoscape.js ontology graph — nodes colored by pole (Yang=orange, Yin=blue, Spiral=purple), clickable detail panel with artifacts, connections, and ADR links that open the full record in a modal
/searchSearchfull-text search across nodes, ADRs, reflection modes — returns kind/id/title/snippet/score
/sessionsSessionslive actor registry — actor type, mode, last_seen; auth sessions (id, role, key_label, expires) for authed deployments
/notificationsNotificationsnotification feed — pre_commit / drift / ontology_drift; ack/dismiss; emit custom; action buttons
/backlogBacklogitems with priority (Critical/High/Medium/Low) and status (Open/InProgress/Done/Cancelled); add/update
/qaQ&Aserver-hydrated from reflection/qa.ncl; add/edit/delete; persisted as typed NCL
/actionsActionsquick actions catalog from .ontoref/config.ncl; execute via POST /actions/run
/modesModesreflection mode list from reflection/modes/ — name, description, DAG contract
/composeComposeagent task composer — renders mode forms interactively; POST /compose/send dispatches to ./ontoref; live sharing for AI actors
The MCP Server — 19 Tools
Tool Description
ontoref_help List available tools and usage
ontoref_list_projects Enumerate all registered projects
ontoref_set_project Set session default project context
ontoref_project_status Full project dashboard — health, drift, actors
ontoref_describe Architecture overview and self-description
ontoref_search Free-text search across nodes, ADRs, modes
ontoref_get Fetch ontology node by id
ontoref_get_node Full ontology node with edges and constraints
ontoref_list_adrs List ADRs filtered by status
ontoref_get_adr Full ADR content with constraints
ontoref_list_modes List all reflection modes
ontoref_get_mode Mode DAG contract — steps, preconditions, postconditions
ontoref_get_backlog Backlog items filtered by status
ontoref_backlog Add or update_status on a backlog item
ontoref_constraints All hard + soft architectural constraints
ontoref_qa_list List Q&A knowledge store with optional filter
ontoref_qa_add Persist new Q&A entry to reflection/qa.ncl
ontoref_action_list Quick actions catalog from .ontoref/config.ncl
ontoref_action_add Create reflection mode + register as quick action

SurrealDB Persistence — Optional

  • Enabled with --db feature flag and --db-url ws://...
  • Connects via WebSocket at startup — 5s timeout, fail-open (daemon runs without it)
  • Seeds ontology tables from local NCL files on startup and on file changes
  • Persists: actor sessions, seeded ontology tables, search index, notification history
  • Without --db: DashMap-backed in-memory, process-lifetime only
  • Namespace configurable via --db-namespace; credentials via --db-username/--db-password

Notification Barrier

  • pre_commit — pre-commit hook polls GET /notifications/pending?token=X&project=Y; blocks git commit until all acked
  • drift — schema drift detected between codebase and ontology
  • ontology_drift — emitted by passive observer with missing/stale/drift/broken counts after 15s debounce
  • Fail-open: if daemon is unreachable, pre-commit hook passes — commits are never blocked by daemon downtime
  • Ack via UI or POST /notifications/ack; custom notifications via POST /{slug}/notifications/emit
  • Action buttons in notifications can link to any dashboard page
# Configure and start the daemon (optional — protocol works without it)
ontoref config-edit # browser form → ~/.config/ontoref/config.ncl
ontoref-daemon-boot # NCL pipe bootstrap: nickel export config.ncl | daemon --config-stdin
ontoref-daemon-boot --dry-run # preview composed JSON without starting
# With SOPS-encrypted secrets merged at boot
ontoref-daemon-boot --sops secrets.enc.json

# Connect Claude Code via MCP (add to .claude/mcp.json)
{
  "mcpServers": {
    "ontoref": {"type": "http", "url": "http://localhost:7421/mcp"}
  }
}

# Search across ontology nodes, ADRs, and reflection modes
ontoref_search({ q: "notification drift", project: "my-project" })

# Persist a Q&A entry (written to reflection/qa.ncl, git-versioned)
ontoref_qa_add({
  question: "Why does ontoref-ontology have zero stratumiops deps?",
  answer: "ADR-001: minimal adoption surface. Ontology crate must build standalone.",
  tags: ["adr-001", "architecture"]
})

# Check live actor sessions
curl http://localhost:7421/actors
# {"sessions": [{"token": "abc123", "actor_type": "agent", "current_mode": "describe", ...}]}

The UI in Action · Graph View

Force-directed graph of the live ontology. Nodes are typed (Axiom · Tension · Practice) and polarized (Yang · Yin · Spiral). Click any node to open its detail panel — artifacts, connections, NCL source.

Ontoref Graph View — force-directed ontology graph, dark mode
Yang · Axiom
Yin · Tension
Spiral · Practice
Filter buttons · Edge labels · Node detail panel

Technology Stack

Rust Edition 2021NickelNushell 0.111+axumTera TemplatesDashMapnotifyMCP ProtocolD3.jsServer-Sent EventsSurrealDBNATS JetStreamSHA-256 SealsDAG ContractsshellcheckPOSIX Advisory Locks

Protocol Metrics

3 Rust Cratesontology · reflection · daemon
19 MCP ToolsAI agent integration · stdio + HTTP
1 Web UI · 12 Pagesdashboard · graph · search · sessions · notifications · backlog · Q&A · actions · modes · compose
6 Protocol LayersDeclarative → Adoption
1 Search Enginenodes · ADRs · reflection modes
16 Nu ModulesStructured data pipelines
8+ Reflection ModesDAG workflow contracts
3 Actor Typesdeveloper / agent / CI
0 EnforcementVoluntary adoption

Structure That Remembers Why

Start with ontoref setup. Your project gains machine-queryable invariants, living ADRs, actor-aware operational modes, and a daemon that shares context across every actor in real time.

Explore the Protocol

Ontoref — A Self-Describing Ontology & Reflection Protocol for Evolving Codebases

Protocol + Runtime. Zero enforcement. One graph per project.

+ Ontoref
Architecture
Protocol + Runtime · v0.1.0
Ontoref — Self-Describing Ontology and Reflection Protocol

Structure that remembers why

Self-Describing Protocol for
Evolving Systems

Ontology + Reflection + Daemon + MCP — encode what a system IS (invariants, tensions, constraints) and where it IS GOING (state dimensions, transition conditions, membranes) in machine-queryable directed acyclic graphs. Software projects, personal operational systems, agent contexts — same three files, same protocol. First-class web UI (11 pages), MCP server (19 tools), live session sharing. One protocol for developers, agents, CI, and individuals.
Protocol + Runtime. Zero enforcement.

The 7 Problems It Solves

01

Decisions Without Memory

  • Architectural choices made in chat, forgotten after rotation
  • No machine-queryable source of why something exists
  • ADRs as typed Nickel: invariants, constraints, supersession chain
  • Hard constraints enforced at every operation
02

Invisible Configuration Drift

  • Configs change outside any review cycle
  • No audit trail linking change to PR or ADR
  • Rollback requires manual file archaeology
  • Sealed profiles: sha256 hash, full history, verified rollback
03

Agents Without Context

  • LLMs start each session with zero project knowledge
  • Same mistakes, same questions, no accumulation across operations
  • Actor registry tracks each session token, type, current mode, last seen — persisted to disk
  • MCP tools give agents direct DAG read/write: nodes, ADRs, backlog, Q&A
  • Composed tasks shared via daemon — multiple actors see the same operational context live
04

Scattered Project Knowledge

  • Guidelines in wikis, patterns in docs, decisions in Slack
  • No single source queryable by humans, agents, and CI equally
  • .ontology/ separates three orthogonal concerns: core.ncl (what IS) · state.ncl (where we ARE vs want to BE) · gate.ncl (when READY to cross a boundary)
  • reflection/ reads all three and answers self-knowledge queries — an agent understands the project without reading code, only by consulting the declarative graph
05

Protocol Fragmentation

  • Each project re-invents its own conventions
  • No shared contract for how operations are defined and executed
  • Reflection modes: typed DAG contracts for any workflow
  • One protocol adopted per-project, without enforcing uniformity
06

Knowledge Lost Between Sessions

  • Q&A answered in one session forgotten by the next
  • Agent re-asks questions already answered in previous sessions
  • Q&A Knowledge Store: typed NCL, git-versioned, persists across browser resets
  • Notification barrier surfaces drift to agents proactively — pre_commit, drift, ontology_drift signals block until acknowledged
07

Decisions Without a Map

  • Personal and professional decisions made against implicit, unverifiable assumptions
  • No queryable model of what you never compromise
  • No structured way to ask: does this opportunity violate who I am?
  • ontoref as personal operational ontology — same core/state/gate files applied to life, career, and ecosystem dimensions
  • jpl validate "accept offer" → invariants_at_risk, relevant edges, verdict

Ontology & Reflection — Yin and Yang

Yin — The Ontology Layer

What must be true

  • Invariants — axioms that cannot change without a new ADR
  • Tensions — structural conflicts the project navigates, never resolves
  • Practices — confirmed patterns with artifact paths to real files and declared ADR validators
  • Gates — membranes controlling readiness thresholds
  • Dimensions — current vs desired state, with transition conditions
  • Q&A Knowledge Store — accumulated Q&A persisted to NCL, git-versioned, queryable by any actor

Yang — The Reflection Layer

How things move and change

  • Modes — typed DAG workflow contracts (preconditions, steps, postconditions)
  • Forms — parameter collection driving modes
  • ADR lifecycle — Proposed → Accepted → Superseded, with constraint history
  • Actors — developer / agent / CI, same protocol, different capabilities
  • Config seals — sha256-sealed profiles, drift detection, rollback
  • Quick Actions — runnable shortcuts over modes; configured in .ontoref/config.ncl
  • Passive Drift Observer — watches code changes, emits ontology_drift notifications with missing/stale/drift/broken counts
Ontology without Reflection = correct but static. Perfect invariants with no operations = dead documentation.
Reflection without Ontology = fluid but unanchored. Workflows that forget what they protect.

The protocol lives in coexistence.

DECLARATIVE LAYER · Nickel
.ontology/ · adrs/ · reflection/schemas/
Strong types, contracts, enums. Fails at definition time, not at runtime.
OPERATIONAL LAYER · Nushell
adr · register · config · backlog · forms · describe
Typed pipelines over structured data. No text streams.
ENTRY POINT · Bash → Nu
ontoref · actor detection · advisory locking · ONTOREF_IMPORT_PATH
Single entry point per project. Detects actor (developer/agent/CI), acquires lock, dispatches to correct Nu module.
KNOWLEDGE GRAPH · .ontology/
nodes · invariants · tensions · gates · dimensions · states
The project knows what it knows. Actor-agnostic. Machine-queryable via nickel export.
RUNTIME LAYER · Rust + axum
ontoref-daemon · ontoref-ontology · ontoref-reflection · search engine · notification barrier · SurrealDB (optional)
Optional persistent daemon. NCL export cache, HTTP UI (11 pages), MCP server (29 tools), actor registry, notification store, search engine, SurrealDB persistence. Never a protocol requirement.
ADOPTION LAYER · Per-project
.ontoref/config.ncl · ontoref CLI · adopt_ontoref mode
Each project maintains its own .ontology/ data. Ontoref provides the schemas, modules, and migration scripts. Zero lock-in.

Crates & Tooling

🧩

ontoref-ontology

  • Load and query .ontology/ NCL files as typed Rust structs
  • Node, Edge, Dimension, Gate, Membrane types — Node carries artifact_paths and adrs, both serde(default)
  • Graph traversal: callers, callees, impact queries
  • Invariant extraction and constraint validation
  • Zero stratumiops dependencies — minimal adoption surface (ADR-001)
🔄

ontoref-reflection

  • Execute reflection modes as typed NCL DAG contracts
  • Step execution with dependency resolution
  • ADR lifecycle: Proposed → Accepted → Superseded
  • Config seal and rollback operations
  • stratum-graph + stratum-state required; platform-nats feature-gated
📜

Nushell Modules

  • store.nu — SurrealDB-backed cache with NCL export
  • sync.nu — ontology code synchronization
  • describe.nu — actor-aware project self-knowledge
  • coder.nu — structured session records
  • 16 modules total — one per operational domain
⚙️

Nickel Schemas

  • Core ontology types: Node, Edge, Pole, AbstractionLevel
  • State machine types: Dimension, Transition, Gate, Membrane
  • ADR schema: Constraint, Severity, Status, supersession
  • Reflection schema: Mode, Step, OnError, Dependency
🖥️

ontoref-daemon · HTTP & UI

  • HTTP UI (axum + Tera): 11 pages — dashboard, graph, search, sessions, notifications, backlog, Q&A, actions, modes, compose, manage/login, manage/logout
  • Graph node detail panel: artifacts, connections, and ADR validators — each ADR is a clickable link that opens the full record via GET /api/adr/{id}
  • Actor registry (DashMap): token, type (developer / agent / CI), registered_at, last_seen, current_mode — serializable snapshot
  • Notification barrier: pre_commit · drift · ontology_drift — pre-commit hook polls & blocks on ack
  • Compose / live sharing: mode forms rendered interactively, ./ontoref dispatched server-side, shared across actors
  • File watcher (notify): passive drift observer, no polling

ontoref-daemon · MCP & Data

  • MCP server: stdio + streamable-HTTP, 19 tools — nodes, ADRs, modes, backlog, Q&A, sessions, search, notifications
  • Search engine: full-text across nodes / ADRs / reflection modes — returns kind · id · title · snippet · score
  • SurrealDB persistence (optional --db): actor sessions, seeded ontology tables, search index, notification history — fail-open
  • NCL export cache: avoids repeated nickel export on unchanged files
  • db + nats feature flags — builds standalone with --no-default-features

Adopt in Any Project

ontoref setup wires up any new or existing project — idempotent scaffold with optional auth key bootstrap.

stratumiopsMaster orchestration repo
vaporaAI agent orchestration
kogralKnowledge graph + MCP
syntaxisProject orchestration
provisioningDeclarative IaC
your-projectAny codebase
# Onboard a new project (idempotent; kind: Service by default)
ontorefsetup
ontorefsetup --kind Library
ontorefsetup --gen-keys ["admin:dev" "viewer:ci"]# bootstrap auth keys once

# Query the project self-knowledge
ontoref describe project
ontoref describe constraints
ontoref describe impact ontology-node-id

# ADR lifecycle
ontoref adr new --title "Adopt Nickel for configuration"
ontoref adr list --status Accepted

Daemon & MCP — Runtime Intelligence Layer

ontoref-daemon is an optional persistent process. It caches NCL exports, serves 11 UI pages, exposes 29 MCP tools, maintains an actor registry, stores notifications, indexes everything for search, and optionally persists to SurrealDB. The annotated API surface is discoverable at GET /api/catalog (populated at link time via #[onto_api] proc-macro). Auth is opt-in: all surfaces (CLI, UI, MCP) exchange a project key for a UUID v4 session token via POST /sessions; CLI injects ONTOREF_TOKEN as Bearer automatically. It never changes the protocol — it accelerates and shares access to it. Configured via ~/.config/ontoref/config.ncl (Nickel, type-checked); edit interactively with ontoref config-edit. Started via NCL pipe bootstrap: ontoref-daemon-boot.

The Web UI — 12 Pages
localhost:7421/ui/{slug}/
DashboardGraphSearchSessionsNotifBacklogQ&AActionsModesCompose
/Dashboardproject overview, actor count, cache stats, notification count, backlog summary
/graphGraphCytoscape.js ontology graph — nodes colored by pole (Yang=orange, Yin=blue, Spiral=purple), clickable detail panel with artifacts, connections, and ADR links that open the full record in a modal
/searchSearchfull-text search across nodes, ADRs, reflection modes — returns kind/id/title/snippet/score
/sessionsSessionslive actor registry — actor type, mode, last_seen; auth sessions (id, role, key_label, expires) for authed deployments
/notificationsNotificationsnotification feed — pre_commit / drift / ontology_drift; ack/dismiss; emit custom; action buttons
/backlogBacklogitems with priority (Critical/High/Medium/Low) and status (Open/InProgress/Done/Cancelled); add/update
/qaQ&Aserver-hydrated from reflection/qa.ncl; add/edit/delete; persisted as typed NCL
/actionsActionsquick actions catalog from .ontoref/config.ncl; execute via POST /actions/run
/modesModesreflection mode list from reflection/modes/ — name, description, DAG contract
/composeComposeagent task composer — renders mode forms interactively; POST /compose/send dispatches to ./ontoref; live sharing for AI actors
The MCP Server — 19 Tools
Tool Description
ontoref_help List available tools and usage
ontoref_list_projects Enumerate all registered projects
ontoref_set_project Set session default project context
ontoref_project_status Full project dashboard — health, drift, actors
ontoref_describe Architecture overview and self-description
ontoref_search Free-text search across nodes, ADRs, modes
ontoref_get Fetch ontology node by id
ontoref_get_node Full ontology node with edges and constraints
ontoref_list_adrs List ADRs filtered by status
ontoref_get_adr Full ADR content with constraints
ontoref_list_modes List all reflection modes
ontoref_get_mode Mode DAG contract — steps, preconditions, postconditions
ontoref_get_backlog Backlog items filtered by status
ontoref_backlog Add or update_status on a backlog item
ontoref_constraints All hard + soft architectural constraints
ontoref_qa_list List Q&A knowledge store with optional filter
ontoref_qa_add Persist new Q&A entry to reflection/qa.ncl
ontoref_action_list Quick actions catalog from .ontoref/config.ncl
ontoref_action_add Create reflection mode + register as quick action

SurrealDB Persistence — Optional

  • Enabled with --db feature flag and --db-url ws://...
  • Connects via WebSocket at startup — 5s timeout, fail-open (daemon runs without it)
  • Seeds ontology tables from local NCL files on startup and on file changes
  • Persists: actor sessions, seeded ontology tables, search index, notification history
  • Without --db: DashMap-backed in-memory, process-lifetime only
  • Namespace configurable via --db-namespace; credentials via --db-username/--db-password

Notification Barrier

  • pre_commit — pre-commit hook polls GET /notifications/pending?token=X&project=Y; blocks git commit until all acked
  • drift — schema drift detected between codebase and ontology
  • ontology_drift — emitted by passive observer with missing/stale/drift/broken counts after 15s debounce
  • Fail-open: if daemon is unreachable, pre-commit hook passes — commits are never blocked by daemon downtime
  • Ack via UI or POST /notifications/ack; custom notifications via POST /{slug}/notifications/emit
  • Action buttons in notifications can link to any dashboard page
# Configure and start the daemon (optional — protocol works without it)
ontoref config-edit # browser form → ~/.config/ontoref/config.ncl
ontoref-daemon-boot # NCL pipe bootstrap: nickel export config.ncl | daemon --config-stdin
ontoref-daemon-boot --dry-run # preview composed JSON without starting
# With SOPS-encrypted secrets merged at boot
ontoref-daemon-boot --sops secrets.enc.json

# Connect Claude Code via MCP (add to .claude/mcp.json)
{
  "mcpServers": {
    "ontoref": {"type": "http", "url": "http://localhost:7421/mcp"}
  }
}

# Search across ontology nodes, ADRs, and reflection modes
ontoref_search({ q: "notification drift", project: "my-project" })

# Persist a Q&A entry (written to reflection/qa.ncl, git-versioned)
ontoref_qa_add({
  question: "Why does ontoref-ontology have zero stratumiops deps?",
  answer: "ADR-001: minimal adoption surface. Ontology crate must build standalone.",
  tags: ["adr-001", "architecture"]
})

# Check live actor sessions
curl http://localhost:7421/actors
# {"sessions": [{"token": "abc123", "actor_type": "agent", "current_mode": "describe", ...}]}

The UI in Action · Graph View

Force-directed graph of the live ontology. Nodes are typed (Axiom · Tension · Practice) and polarized (Yang · Yin · Spiral). Click any node to open its detail panel — artifacts, connections, NCL source.

Ontoref Graph View — force-directed ontology graph, dark mode
Yang · Axiom
Yin · Tension
Spiral · Practice
Filter buttons · Edge labels · Node detail panel

Technology Stack

Rust Edition 2021NickelNushell 0.111+axumTera TemplatesDashMapnotifyMCP ProtocolCytoscape.jsServer-Sent EventsSurrealDBNATS JetStreamSHA-256 SealsDAG ContractsshellcheckPOSIX Advisory Locks

Protocol Metrics

3 Rust Cratesontology · reflection · daemon
19 MCP ToolsAI agent integration · stdio + HTTP
1 Web UI · 12 Pagesdashboard · graph · search · sessions · notifications · backlog · Q&A · actions · modes · compose
6 Protocol LayersDeclarative → Adoption
1 Search Enginenodes · ADRs · reflection modes
16 Nu ModulesStructured data pipelines
8+ Reflection ModesDAG workflow contracts
3 Actor Typesdeveloper / agent / CI
0 EnforcementVoluntary adoption

Structure That Remembers Why

Start with ontoref setup. Your project gains machine-queryable invariants, living ADRs, actor-aware operational modes, and a daemon that shares context across every actor in real time.

Explore the Protocol

Ontoref — A Self-Describing Ontology & Reflection Protocol for Evolving Codebases

Protocol + Runtime. Zero enforcement. One graph per project.

diff --git a/assets/web/src/index.html b/assets/web/src/index.html index 7f4847a..10955e0 100644 --- a/assets/web/src/index.html +++ b/assets/web/src/index.html @@ -2728,7 +2728,7 @@ DashMap notify MCP Protocol - D3.js + Cytoscape.js Server-Sent Events SurrealDB NATS JetStream diff --git a/crates/ontoref-daemon/src/api_catalog.rs b/crates/ontoref-daemon/src/api_catalog.rs index 8339c56..362ef89 100644 --- a/crates/ontoref-daemon/src/api_catalog.rs +++ b/crates/ontoref-daemon/src/api_catalog.rs @@ -1,36 +1,4 @@ -/// A single query/path/body parameter declared on an API route. -#[derive(serde::Serialize, Clone)] -pub struct ApiParam { - pub name: &'static str, - /// Rust-like type hint: string | u32 | bool | i64 | json. - pub kind: &'static str, - /// "required" | "optional" | "default=" - pub constraint: &'static str, - pub description: &'static str, -} - -/// Static metadata for a daemon HTTP endpoint. -/// -/// Registered at link time via [`inventory::submit!`] — generated by -/// `#[onto_api(...)]` proc-macro attribute on each handler function. -/// Collected by [`GET /api/catalog`](super::api_catalog_handler). -#[derive(serde::Serialize, Clone)] -pub struct ApiRouteEntry { - pub method: &'static str, - pub path: &'static str, - pub description: &'static str, - /// Authentication required: "none" | "viewer" | "admin" - pub auth: &'static str, - /// Which actors typically call this endpoint. - pub actors: &'static [&'static str], - pub params: &'static [ApiParam], - /// Semantic grouping tags (e.g. "graph", "federation", "describe"). - pub tags: &'static [&'static str], - /// Non-empty when the endpoint is only compiled under a feature flag. - pub feature: &'static str, -} - -inventory::collect!(ApiRouteEntry); +pub use ontoref_ontology::api::{ApiParam, ApiRouteEntry}; /// Return the full API catalog sorted by path then method. pub fn catalog() -> Vec<&'static ApiRouteEntry> { diff --git a/crates/ontoref-daemon/src/main.rs b/crates/ontoref-daemon/src/main.rs index 1267c11..ae8577a 100644 --- a/crates/ontoref-daemon/src/main.rs +++ b/crates/ontoref-daemon/src/main.rs @@ -305,6 +305,12 @@ struct Cli { #[arg(long, value_name = "PASSWORD")] hash_password: Option, + /// Print all #[onto_api] registered routes as a JSON array and exit. + /// Pipe to api-catalog.json so the ontoref UI can display this project's + /// API surface when it is registered as a non-primary slug in the daemon. + #[arg(long)] + dump_api_catalog: bool, + /// Run as an MCP server over stdin/stdout (for Claude Desktop, Cursor, /// etc.). No HTTP server is started in this mode. #[cfg(feature = "mcp")] @@ -448,6 +454,11 @@ async fn main() { tracing_subscriber::fmt().with_env_filter(env_filter).init(); } + if cli.dump_api_catalog { + println!("{}", ontoref_ontology::api::dump_catalog_json()); + return; + } + if let Some(ref password) = cli.hash_password { use argon2::{ password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, diff --git a/crates/ontoref-daemon/src/ui/handlers.rs b/crates/ontoref-daemon/src/ui/handlers.rs index 0a0f1d7..ff2d6e3 100644 --- a/crates/ontoref-daemon/src/ui/handlers.rs +++ b/crates/ontoref-daemon/src/ui/handlers.rs @@ -880,6 +880,84 @@ pub async fn dashboard_mp( let adr_count = count_ncl_files(&ctx_ref.root.join("adrs")); let mode_count = count_ncl_files(&ctx_ref.root.join("reflection").join("modes")); + // ── Project identity (same data as project_picker card) ────────────────── + let config_json = load_config_json( + &ctx_ref.root, + &ctx_ref.cache, + ctx_ref.import_path.as_deref(), + ) + .await; + let card = config_json + .as_ref() + .map(extract_card_from_config) + .unwrap_or(serde_json::Value::Null); + let description = if card + .get("tagline") + .and_then(|v| v.as_str()) + .unwrap_or("") + .is_empty() + { + readme_description(&ctx_ref.root) + } else { + String::new() + }; + let manifest_path = ctx_ref.root.join(".ontology").join("manifest.ncl"); + let (layers, op_modes, default_mode, repo_kind) = if manifest_path.exists() { + match ctx_ref + .cache + .export(&manifest_path, ctx_ref.import_path.as_deref()) + .await + { + Ok((json, _)) => { + let layers: Vec = json + .get("layers") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .map(|l| { + serde_json::json!({ + "id": l.get("id").and_then(|v| v.as_str()).unwrap_or(""), + "description": l.get("description").and_then(|v| v.as_str()).unwrap_or(""), + }) + }) + .collect() + }) + .unwrap_or_default(); + let op_modes: Vec = json + .get("operational_modes") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .map(|m| { + serde_json::json!({ + "id": m.get("id").and_then(|v| v.as_str()).unwrap_or(""), + "description": m.get("description").and_then(|v| v.as_str()).unwrap_or(""), + }) + }) + .collect() + }) + .unwrap_or_default(); + let default_mode = json + .get("default_mode") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let repo_kind = json + .get("repo_kind") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + (layers, op_modes, default_mode, repo_kind) + } + Err(_) => (vec![], vec![], String::new(), String::new()), + } + } else { + (vec![], vec![], String::new(), String::new()) + }; + let repos = git_remotes(&ctx_ref.root); + let showcase = detect_showcase(&ctx_ref.root, &base_url); + let generated = detect_generated(&ctx_ref.root, &base_url); + let mut ctx = Context::new(); ctx.insert("uptime_secs", &state.started_at.elapsed().as_secs()); ctx.insert("cache_entries", &ctx_ref.cache.len()); @@ -899,6 +977,15 @@ pub async fn dashboard_mp( ctx.insert("adr_count", &adr_count); ctx.insert("mode_count", &mode_count); ctx.insert("current_role", &auth_role_str(&auth)); + ctx.insert("card", &card); + ctx.insert("description", &description); + ctx.insert("repo_kind", &repo_kind); + ctx.insert("repos", &repos); + ctx.insert("layers", &layers); + ctx.insert("op_modes", &op_modes); + ctx.insert("default_mode", &default_mode); + ctx.insert("showcase", &showcase); + ctx.insert("generated", &generated); let file_versions: std::collections::BTreeMap = ctx_ref .file_versions @@ -936,9 +1023,10 @@ pub async fn api_catalog_page_mp( let ctx_ref = state.registry.get(&slug).ok_or(UiError::NotConfigured)?; let base_url = format!("/ui/{slug}"); - // The #[onto_api] catalog is the ontoref-daemon's own HTTP surface. - // Only expose it for the primary project (ontoref itself). Consumer - // projects have their own API surfaces not registered in this process. + // The #[onto_api] catalog is populated via inventory::submit! at link time. + // For the primary project (ontoref itself) we read from the live inventory. + // For consumer projects (separate binaries) we read api-catalog.json from + // their project root — generated by `just export-api-catalog` in that project. let is_primary = slug == state.registry.primary_slug(); let routes: Vec = if is_primary { crate::api_catalog::catalog() @@ -969,7 +1057,7 @@ pub async fn api_catalog_page_mp( }) .collect() } else { - vec![] + read_project_api_catalog(&ctx_ref.root) }; let catalog_json = serde_json::to_string(&routes).unwrap_or_else(|_| "[]".to_string()); @@ -1460,6 +1548,30 @@ pub async fn serve_public_single( serve_dir_from(&state.project_root.join("public"), &pub_path).await } +/// Read `api-catalog.json` from a consumer project root. +/// +/// Returns an empty vec (silently) when the file is absent — the UI will show +/// a "no catalog" state instead of an error. Returns an empty vec on parse +/// failure and logs a warning so misconfigured files are visible in daemon +/// logs. +fn read_project_api_catalog(project_root: &std::path::Path) -> Vec { + let path = project_root.join("api-catalog.json"); + let bytes = match std::fs::read(&path) { + Ok(b) => b, + Err(_) => return vec![], + }; + match serde_json::from_slice::>(&bytes) { + Ok(v) => v, + Err(e) => { + tracing::warn!( + "api-catalog.json at {} is invalid JSON: {e}", + path.display() + ); + vec![] + } + } +} + /// Generic file server for an arbitrary base directory. async fn serve_dir_from(base: &std::path::Path, rel: &str) -> Result { let Ok(canonical_base) = base.canonicalize() else { diff --git a/crates/ontoref-daemon/templates/base.html b/crates/ontoref-daemon/templates/base.html index 91d729e..c21bedf 100644 --- a/crates/ontoref-daemon/templates/base.html +++ b/crates/ontoref-daemon/templates/base.html @@ -13,14 +13,22 @@ if(m==="icons")document.documentElement.classList.add("nav-icons"); else if(m==="names")document.documentElement.classList.add("nav-names"); })() - - + {% block head %}{% endblock head %} diff --git a/crates/ontoref-daemon/templates/pages/adrs.html b/crates/ontoref-daemon/templates/pages/adrs.html index 7b1eab4..8d2aaae 100644 --- a/crates/ontoref-daemon/templates/pages/adrs.html +++ b/crates/ontoref-daemon/templates/pages/adrs.html @@ -6,13 +6,6 @@ {% block nav_group_dev %}active{% endblock nav_group_dev %} {% block head %} - {% endblock head %} {% block content %} @@ -92,13 +85,15 @@ +