Graph data model
What lives in the graph, how it is identified, and how a piece of knowledge earns its place. The schema is one Pydantic module (writ/graph/schema.py, 757 lines) plus the edge registry (writ/graph/db/_common.py).
- Which node types exist, and which of them the retrieval pipeline can actually return.
- Where each of the 24 edge types comes from: hand-authored, machine-derived, or runtime records.
- How identity, multi-project isolation, and concurrency safety are enforced in the database itself.
- How a rule proposed by the AI can become canonical source, and why records never can.
- Diagram convention: the dark green rounded block is where the flow starts (START or INPUT); orange blocks are outcomes; numbered steps show reading order.
1.1Node types come in four tiers
Thirteen curated types are defined in one enum (schema.py:112-129). Only six enter the ranked candidate pool that build_pipeline loads (pipeline.py:565-621). Five more never rank: they surface only when graph traversal pulls them in next to a ranked result. Categories and Abstractions are structure. Decision-memory records are deliberately kept out of every retrieval and reconcile path (schema.py:577-586).
flowchart TB
subgraph RANK["Tier 1: ranked candidates"]
RULE["Rule<br/>the coding-rule corpus"]
SKL["Skill"]
PBK["Playbook"]
TEC["Technique"]
ANT["AntiPattern"]
FRB["ForbiddenResponse"]
end
subgraph EXPAND["Tier 2: adjacency-surfaced"]
PHA["Phase"]
RAT["Rationalization"]
PSC["PressureScenario"]
EXM["WorkedExample"]
ROL["SubagentRole"]
end
subgraph STRUCT["Tier 3: structure"]
CAT["Category<br/>routing declarations,<br/>BELONGS_TO hub"]
ABS["Abstraction<br/>summary-mode substitute<br/>for clustered rules"]
end
subgraph REC["Tier 4: records"]
DEC["Decision"]
FCH["FileChange"]
CMT["Commit"]
PRJ["Project registry"]
end
RANK ~~~ EXPAND
STRUCT ~~~ REC
Every curated type has exactly one primary-key field, registered once so ingest, the database layer, and reconcile all derive from the same map (NODE_ID_FIELDS, schema.py:701-715). Ids share one grammar (RULE_ID_PATTERN, schema.py:13): examples are SEC-INJ-SQL-001 (Rule), SKL-PROC-MODE-001 (Skill), CAT-CODE-SECURITY-001 (Category).
| Tier | Types | Primary key | Why this tier exists |
|---|---|---|---|
| Ranked candidates | Rule, Skill, Playbook, Technique, AntiPattern, ForbiddenResponse | rule_id, skill_id, playbook_id, technique_id, antipattern_id, forbidden_id | Content that should be findable by meaning or keywords. |
| Adjacency-surfaced | Phase, Rationalization, PressureScenario, WorkedExample, SubagentRole | phase_id, rationalization_id, scenario_id, example_id, role_id | Supporting material that only makes sense next to the node it belongs to. |
| Structure | Category, Abstraction | category_id, abstraction_id | Categories declare routing; Abstractions stand in for many rules when token budget is tight. |
| Records | Decision, FileChange, Commit, Project | decision_id, change_id, commit_hash, name | Runtime history. Absent from every retrieval and reconcile enumeration by design. |
1.2Edges: 24 types, three origins
ALLOWED_EDGE_TYPES is the single source of truth (_common.py:32-49); the corpus subset is derived, never hand-copied (CORPUS_EDGE_TYPES = ALLOWED - RECORD, _common.py:64). The split that matters operationally is declared versus derived: declared edges are authored knowledge and survive the export round-trip; derived edges are recomputed on every ingest and are deliberately excluded from export (writ/export.py:53-65) so they never freeze into source.
| Group | Types | Origin |
|---|---|---|
| Declared corpus (14) | DEPENDS_ON, PRECEDES, CONFLICTS_WITH, SUPPLEMENTS, SUPERSEDES, TEACHES, COUNTERS, DEMONSTRATES, DISPATCHES, GATES, PRESSURE_TESTS, CONTAINS, ATTACHED_TO, INVOKES | Hand-authored in front-matter edges: lists or RULE-START ### Edges sections. Each type documents its complete valid source and target sets (schema.py:637-679). |
| Derived (3) | RELATED_TO, BELONGS_TO, ABSTRACTS | RELATED_TO from rule-id mentions in prose (ingest.py:41); BELONGS_TO from every node's category field plus the category tree; ABSTRACTS from the compression run (chapter 4). |
| Record (7) | HAS_DECISION, HAS_CHANGE, HAS_COMMIT, MOTIVATED_BY, GOVERNED_BY, INCLUDES, REALIZES | Written at runtime by decision-memory capture. Endpoint labels are validated against an allowlist before any label reaches a Cypher string (_common.py:73), closing the label-interpolation injection surface. |
1.3Identity, multi-project isolation, and race safety
- Uniqueness is composite.
(id, project)constraints cover Rule, Abstraction, and all ten methodology labels (writ/graph/db/schema_store.py:24-49), so two projects can hold the same rule id without colliding. - Constraints double as concurrency protection. A Cypher MERGE on a non-constrained key is not race-safe: concurrent creates fork duplicate nodes. The uniqueness constraint makes the losing writer match the winner's node instead (comment at
schema_store.py:28-33). - Retrieval honors the same boundary. A project-scoped query admits only that project plus the shared corpus:
allowed_projects = {project, "_shared"}(pipeline.py:270). This is the anti-leak guarantee for multi-project use.
1.4Provenance: how knowledge earns canon
Every node carries a provenance state (VALID_PROVENANCE, schema.py:32). The design principle: a rule can be born inside the graph, but it only becomes canonical markdown source through a human gate. Records are permanent history and are excluded from promotion by construction.
stateDiagram-v2 direction LR state "hand-authored<br/>(the corpus at rest)" as HA state "proposed<br/>(graph-first, no markdown home)" as PR state "graduation_pending<br/>(evidence threshold crossed)" as GP state "graduated<br/>(exported to source)" as GR state "record<br/>(permanent runtime history)" as RC [*] --> HA: ingest from bible/ [*] --> PR: POST /propose or writ add PR --> GP: frequency crossing GP --> GR: human promotion gate [*] --> RC: decision-memory capture
- Evidence is counted, not asserted.
times_seen_positiveandtimes_seen_negativeare runtime-only properties that reconcile can never clear (RUNTIME_EXEMPT_PROPS,schema.py:742). The graduation flip lives inevaluate_and_flip_graduation(rule_store.py:125). - Evidence also feeds ranking. With at least 50 observations and a positive ratio of at least 0.75, the empirical ratio replaces the static confidence tier in the retrieval score (
ranking.py:118-136). A battle-tested rule earns its weight. - Two orthogonal axes. Provenance records how a node earned canon;
graduated_viarecords who wrote the final words (approved as-is versus human-edited,schema.py:51). A human-edited graduated rule stays graduated. - Pruning is provenance-aware.
PARITY_EXEMPT_PROVENANCE= the graph-first states plusrecord(schema.py:44), so reconcile (chapter 4) can never delete runtime history or a proposal that has not yet been judged.
The provenance machinery is the schema-level encoding of Writ's governance stance: the system may propose knowledge and gather evidence for it, but promotion into canon is always a human act. There is no code path that graduates a rule without the gate.