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) 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). Only six enter the ranked candidate pool that build_pipeline loads (pipeline.py). 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).
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). Ids share one grammar (RULE_ID_PATTERN, schema.py): 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); the corpus subset is derived, never hand-copied (CORPUS_EDGE_TYPES = ALLOWED - RECORD, _common.py). 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) 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). |
| Derived (3) | RELATED_TO, BELONGS_TO, ABSTRACTS | RELATED_TO from rule-id mentions in prose (ingest.py); BELONGS_TO from every node's category field plus the category tree; ABSTRACTS from the compression run (chapter 4). Two of the three, RELATED_TO and BELONGS_TO, are excluded from the markdown export (DERIVED_EDGE_TYPES in writ/export.py) and re-derived on import. ABSTRACTS is not handled there at all: it round-trips through the compression artifact instead. |
| 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), closing the label-interpolation injection surface. |
1.3Identity, multi-project isolation, and race safety
- Uniqueness is composite.
(id, project)constraints cover Rule, Abstraction, and every methodology label (writ/graph/db/schema_store.py), 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). - Retrieval honors the same boundary, and it keys on node type. The split lives in
writ/retrieval/node_scope.py: doctrine types (rules and the retrievable methodology labels, listed inDOCTRINE_NODE_TYPES) reach every caller regardless of the project tag they carry, while record types are scoped to the calling project plus the shared corpus. That asymmetry is deliberate. Scoping by project tag alone would have hidden the entire doctrine corpus from every project but the one it was authored in, which is what the module's own docstring records as the reason for the redesign.
1.4Provenance: how knowledge earns canon
Every node carries a provenance state (VALID_PROVENANCE, schema.py). 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). The graduation flip lives inevaluate_and_flip_graduation(rule_store.py). - 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). 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). A human-edited graduated rule stays graduated. - Pruning is provenance-aware.
PARITY_EXEMPT_PROVENANCE= the graph-first states plusrecord(schema.py), 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.