7.6 KiB
Graph Workflow
The graph layer is the optional compiled index over the markdown wiki. It does not replace the wiki — it sits alongside it under cml/wiki/graph/ and is reproducible from the markdown at any time. The point is to make typed, provenance-backed relationships machine-queryable while keeping markdown canonical.
If cml/wiki/graph/ontology.yaml is absent, the wiki is pre-graph: don't run extract/lint/query and don't fabricate ontology files. Either propose adding the layer, or proceed without it.
What the graph captures
Three classes of edge come out of an extract:
- Typed semantic edges declared in a page's
graph.relationships[]frontmatter. Examples:founded,proposed,depends_on. Each one carries an explicitsource(a source-page slug), anevidencequote, aconfidence(high/medium/low), and astatus(current/historical/proposed/disputed/superseded). The extractor never invents these. mentionsedges — one per body[[wikilink]](deduplicated per page). Confidence islow; they accelerate navigation but should not be cited as evidence of a typed relationship.sourced_fromedges — one per slug in a non-source page's frontmattersources:list, pointing at the source page.summarizes_rawedges — one per source page'sraw:field, with the raw file path as the (string-literal) object.
Frontmatter schema
graph:
node_id: person:praney-behl # optional; default <node_type>:<slug>
node_type: person # optional; default mapped from type/kind via ontology
canonical: true # mark canonical when multiple slugs alias the same entity
aliases: [Praney, praney@example.com]
relationships:
- predicate: founded
object: company:seedblocks
source: praney-founder-context-dump
evidence: "Solo technical founder and sole director..."
confidence: high
status: current
# optional:
# valid_from: 2025-01-15
# valid_to: 2026-03-01
# notes: "..."
# raw_ref: "cml/raw/founder-dump.md#L42"
# contradicts: <node-id-or-edge-id>
# supersedes: <node-id-or-edge-id>
Required relationship fields: predicate, object, source, evidence, confidence, status.
node_id format is <node_type>:<slug>. The default is derived from the page's type/kind via the ontology's maps_from block. decision, claim, and raw are explicit-only — they don't have wiki pages, so they only show up as edge objects. If a typed edge points at one of these, the wiki_graph_lint.py flag for "broken object reference" will fire until either (a) you create a page for it, or (b) you add it to the ontology with explicit_only: true and accept that lint will continue to flag the reference.
The ontology
cml/wiki/graph/ontology.yaml is the contract. It declares:
node_types[*].maps_from— how pagetype/kindprojects onto a node type.predicates[*]— the allowed predicates, each withsubject_types,object_types, andrequires_evidence."*"is a wildcard for either side.
Edit the ontology when you need a new domain predicate. Re-run wiki_graph_lint.py to validate; existing typed edges will be caught if they no longer match.
When to add a typed edge vs a plain [[wikilink]]
Add a typed edge when:
- A specific source explicitly states the relationship.
- You can quote a snippet of evidence.
- The predicate is meaningful for downstream queries ("who founded what", "what did Stephanie propose").
Use a plain [[wikilink]] when:
- The relationship is implicit, atmospheric, or you're hedging.
- You cannot pin the claim to a single source quote.
- The predicate would be
mentionsanyway.
When in doubt, write the wikilink and skip the typed edge. The lint surfaces missing evidence; it does not punish under-claiming.
Extract / lint / query loop
# Validate the typed metadata first; lint is conservative, never edits.
uv run skills/llm-wiki/scripts/wiki_graph_lint.py cml/wiki/
# Compile to nodes.jsonl, edges.jsonl, graph.sqlite, graph.graphml.
uv run skills/llm-wiki/scripts/wiki_graph_extract.py cml/wiki/
# Navigate.
uv run skills/llm-wiki/scripts/wiki_graph_query.py cml/wiki/ neighbors --node product:konvy
uv run skills/llm-wiki/scripts/wiki_graph_query.py cml/wiki/ edges --subject person:stephanie-emmanouel
uv run skills/llm-wiki/scripts/wiki_graph_query.py cml/wiki/ path --from person:praney-behl --to product:konvy
uv run skills/llm-wiki/scripts/wiki_graph_query.py cml/wiki/ facts --about product:konvy
--json works on both lint and query commands.
Ingest workflow integration
After Step 6 of the standard ingest workflow (after surgical updates and source page creation), run:
- If new typed edges were added on the page being ingested, run
wiki_graph_lint.py. Interactive: triage findings with the user before extract. Drain (headless): if lint is clean, proceed to extract; if lint reports errors, record them inlog.mdand skip extract for this batch — never silently rewrite typed edges, never block waiting for a user. - Run
wiki_graph_extract.pyto refresh the compiled artifacts. - Append a sub-line under the ingest's
log.mdentry:graph: +N nodes, +M typed edges (predicates: founded, contains_product, ...)
Skip extract if this ingest added no graph: metadata and created no new pages — the compiled artifacts are unchanged.
Query workflow integration
When the user asks a question that smells relational ("what's connected to X", "who proposed Y", "trace the path from A to B"):
- Read the index as usual.
- If
cml/wiki/graph/graph.sqliteexists and is fresher than the latest log entry, query it for typed edges around the candidate pages —neighbors,edges,factsare the most useful. - Read the wiki pages behind the relevant nodes/edges. Don't answer from graph rows alone for high-stakes claims; the
evidencefield is a hint, not the source of truth. - Cite with
[[wikilinks]]to wiki pages, not graph rows.
If graph.sqlite is stale (older than the most recent ingest in log.md), use it as-is and note the staleness — do not regenerate inline. Extract is a compile/drain step; the query turn stays read-only, and the background drain refreshes the graph after each ingest.
Generated artifact policy
| File | Canonical? | Default tracking |
|---|---|---|
cml/wiki/graph/ontology.yaml |
Yes — edit by hand | Tracked |
cml/wiki/graph/nodes.jsonl |
Generated | Optional — cml/wiki/graph/.gitignore does not ignore it; track if you want graph diffs in PRs |
cml/wiki/graph/edges.jsonl |
Generated | Same as above |
cml/wiki/graph/graph.sqlite |
Generated | Gitignored by default (large, binary) |
cml/wiki/graph/graph.graphml |
Generated | Gitignored by default |
The bootstrapped cml/wiki/graph/.gitignore ignores graph.sqlite and graph.graphml. Edit it if your team prefers different policy.
Anti-patterns
- Typed edges without evidence. Defeats the entire point. Lint will flag them; do not silence.
- Editing
nodes.jsonl/edges.jsonl/graph.sqliteby hand. Edit the markdown; regenerate. - Inventing ontology entries to make a typed edge "fit". The ontology should reflect domain reality, not paper over a too-eager edge. Either add the predicate with proper
subject_types/object_types, or usementions. - Treating graph rows as evidence in answers. Always cite the wiki page; the graph just told you which wiki page to read.
- Forgetting to regenerate after an ingest. The graph diverges silently. Tie extract to ingest in muscle memory.