ontoref/adrs/adr-003-qa-and-knowledge-persistence-as-ncl.ncl
Jesús Pérez 0396e4037b
Some checks failed
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
chore: add ontology and reflection
2026-03-13 00:21:04 +00:00

101 lines
7.7 KiB
Plaintext

let d = import "defaults.ncl" in
d.make_adr {
id = "adr-003",
title = "Q&A and Accumulated Knowledge Persist to NCL, Not Browser Storage",
status = 'Accepted,
date = "2026-03-12",
context = "The initial Q&A bookmarks feature stored entries in browser localStorage keyed by project. This is convenient but violates the dag-formalized axiom: knowledge that lives only in a browser session is invisible to agents, not git-versioned, not queryable via MCP, and is lost on browser data reset. The same problem applies to quick actions and any other accumulated operational knowledge. The system accumulates knowledge during development sessions (AI interactions, architectural reviews, debugging) that should be first-class artifacts — not ephemeral browser state.",
decision = "All Q&A entries persist to `reflection/qa.ncl` — a typed NCL record file governed by `reflection/schemas/qa.ncl`. Mutations happen via `crates/ontoref-daemon/src/ui/qa_ncl.rs` (line-level surgery, same pattern as backlog_ncl.rs). Four MCP tools expose the store to AI agents: `ontoref_qa_list` (read, with filter), `ontoref_qa_add` (append), `ontoref_qa_delete` (remove block), `ontoref_qa_update` (field mutation). HTTP endpoints `/qa-json` (GET), `/qa/add`, `/qa/delete`, `/qa/update` (POST) serve the UI. The UI renders server-side entries via Tera template injection (SERVER_ENTRIES JSON blob), eliminating the need for a separate fetch on load. The same principle is applied to quick actions: they are declared in `.ontoref/config.ncl` (quick_actions array) and new modes are created as `reflection/modes/<id>.ncl` files via `ontoref_action_add`.",
rationale = [
{
claim = "NCL persistence makes knowledge git-versioned and queryable",
detail = "A Q&A entry in reflection/qa.ncl is a typed Nickel record. It survives browser resets, is visible in git history, can be diffed, and is exported via the NCL cache just like any other protocol artifact. localStorage provides none of these properties.",
},
{
claim = "MCP access enables AI agents to read and write accumulated knowledge",
detail = "Without server-side persistence, an AI agent cannot query what questions have been asked and answered about this project. With ontoref_qa_list, the agent can orient itself immediately from accumulated Q&A without re-asking questions already answered in previous sessions. With ontoref_qa_add, the agent can record newly discovered knowledge while working.",
},
{
claim = "Line-level NCL surgery avoids full AST parsing",
detail = "The qa_ncl.rs module uses the same pattern as backlog_ncl.rs: find blocks by id field, insert/replace/remove by line index. No nickel-lang-core dependency, no AST round-trip, no format drift. The NCL format is predictable enough that targeted string operations are safe and sufficient.",
},
{
claim = "Server-side hydration eliminates the initial fetch round-trip",
detail = "The qa.html template receives `entries` as a Tera context variable and embeds SERVER_ENTRIES as a JSON literal in the page script. The JS initialises its in-memory list directly from this value. No separate GET /qa-json fetch on load, no loading state, no flash of empty content.",
},
{
claim = "Passive drift observation closes the knowledge-accumulation loop",
detail = "As code changes, drift_watcher.rs detects divergence between Yang artifacts and Yin ontology and emits a notification. Combined with Q&A persistence, the system can accumulate observations about drift patterns — enabling future Q&A entries like 'why does crate X often drift?' to be answered from stored context.",
},
],
consequences = {
positive = [
"Q&A knowledge survives session boundaries — AI agents and developers share a common context store",
"MCP tools give AI clients direct read/write access to accumulated project knowledge",
"All entries are typed (QaEntry schema), git-diffable, and auditable",
"Quick actions in config.ncl are the canonical source — no UI state drift",
"Passive drift observer closes the feedback loop without requiring manual sync",
],
negative = [
"qa.ncl and backlog.ncl use line-level surgery — format changes to the NCL templates require updating the mutation modules",
"No concurrent write protection — simultaneous writes from multiple actors could corrupt qa.ncl (same limitation as backlog.ncl; mitigated by advisory file locks in the ./ontoref CLI)",
"Deletion removes the block permanently — no soft-delete or archive mechanism yet",
],
},
alternatives_considered = [
{
option = "Keep localStorage as the storage backend, add optional sync to NCL on explicit user action",
why_rejected = "Two sources of truth creates sync complexity and divergence. AI agents would still not see localStorage entries. The sync step would be frequently skipped. NCL as primary is simpler.",
},
{
option = "Store Q&A in SurrealDB via stratum-db (existing optional dependency)",
why_rejected = "Requires the db feature and a running SurrealDB instance. NCL files are always present, git-versioned, and work without any database. The protocol-not-runtime axiom argues for file-first. SurrealDB can be added as a secondary index later if full-text search is needed.",
},
{
option = "Full AST parse of qa.ncl via nickel-lang-core for mutations",
why_rejected = "nickel-lang-core has an unstable Rust API. The file structure is predictable enough for line-level surgery. backlog_ncl.rs has been operating safely with this pattern. Adding a hard dependency on nickel-lang-core for a file that is always written by the daemon is unnecessary complexity.",
},
],
constraints = [
{
id = "qa-write-via-mutation-module",
claim = "All mutations to reflection/qa.ncl must go through crates/ontoref-daemon/src/ui/qa_ncl.rs — no direct file writes from other call sites",
scope = "crates/ontoref-daemon/src/",
severity = 'Hard,
check_hint = "rg -l 'qa.ncl' crates/ontoref-daemon/src/ | rg -v 'qa_ncl.rs|handlers.rs|api.rs|mcp'",
rationale = "Centralising mutations in one module ensures consistent id generation, NCL format, and cache invalidation.",
},
{
id = "qa-schema-typed",
claim = "reflection/qa.ncl must conform to the QaStore contract from reflection/schemas/qa.ncl — nickel typecheck must pass",
scope = "reflection/qa.ncl",
severity = 'Hard,
check_hint = "nickel typecheck reflection/qa.ncl",
rationale = "Untyped Q&A would degrade to an unstructured log. The schema enforces id, question, answer, actor, created_at fields on every entry.",
},
{
id = "mcp-qa-tools-no-apply-drift",
claim = "MCP tools ontoref_qa_list and ontoref_qa_add must never trigger sync apply steps or modify .ontology/ files",
scope = "crates/ontoref-daemon/src/mcp/mod.rs",
severity = 'Hard,
check_hint = "rg -A20 'QaAddTool|QaListTool' crates/ontoref-daemon/src/mcp/mod.rs | rg -c 'apply|sync|ontology'",
rationale = "Q&A mutation tools operate only on reflection/qa.ncl. Ontology changes require deliberate human or agent review via the sync-ontology mode.",
},
],
related_adrs = ["adr-001-protocol-as-standalone-project", "adr-002-daemon-for-caching-and-notification-barrier"],
ontology_check = {
decision_string = "Q&A and operational knowledge persist as typed NCL artifacts, git-versioned, accessible via MCP and HTTP, rendered server-side in UI",
invariants_at_risk = ["dag-formalized", "protocol-not-runtime"],
verdict = 'Safe,
},
}