Writ Architecture: Graph Data Model
Chapter 1 of 4

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).

This chapter answers
  • 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).

Figure 1.1 The four node tiers Click to enlarge
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).

TierTypesPrimary keyWhy this tier exists
Ranked candidatesRule, Skill, Playbook, Technique, AntiPattern, ForbiddenResponserule_id, skill_id, playbook_id, technique_id, antipattern_id, forbidden_idContent that should be findable by meaning or keywords.
Adjacency-surfacedPhase, Rationalization, PressureScenario, WorkedExample, SubagentRolephase_id, rationalization_id, scenario_id, example_id, role_idSupporting material that only makes sense next to the node it belongs to.
StructureCategory, Abstractioncategory_id, abstraction_idCategories declare routing; Abstractions stand in for many rules when token budget is tight.
RecordsDecision, FileChange, Commit, Projectdecision_id, change_id, commit_hash, nameRuntime 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.

GroupTypesOrigin
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 in DOCTRINE_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.

Figure 1.2 Provenance lifecycle Click to enlarge
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_positive and times_seen_negative are runtime-only properties that reconcile can never clear (RUNTIME_EXEMPT_PROPS, schema.py). The graduation flip lives in evaluate_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_via records 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 plus record (schema.py), so reconcile (chapter 4) can never delete runtime history or a proposal that has not yet been judged.
Why this matters

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.