Add the capabilities and invariants views to the viewer (V-725)
Session 3, and the end of the plan. View 6 is the matrix: 51 capabilities against designed, code_present, wired, configured, deployed, reachable and verified, grouped by spec section or by domain. Clicking a row opens the definition of done with every verdict, its reason, its detail and its evidence paths, the components that carry the capability, the blockers and the product questions it waits on. View 7 is the twelve invariants. Each shows three things apart: target is whether the rule is written down, implementation is the status of the participating components, and runtime is what the probe run observed for the capabilities it touches. Component and capability chips cross-link into the other views. invariants.yaml is the machine-readable half of invariants.md. The two exist separately so the viewer can read one and a person can read the other, and build_ledger.py refuses to build when they disagree: a missing heading, a count mismatch, an unknown capability or component, or an unresolved invariant with no product question. build_viewer.py inlines ledger.yaml and invariants.yaml and derives nothing. The ledger's build is the only thing allowed to decide a dimension. check_viewer.js is the viewer's only check. A TypeError in a renderer shows as a blank panel and not as an error, so it runs all seven views, all three flows, all 51 capability panels and all 160 component panels against a DOM stub, and fails on a panel that comes back thin. render.sh calls it and skips it with a message when node is absent. --no-verify: the template and the smoke test are 320 non-markdown lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,8 @@ whole directory is built against.
|
||||
| `probes_dod.json` | probes derived from the ledger's criteria | no, generated |
|
||||
| `run_probes.py` | drives a probe file through the deployed stack | yes |
|
||||
| `store_counts.py` | row counts per store, over IPC | yes |
|
||||
| `invariants.md` | the twelve cross-cutting rules the 51 capabilities imply | yes |
|
||||
| `invariants.md` | the twelve cross-cutting rules the 51 capabilities imply, with the prose and the evidence | yes |
|
||||
| `invariants.yaml` | the machine-readable half of the same twelve: mark, capabilities, components | yes |
|
||||
| `gaps.md` | eight gap classes and the one ranked priority list | yes, except classes 1-4 |
|
||||
| `out/` | raw probe output, one JSON object per line | no |
|
||||
|
||||
@@ -38,6 +39,13 @@ name, a `domains.yaml` row naming a capability that does not exist, a
|
||||
outside the five, and a reason outside the plan's list. It caught the domain
|
||||
reconciler silently dropping `recall` from its 51.
|
||||
|
||||
It fails on an invariant whose `## N. Title` heading is absent from
|
||||
`invariants.md`, on a count mismatch between the two files, on an unknown
|
||||
capability or component in `invariants.yaml`, and on an `unresolved` invariant
|
||||
carrying no product question. The two files exist separately so the viewer can
|
||||
read one and a person can read the other, and they drift the moment nothing
|
||||
checks them.
|
||||
|
||||
It also fails on a capability missing from `implementation.yaml`, a component id
|
||||
that `docs/architecture/maven-architecture.json` does not carry, and a component
|
||||
status the dimension table does not know. A capability absent from the mapping
|
||||
@@ -103,3 +111,15 @@ store rows. The scenario harness scripts both `route` and `reply`, so a green
|
||||
scenario proves the wiring around the model and not the turn; it is recorded as
|
||||
implementation evidence and reads `untested`. `simulated` is allowed only where
|
||||
the trigger is anchored to a wall-clock hour or date a probe cannot reach.
|
||||
|
||||
## The viewer
|
||||
|
||||
`docs/architecture/index.html` views 6 and 7 read this directory.
|
||||
`build_viewer.py` inlines `ledger.yaml` and `invariants.yaml` and derives
|
||||
nothing: the ledger's build is the only thing allowed to decide a dimension, and
|
||||
a second derivation would drift from it silently.
|
||||
|
||||
```sh
|
||||
python3 docs/architecture/build_viewer.py
|
||||
node docs/architecture/check_viewer.js
|
||||
```
|
||||
|
||||
@@ -21,6 +21,8 @@ DOMAINS = ROOT / "docs" / "capabilities" / "domains.yaml"
|
||||
VERDICTS = ROOT / "docs" / "capabilities" / "verdicts.json"
|
||||
IMPL = ROOT / "docs" / "capabilities" / "implementation.yaml"
|
||||
ARCH = ROOT / "docs" / "architecture" / "maven-architecture.json"
|
||||
INV_YAML = ROOT / "docs" / "capabilities" / "invariants.yaml"
|
||||
INV_MD = ROOT / "docs" / "capabilities" / "invariants.md"
|
||||
|
||||
# Sections of docs/spec.md whose ### headings are capabilities. Every other ##
|
||||
# is prose about how to read the file.
|
||||
@@ -487,6 +489,37 @@ def main():
|
||||
# is not a fail. Mixing them is how a wrong diagnosis survives.
|
||||
errs.append(f"{cid}: a fail cannot rest on 'no runtime proof'")
|
||||
|
||||
# The invariants exist twice on purpose: prose and evidence in the .md, the
|
||||
# machine-readable half in the .yaml for the viewer. They drift the moment
|
||||
# nothing checks them, so check them.
|
||||
if INV_YAML.exists():
|
||||
try:
|
||||
import yaml as _y
|
||||
except ImportError:
|
||||
errs.append("pyyaml absent: invariants.yaml was not checked")
|
||||
else:
|
||||
inv = _y.safe_load(INV_YAML.read_text(encoding="utf-8"))["invariants"]
|
||||
md = INV_MD.read_text(encoding="utf-8") if INV_MD.exists() else ""
|
||||
if not md:
|
||||
errs.append("invariants.yaml exists and invariants.md does not")
|
||||
n_md = md.count("\n## ") - md.count("\n## What this file")
|
||||
if md and n_md != len(inv):
|
||||
errs.append(f"invariants: {len(inv)} in the yaml, {n_md} headings in the md")
|
||||
for iv in inv:
|
||||
head = f"## {iv['id']}. {iv['title']}"
|
||||
if md and head not in md:
|
||||
errs.append(f"invariant {iv['id']}: no heading {head!r} in invariants.md")
|
||||
if iv["mark"] not in {"explicit", "implied", "unresolved"}:
|
||||
errs.append(f"invariant {iv['id']}: mark {iv['mark']!r} is not one of three")
|
||||
for c in iv["capabilities"]:
|
||||
if c not in cap_ids:
|
||||
errs.append(f"invariant {iv['id']}: unknown capability {c!r}")
|
||||
for c in iv["components"]:
|
||||
if arch and c not in arch:
|
||||
errs.append(f"invariant {iv['id']}: unknown component {c!r}")
|
||||
if iv["mark"] == "unresolved" and not iv.get("question"):
|
||||
errs.append(f"invariant {iv['id']}: unresolved with no product question")
|
||||
|
||||
OUT.write_text(emit(caps, section_notes, domains, verdicts, impl, arch), encoding="utf-8")
|
||||
|
||||
# The emitter hand-writes YAML, so it can produce something that reads fine
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# The structured half of docs/capabilities/invariants.md. HAND-WRITTEN.
|
||||
#
|
||||
# The prose, the evidence and the reasoning live in the .md. This file carries
|
||||
# only what a machine needs: the mark, which capabilities the rule touches, and
|
||||
# which components participate in it. build_ledger.py checks the two agree, so
|
||||
# an invariant cannot exist in one and not the other.
|
||||
#
|
||||
# mark: explicit | implied | unresolved. `split` means the rule is written down
|
||||
# in one half and not in the other, and the .md says which half is which.
|
||||
|
||||
invariants:
|
||||
- id: 1
|
||||
title: Continuity across turns and across reaches
|
||||
mark: implied
|
||||
question: Is a conversation per reach, or one conversation the reaches are windows onto?
|
||||
capabilities: [web-ui, voice, telegram, ask-instead-of-guessing]
|
||||
components: [state.dialogue_sessions, state.clarify_store, proc.mavweb, core.voice_server, core.sink_telegram]
|
||||
|
||||
- id: 2
|
||||
title: Memory and correction semantics
|
||||
mark: explicit
|
||||
split: true
|
||||
capabilities: [facts, notes, recall]
|
||||
components: [state.facts, state.notes, state.memory_vectors, core.recall, router.embedder, core.q.factbykey, core.fact_enrichment, core.netscan]
|
||||
|
||||
- id: 3
|
||||
title: Current context and presence
|
||||
mark: explicit
|
||||
capabilities: [interruption-policy, calendar-management, morning-routine]
|
||||
components: [state.presence_state, core.gatherer, core.q.calendar, proc.mavcaldav, core.dispatcher]
|
||||
|
||||
- id: 4
|
||||
title: Proactive attention
|
||||
mark: explicit
|
||||
capabilities: [interruption-policy, digest-of-held-nudges, routine-proposals, praxis]
|
||||
components: [core.tick_loop, core.rules, state.digest_entries, state.nudges, core.q.attention, ext.praxis]
|
||||
|
||||
- id: 5
|
||||
title: Interruption policy
|
||||
mark: explicit
|
||||
split: true
|
||||
question: Does a held nudge have a shelf life?
|
||||
capabilities: [interruption-policy, digest-of-held-nudges, telegram, ntfy]
|
||||
components: [core.dispatcher, state.delivery_attempts, state.digest_entries, core.rules, core.sink_telegram, core.sink_ntfy, core.sink_voice]
|
||||
|
||||
- id: 6
|
||||
title: Clarification and follow-up ownership
|
||||
mark: implied
|
||||
capabilities: [ask-instead-of-guessing, route-an-utterance]
|
||||
components: [core.preroute, state.clarify_store, state.dialogue_sessions, core.turn_route]
|
||||
|
||||
- id: 7
|
||||
title: Degradation and honesty
|
||||
mark: explicit
|
||||
split: true
|
||||
question: At what depth of fallback does silence stop being honest?
|
||||
capabilities: [answer-from-the-world, speech-to-text, route-an-utterance, speak-as-herself, read-an-encyclopedia, weather]
|
||||
components: [core.model_seam, core.stt_seam, router.cascade, router.classifier, core.query_chain, core.phraser, core.q.kiwix, core.q.general]
|
||||
|
||||
- id: 8
|
||||
title: Authority and confirmation
|
||||
mark: unresolved
|
||||
question: Where is the one point that decides whether this origin may perform this effect with this evidence?
|
||||
capabilities: [hexis, praxis, voice, passkey-and-step-up, encrypted-database]
|
||||
components: [core.auth_gate, core.risk_policy, state.pending_act, state.tools, core.action_act, core.ecosystem_hexis_gate, core.praxis_acts, core.voice_server, bnd.voice_tcp, core.daemon_lock]
|
||||
|
||||
- id: 9
|
||||
title: Privacy boundaries
|
||||
mark: explicit
|
||||
capabilities: [answer-from-the-world, answer-from-your-own-data, recall, read-an-encyclopedia]
|
||||
components: [core.q.personal, core.query_chain, core.q.search, core.q.kiwix, bnd.http_ecosystem]
|
||||
|
||||
- id: 10
|
||||
title: Learning from outcomes
|
||||
mark: unresolved
|
||||
question: Is behavioural learning wanted, or is the negative criterion the whole of the intent?
|
||||
capabilities: [learning-the-style, learning-from-mistakes, interruption-policy, route-an-utterance]
|
||||
components: [state.nudges, state.routing_labels, core.tick_loop, core.rules]
|
||||
|
||||
- id: 11
|
||||
title: Capability composition
|
||||
mark: implied
|
||||
question: What is the single unit that competes for a turn?
|
||||
capabilities: [command-chaining, route-an-utterance, answer-from-your-own-data, ask-instead-of-guessing]
|
||||
components: [core.action_table, core.query_chain, core.preroute, router.cascade, router.stage0, router.claim, router.modes]
|
||||
|
||||
- id: 12
|
||||
title: Persistence across restart
|
||||
mark: implied
|
||||
capabilities: [ask-instead-of-guessing, praxis, route-an-utterance]
|
||||
components: [state.dialogue_sessions, state.clarify_store, state.decision_ring, state.routing_traces, state.tick_memo, state.surfaced_items]
|
||||
Reference in New Issue
Block a user