STATUTE is a deterministic pipeline that reverse-engineers Oracle PL/SQL. It takes a folder of .sql files through these steps to a traceable Business Requirements Document, diagrams and a knowledge graph:
inventory the files → parse the code → model the data → analyse the logic → mine the rules → draw the diagrams → write the BRD → build the graph.
Summary · Agents · Workflow · Run it · Known problems · Glossary
Note
This README uses ASD-STE100 Simplified Technical English. The writing rules and the project
vocabulary are in docs/ste-style-guide.md. Each term in the
Glossary has only one meaning.
Caution
Do not share brd.md or the graph export outside the team that owns the source code. These files contain the complete business logic, schema, interfaces and error contracts. STATUTE has no redaction.
Do not rebuild a system from the BRD without a review by a business expert. On a blind procedure, the rule extraction recall was 0.400.
STATUTE reads an Oracle PL/SQL codebase. It writes a specification that a business analyst can review and a development team can rebuild from. Eight agents run in sequence. Each agent is an independent Python CLI program. It is not an autonomous LLM agent.
No language model generates, summarises, judges or rewrites any output. A formal Oracle PL/SQL grammar gives the structure. sqlglot decomposes the SQL. Rules assemble each sentence from the parse trees. The same source always gives the same document. Hallucination is not only reduced. It is structurally impossible, because no model is in the generation path. Thus, each claim can safely cite a file and a line.
This README is the one location that explains all of STATUTE. It gives these topics:
- the general design
- each of the 8 agents: its job, its procedure and its tests
- the formulas, the thresholds and the decision rules
- the data map
- the runbook
- the validation results and the known problems
Each claim in this README comes from a repository file or function, or from a command that was run on the repository.
| If you are… | Read |
|---|---|
| A manager or reviewer | 1, 3, 4, 20, 22 |
| A developer who joins the project | All sections, in sequence. Keep 18, 19 and 21 open while you work |
| An operator who runs the pipeline | 18, then the section for the agent that you run (sections 5 to 12) |
| A business analyst who reads a BRD | 1, 11, 14, 21 |
- 🧭 Summary
- 1.1 The problem · 1.2 What STATUTE produces · 1.3 Why STATUTE stops at documentation · 1.4 Why STATUTE uses no language model
- 🏗️ How STATUTE is built
- 2.1 Components
- 2.2 Responsibility matrix
- 2.3 Architecture
- 2.4 System context
- 2.5 Technology stack
- 2.6 Repository layout
- 🛡️ Design rules
- 🔄 The end-to-end workflow
- 4.1 Full flow · 4.2 Data flow and lineage · 4.3 Control flow and state · 4.4 Errors and recovery · 4.5 Who does which step
- 🗃️ Agent 1 — Inventory
- 🔵 Agent 2 — Parser
- 🧱 Agent 3 — Data
- 🧮 Agent 4 — Logic
- ⚖️ Agent 5 — Rules
- 🖼️ Agent 6 — Diagram
- 📝 Agent 7 — Synthesis
- 🕸️ Agent 8 — Knowledge graph
- 📐 Formulas, thresholds and algorithms
- ✍️ The annotation layer
- 🔒 Security and deployment
- 🏛️ Architecture decisions and references
- 🗂️ Data and file map
▶️ How to run STATUTE- 18.1 Prerequisites · 18.2 Installation · 18.3 Run the pipeline · 18.4 Query the knowledge graph · 18.5 Run the tests · 18.6 Configuration and environment variables · 18.7 Regenerate the grammar · 18.8 Fault diagnosis
- 🧩 How to extend STATUTE
- ✅ Validation results
⚠️ Known problems- 📌 Key points
- 📖 Glossary
- 📄 License
An organisation has an Oracle PL/SQL system in service. The system has no reliable documentation, and its authors are not available. Before the organisation can modernise, replace or audit the system, a person must answer two questions:
- What does the system do?
- Which rules does the system enforce?
The precedent for this project is an industrial 6.4-million-line COBOL system. Two modernisation attempts failed: one automatic conversion and one package replacement. After these failures, the team rewrote the system from a specification that they derived from the code.
This sets the main constraint: builders use the output, not only readers. Thus, the output is numbered, traceable and verifiable. It states clearly what it does not know.
| Item | Value |
|---|---|
| Input | A folder of Oracle PL/SQL .sql files. STATUTE only reads them |
| Output | brd.md, brd_index.json, gaps_register.json, Mermaid diagrams (*.mmd), import.cypher and CSV files |
| Agents | 8 independent Python CLI programs, run in sequence by a person |
| Orchestrator | None. Versioned JSON artifacts on disk connect the agents |
| Language model calls | 0. A formal grammar, sqlglot and rules produce all output |
| Reference corpus | src/: 7 files, 940 lines, 603 lines of code |
| Reference result | 41 business rules, 21 gaps, 7 diagrams, a graph of 353 nodes and 769 relationships |
| Traceability | Each rule cites a file and a line through its statement_id |
| Tests | 414 checks in 8 suites, all passed (see 20) |
| Optional consumer | Neo4j. No agent needs it |
flowchart LR
A[("src/*.sql<br/>Oracle PL/SQL")] --> B["8-stage<br/>deterministic pipeline"]
B --> C[("brd.md<br/>Business Requirements")]
B --> D[("*.mmd<br/>ERD, flows, states")]
B --> E[("import.cypher<br/>knowledge graph")]
B --> F[("brd_index.json<br/>machine-readable")]
style B fill:#eaf3fb,stroke:#2c6fad,stroke-width:2px
The .claude/agents/*.md files are specifications for the development harness. The pipeline never reads them.
These results come from the reference corpus in src/:
| Deliverable | Detail |
|---|---|
| Business Requirements Document | brd.md, 2,549 lines · 41 requirements · 4 parts for 4 audiences · contents with links |
| Machine-readable index | brd_index.json: the same content as structured data |
| Gaps register | gaps_register.json: 21 open matters, ranked by severity |
| Diagrams | ERD · system data-flow map · entity state model · 5 process flows |
| Knowledge graph | 353 nodes and 769 relationships · Cypher with MERGE and CSV files |
| Query interface | 12 plain-English questions. You can ask them with or without Neo4j |
STATUTE does not generate replacement code. Published results show that extraction gets approximately 90 % precision and recall. End-to-end code generation gets approximately 9 %. Extraction is a problem with a sufficient solution. Translation is not. Thus, the pipeline stops where the evidence stops.
Two findings caused this decision:
- Empirical. No automated metric of documentation quality correlates well with expert judgement. The best reported correlation is r = 0.34. On difficult material, the agreement between experts fell to ICC 0.12. Thus, "the model checked it" gives no safety.
- Practical. A person who uses a specification cannot detect a fabricated business rule in it.
A deterministic pipeline that misses a rule fails visibly. A generative pipeline that invents a rule fails invisibly.
For a document that a team rebuilds from, the first failure mode is better.
| # | Agent | Script (lines) | Job | Dependencies | Detail document |
|---|---|---|---|---|---|
| 1 | Inventory | 01_inventory.py (724) |
Classify the files and give each file a stable file_id |
none | agent-01 |
| 2 | Parser | 02_parser.py (858) |
ANTLR4 → statements, CFG, statement_id |
ANTLR4, sqlglot | agent-02 |
| 3 | Data | 03_data.py (1,431) |
DDL → schema, real enforcement state and ERD | ANTLR4 | agent-03 |
| 4 | Logic | 04_logic.py (917) |
Pseudocode, complexity, slices, CRUD | none | agent-04 |
| 5 | Rules | 05_rules.py (1,198) |
9 sources → business rules with a confidence | none | agent-05 |
| 6 | Diagram | 06_diagram.py (1,207) |
Model that is independent of the renderer → Mermaid | none | agent-06 |
| 7 | Synthesis | 07_synthesis.py (1,308) |
The BRD, the index and the gaps register | none | agent-07 |
| 8 | Graph | 08_graph.py (419) |
Property graph and questions (optional) | none | agent-08 |
All scripts are in .claude/scripts/. Three libraries support them:
| Library | Lines | Purpose | Used by |
|---|---|---|---|
lib_business_language.py |
271 | Changes identifiers into business language | Agents 7 and 8 |
lib_graph_model.py |
376 | In-memory property graph | Agent 8 |
lib_graph_language.py |
433 | Catalogue of plain-English intents | Agent 8 |
| Capability | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| Read the source files | ● | ● | ● | ● | ● | ● | ||
| Classify and route the files | ● | ○ | ○ | |||||
| Parse the PL/SQL grammar | ● | ● | ||||||
| Statement tree and CFG | ● | ○ | ○ | ○ | ○ | ○ | ||
| Schema and enforcement | ● | ○ | ○ | ○ | ○ | |||
| Complexity, slices, CRUD | ● | ○ | ○ | ○ | ○ | |||
| Business rules | ○ | ○ | ● | ○ | ○ | ○ | ||
| Diagrams | ● ERD | ● | ○ | |||||
| Document assembly | ● | |||||||
| Graph and questions | ● |
● owns · ○ uses
flowchart TD
SRC[("src/*.sql")] --> A1
A1["<b>1 Inventory</b><br/>classify + stable file_id"]
A2["<b>2 Parser</b><br/>ANTLR4 → statements + CFG"]
A3["<b>3 Data</b><br/>DDL → schema + enforcement + ERD"]
A4["<b>4 Logic</b><br/>pseudocode + complexity + slices"]
A5["<b>5 Rules</b><br/>9 sources → business rules"]
A6["<b>6 Diagram</b><br/>model → Mermaid"]
A7["<b>7 Synthesis</b><br/>the BRD"]
A8["<b>8 Graph</b><br/>knowledge graph + questions"]
A1 --> A2
A1 --> A3
A2 --> A3
A2 --> A4
A2 --> A5
A3 --> A5
A4 --> A5
A2 --> A6
A3 --> A6
A4 --> A6
A5 --> A6
A1 --> A7
A2 --> A7
A3 --> A7
A4 --> A7
A5 --> A7
A6 --> A7
A7 --> A8
A7 --> OUT[("brd.md<br/>brd_index.json<br/>gaps_register.json")]
A6 --> MMD[("diagrams/*.mmd")]
A3 --> ERD[("erd.mmd")]
A8 --> CYP[("import.cypher<br/>+ CSVs")]
ANN[("brd_annotations.json")] -.->|read-only| A7
classDef opt stroke-dasharray: 4 2
class A8 opt
Eight independent CLI programs. Versioned JSON artifacts on disk connect them. There is no orchestrator, no shared state object, no service and no database.
flowchart LR
subgraph Ext["Outside the system"]
DEV["Analyst / Architect"]
SME["Business SME"]
NEO["Neo4j<br/><i>optional consumer</i>"]
end
subgraph Sys["STATUTE — local CLI pipeline"]
SRC[("src/*.sql")]
PIPE["8 agent scripts"]
OUT[("output/<br/>versioned artifacts")]
end
DEV -->|runs the agents| PIPE
SRC --> PIPE
PIPE --> OUT
OUT --> DEV
SME -->|annotations, read-only| PIPE
OUT -.->|manual import| NEO
classDef ext fill:#f5f5f5,stroke:#999
class DEV,SME,NEO ext
STATUTE has no inbound interface. It has no API, no queue and no scheduler integration. Only a person who runs a command starts an agent.
| Layer | Technology | Where | Why |
|---|---|---|---|
| Language | Python 3.11+ | All agents | The code uses the X | Y type-hint syntax |
| Parse | ANTLR4 and a vendored Oracle PL/SQL grammar (Apache 2.0) | Agents 2 and 3 | A regex fails on nested blocks, on string literals that contain keywords and on CASE inside CASE |
| SQL decomposition | sqlglot (Oracle dialect) | Agent 2 | It gets the tables and columns from DML at a lower cost than a walk of the ANTLR tree |
| Diagrams | Mermaid | Agents 3 and 6 | GitHub, VS Code and most wikis show Mermaid with no extra tools |
| Graph target | Neo4j | Agent 8 | Optional consumer. Never a runtime dependency |
| All other functions | Python standard library | All agents | No dependency risk |
Verified as absent from the pipeline scripts: model calls · network calls · environment variables · database drivers · secrets · logging · Dockerfile · CI that runs the tests · dependency manifest.
.claude/
agents/ 8 agent specifications (harness metadata and rationale)
scripts/
01_inventory.py (724) Agent 1
02_parser.py (858) Agent 2
03_data.py (1,431) Agent 3 (largest)
04_logic.py (917) Agent 4
05_rules.py (1,198) Agent 5
06_diagram.py (1,207) Agent 6
07_synthesis.py (1,308) Agent 7
08_graph.py (419) Agent 8
lib_business_language.py (271) identifier → business language (Agents 7, 8)
lib_graph_model.py (376) in-memory property graph (Agent 8)
lib_graph_language.py (433) plain-English intent catalogue (Agent 8)
vendor/plsql_grammar/ vendored ANTLR4 grammar and NOTICE.md
archive/ old early scripts (not in the pipeline)
skills/ 2 skill definitions (file-catalog, reference-graph)
src/ the reference corpus: 7 PL/SQL files
openwiki/ OpenWiki pages about the corpus (not pipeline output)
.github/workflows/ openwiki-update.yml (inside src/, so GitHub does not run it)
AGENTS.md, CLAUDE.md OpenWiki context notes
tests/
test_*.py 8 suites, 414 checks
evaluate_rules.py the only evaluation harness
fixtures/ground_truth/ 4 annotated procedures and BASELINE.json
fixtures/sample_*/ sample PL/SQL, DDL and logic inputs for the suites
output/ the latest committed run of each agent
docs/ 15 technical documents and the STE style guide
tools_antlr_build/ grammar regeneration (ANTLR jar, grammar, explore_ddl.py)
reference/ COBOL reference harness. Git ignores it. Design guidance only
The output is a pure function of the input. Rules assemble each sentence from parse trees. Thus, the same source always gives the same document, and no rule is invented.
| Identifier | Form | Owner |
|---|---|---|
file_id |
SLUG(rel_path) + "__" + SHA256(rel_path)[0:8] |
Agent 1 |
object_id |
TYPE-OWNER.NAME, with :: for package members |
Agent 2 |
statement_id |
file_id__object_id__STMT_nnnn |
Agent 2 |
column_id |
TABLE.COLUMN |
Agent 3 |
rule_id |
BR-nnn |
Agent 5 |
gap_id |
GAP-nnn |
Agent 7 |
The hash uses the path, not the contents. Thus, a file keeps its identity after an edit. This makes the exact traceability matrix and the annotation layer possible.
Each agent writes output/<stage>/<timestamp>/. Only after this write does it update output/<stage>/latest.json.
If an agent stops during a write, it leaves an orphan directory. The pointer still names the last good run, so the downstream agents are not affected.
The parser never interprets meaning. The rules agent never parses again. The diagram agent draws only what earlier agents found. When a defect occurs, only one location can contain it.
3.5 Confidence is data, and it is never hidden
STATUTE marks a rule that it infers from structure as needs review. It publishes a DISABLED database constraint with a warning that the database does not enforce it.
When a diagram is too large to read, the diagram states this. All of these items go into the gaps register.
Agents 6 and 8 build an in-memory model first and render it last. Thus, the node budget is enforceable, because there is a model to count. Tests can assert the meaning, not the string format. The local answers of the graph and its Neo4j answers cannot disagree.
flowchart TD
START(["Operator runs Agent 1"]) --> S1["01_inventory.py src"]
S1 --> C1{"exit 0?"}
C1 -->|no| ABORT["Pipeline stops.<br/>latest.json not changed."]
C1 -->|yes| S2["02_parser.py"]
S2 --> S3["03_data.py"]
S3 --> S4["04_logic.py"]
S4 --> S5["05_rules.py"]
S5 --> S6["06_diagram.py"]
S6 --> C6{"validation passed?"}
C6 -->|no| FAIL6["problem list on stderr<br/>exit 1"]
C6 -->|yes| S7["07_synthesis.py"]
S7 --> S8["08_graph.py (optional)"]
S8 --> DONE(["brd.md + diagrams + graph"])
S1 -.->|latest.json| S2
S2 -.->|latest.json| S3
S5 -.->|latest.json| S6
S6 -.->|latest.json| S7
S7 -.->|brd_index.json| S8
classDef err fill:#fdecea,stroke:#c0392b
class ABORT,FAIL6 err
The operator controls the sequence. No script starts another script. Each script prints the next command, with the pinned run version.
The committed run (output/*/latest.json, 2026-07-29) printed these results:
Stage 1 7 files, 940 lines total, 603 code
Stage 2 5 objects, 124 statements, 0 parse errors, 0 unresolved calls
Stage 3 15 tables, 7 FKs + 1 inferred, 1 CHECK, 6 comment-only enums flagged
Stage 4 151 pseudocode lines, 25 slices, 1 over threshold, 1 SAVEPOINT hazard
Stage 5 41 rules — 19 validation, 12 limit-check, 6 calculation, 4 error-handling
Stage 6 7 figures, 100% decision labels, 100% branch traceability
Stage 7 41 requirements, 21 glossary terms, 41 traceability rows, 21 gaps
Stage 8 353 nodes / 769 relationships, 12 questions, 4 blind spots
flowchart LR
SQL[("*.sql")] --> INV["inventory-artifact.json"]
INV --> PAR["parser_artifact.json<br/>+ raw_structure/*"]
SQL -.->|read again| PAR
SQL -.->|read again for conditions| LOG
SQL -.->|read again for conditions| RUL
SQL -.->|read again for SET values| DIA
PAR --> DAT["data_artifact.json<br/>+ erd.mmd"]
PAR --> LOG["logic_artifact.json"]
DAT --> RUL["rules_artifact.json"]
LOG --> RUL
PAR --> RUL
RUL --> DIA["diagrams_artifact.json<br/>+ *.mmd"]
PAR --> DIA
DAT --> DIA
LOG --> DIA
INV --> SYN["brd.md + brd_index.json<br/>+ gaps_register.json"]
PAR --> SYN
DAT --> SYN
LOG --> SYN
RUL --> SYN
DIA --> SYN
ANN[("annotations")] -.-> SYN
SYN --> GRA["import.cypher + CSVs"]
The lineage of one rule, from end to end:
src/05_medium_fund_transfer_with_validation.sql : line 79
→ Agent 1 file_id = 05_MEDIUM_FUND_TRANSFER_WITH_VALIDATION__5E3A3BBB
→ Agent 2 statement_id = <file_id>__PROC-.SP_TRANSFER_FUNDS__STMT_00nn
→ Agent 5 BR-002, source.statement_id + line 79
→ Agent 6 decision node + BRANCH edge labelled "BR-002"
→ Agent 7 §5 rule block + §12 traceability row citing file:line
→ Agent 8 (:BusinessRule {rule_id:"BR-002"})-[:IMPLEMENTED_AT]->(:Statement)
STATUTE has no shared state object, session, thread or checkpointer. The file system holds all state.
stateDiagram-v2
[*] --> NoRun
NoRun --> RunInProgress : agent starts
RunInProgress --> OrphanRun : stop before write
RunInProgress --> RunWritten : artifact on disk
RunWritten --> RunPublished : latest.json updated
RunPublished --> RunInProgress : next agent
OrphanRun --> RunInProgress : run again
RunPublished --> [*]
note right of OrphanRun
The orphan directory stays.
latest.json still points to
the last good run, so
downstream is not affected.
Nothing removes it.
end note
Each agent has the same shape:
flowchart TD
A["argparse"] --> B["load_run() upstream<br/>from latest.json or a pinned run"]
B --> C{"required inputs present?"}
C -->|no| D["exception → non-zero exit"]
C -->|yes| E{"optional inputs present?"}
E -->|no| F["degrade + record"]
E -->|yes| G["full capability"]
F --> H["process"]
G --> H
H --> I["write the versioned run directory"]
I --> J{"write OK?"}
J -->|no| K["latest.json NOT updated"]
J -->|yes| L["update latest.json"]
L --> M["print summary + next command"]
classDef err fill:#fdecea,stroke:#c0392b
class D,K err
Graceful degradation is in Agent 5 (the logic artifact is optional) and in Agents 6 and 8 (many optional inputs). Agent 7 needs all six upstream artifacts.
flowchart TD
E["Error occurs"] --> T{"Type"}
T -->|Required artifact absent| A["exception → non-zero exit"]
T -->|Optional artifact absent| B["degrade + record<br/>agent succeeds"]
T -->|Parse error in one file| C["issues[] entry<br/>run continues"]
T -->|Source file not readable| D["status + warnings<br/>counted in summary"]
T -->|Diagram validation problem| F["stderr list → exit 1"]
T -->|Annotations not valid| G["empty dict<br/>placeholders shown"]
T -->|Graph edge to an absent node| H["dropped with no message"]
T -->|Question with no match| I["structured refusal → exit 2"]
A --> R["latest.json not changed<br/>previous run stays current"]
F --> R
R --> RE["Recovery: correct the cause and run again.<br/>Nothing to remove."]
classDef err fill:#fdecea,stroke:#c0392b
classDef ok fill:#eaf3fb,stroke:#2c6fad
class A,F err
class B,C,D,G,I ok
No agent retries. There is no backoff, because there is no external service. All agents are idempotent, except for the timestamp and the run directory.
The operator starts each agent. The agents communicate only through output/<stage>/latest.json and the run directories.
sequenceDiagram
autonumber
actor OP as Operator
participant A1 as 01_inventory.py
participant A2 as 02_parser.py
participant A35 as 03_data.py to 05_rules.py
participant A6 as 06_diagram.py
participant A7 as 07_synthesis.py
participant A8 as 08_graph.py
participant FS as output/ on disk
participant ANN as brd_annotations.json
OP->>A1: python 01_inventory.py src
A1->>FS: inventory run directory, then latest.json
A1-->>OP: summary and the next command
OP->>A2: python 02_parser.py
A2->>FS: read inventory latest.json
A2->>FS: parser_artifact.json and raw_structure, then latest.json
OP->>A35: run Agents 3, 4 and 5 in sequence
A35->>FS: read upstream latest.json files
A35->>FS: data, logic and rules artifacts, then latest.json
OP->>A6: python 06_diagram.py
A6->>FS: read parser, data, logic, rules, inventory
alt validate_spec finds a problem
A6-->>OP: problem list on stderr, exit 1
else validation passed
A6->>FS: diagrams_artifact.json and diagrams, then latest.json
end
OP->>A7: python 07_synthesis.py
A7->>FS: read all six upstream artifacts
A7->>ANN: read only, if present
A7->>FS: brd.md, brd_index.json, gaps_register.json
opt knowledge graph
OP->>A8: python 08_graph.py, or --ask a question
A8->>FS: read seven artifacts
A8->>FS: import.cypher, nodes and rels CSV files
A8-->>OP: answer rows and Cypher, or a refusal with exit 2
end
Purpose. Examine the source tree, find the role of each file and give each file a stable identity.
| Input | Output |
|---|---|
| A file system directory (positional argument) | inventory-artifact.json |
flowchart TD
CLI["01_inventory.py sql_dir"] --> VAL{"directory valid?"}
VAL -->|no| ERR["stderr + exit 1"]
VAL -->|yes| DISC["discover()<br/>recursive .sql walk"]
DISC --> SKIP{"should_exclude()"}
SKIP -->|excluded| DISC
SKIP -->|kept| READ["read_file_safe()<br/>encoding fallback"]
READ -->|not readable| WARN["status + warnings<br/>counted, not fatal"]
READ -->|ok| PROC["process_file()"]
PROC --> FID["make_file_id()<br/>SLUG + SHA256(path)[0:8]"]
PROC --> ROLE["classify_file_role()"]
PROC --> HINTS["extract_content_hints()<br/>25 regex signals"]
PROC --> CX["infer_complexity()"]
FID --> SUM["build_summary()"]
ROLE --> SUM
HINTS --> SUM
CX --> SUM
SUM --> W["inventory-artifact.json"]
W --> PTR["latest.json (success only)"]
classDef err fill:#fdecea,stroke:#c0392b
class ERR,WARN err
Options. --exclude (-e), --no-default-excludes, --encoding, --no-content-hints, --verbose (-v), --output (-o) and --output-root.
Output contents. inventory-artifact.json has three parts:
file_index: the map fromfile_idto the file namefile_metadata: the role, line counts, SHA-256, encoding, status, warnings and complexity of each filesummary: 15 counters
Why it is important. Downstream routing uses the file role. Agent 2 parses only files with a role in PARSE_WORTHY_ROLES. Agent 3 parses only DDL.
A real defect occurred here. Agent 1 classified files that contain CREATE VIEW as mixed. These files never got to Agent 3, and their tables were not in the data model. Agent 3 now has a DDL_CONTENT_HINTS mechanism that compensates for this.
Rules
- The classification uses the file content, never the file name. A file with the name
utils.sqlthat containsCREATE TABLEis DDL. - An unreadable file is counted with a status and warnings. It does not stop the run.
latest.jsonchanges only after a successful write.
Tests. 22 checks and a golden-fixture diff after normalisation.
Purpose. Change the PL/SQL text into structure. This is the only agent that reads the source characters for structure.
| Input | Output |
|---|---|
inventory-artifact.json and the source files |
parser_artifact.json and raw_structure/*.json (one file for each object) |
flowchart TD
IN["inventory-artifact.json"] --> ROUTE["route_files()"]
ROUTE -->|schema_ddl / seed_data| PASS["passthrough → Agent 3"]
ROUTE -->|package/procedure/function/<br/>trigger/mixed| PA["PASS A: discover_objects()"]
PA --> LEX["ANTLR4 PlSqlLexer"]
LEX --> PARSE["ANTLR4 PlSqlParser"]
PARSE -->|syntax error| ISSUE["issues[]"]
PARSE --> WRAP{"wrapped object?"}
WRAP -->|yes| SKIP["wrapped_object_skipped"]
WRAP -->|no| OMAP["object map"]
OMAP --> PB["PASS B: extract_statements()"]
PB --> CLS["classify_statement()"]
CLS --> TREE["nested tree<br/>parent_id + scope_path"]
CLS -->|DML| SG["sqlglot enrichment<br/>tables / writes / predicate_reads"]
CLS -->|CALL| RES["resolve against the object map"]
TREE --> CFG["build_cfg()<br/>SEQUENCE / BRANCH_ENTRY /<br/>EXCEPTION_EDGE / LOOP_BACK_EDGE"]
CFG --> OUT["raw_structure/*.json<br/>+ parser_artifact.json"]
classDef err fill:#fdecea,stroke:#c0392b
class ISSUE,SKIP err
Why two passes. A call in file A can go to an object in file B. The agent cannot resolve the call before it knows all objects. Pass A finds each object in the run. Pass B extracts the statements and resolves the references.
The statement identifier is the primary join key of the pipeline:
04_MEDIUM_PROCESS_MONTHLY_INTEREST_CREDIT__B115DB87__PROC-.SP_PROCESS_MONTHLY_INTEREST_CREDIT__STMT_0004
The sequence number starts with zeros, so a string sort is equal to a numeric sort. The identifier contains the file_id, so it has the same path stability.
Known traps
reads[]contains columns and parameters. A liveSELECT_INTOrecord listsp_from_account(a parameter) together with real columns. Consumers must filter the list. Only Agent 8 filters it structurally.- The agent stores no condition text. An
IFrecord has onlynesting_depth. Agents 4, 5 and 6 each read the source again and cut out the condition. Thus, three implementations do one task. - The generated ANTLR Python uses
this.in place ofself.You must patch it after each regeneration of the grammar (seevendor/plsql_grammar/NOTICE.md).
Past defects that tests now guard
build_cfglinked only siblings. A decision had no edge to any branch.classify_statementreturned"IF_STATEMENT", but the recursion checked"IF".- A search of direct children did not find
Into_clauseContext. - sqlglot mixed the
UPDATE SETtargets with theWHEREcolumns.
Tests. 31 checks.
Purpose. Build the physical data dictionary. Record which rules the database really enforces.
| Input | Output |
|---|---|
| The inventory and the parser artifact | data_artifact.json and erd.mmd |
flowchart TD
INV["inventory"] --> ROUTE["route_ddl_files()<br/>DDL_ROLES OR DDL_CONTENT_HINTS"]
PAR["parser artifact"] --> XVAL["cross-validate refs<br/>resolve synonyms + views"]
ROUTE --> ANTLR["ANTLR4 parse"]
ANTLR --> COLS["extract_column()<br/>virtual / IDENTITY / default"]
ANTLR --> CONS["parse_constraint_state()"]
CONS --> ENF["enforcement_summary()<br/>STATUS x VALIDATED"]
ANTLR --> OTHER["FKs / CHECK / UNIQUE /<br/>indexes / sequences / partitioning"]
COLS --> TYPE["map_oracle_type()<br/>Oracle → normalized → PySpark"]
TYPE --> CAT["column_catalogue<br/>+ usage_count"]
XVAL --> CAT
ENF --> CAND["ddl_rule_candidates → Agent 5"]
CAT --> ART["data_artifact.json"]
OTHER --> ERD["erd.mmd<br/><i>owned here, indexed by Agent 6</i>"]
The two-axis enforcement model (the central idea). An Oracle constraint has two independent axes, not one. A constraint can exist and not be enforced.
| STATUS | VALIDATED | is_enforced |
confidence |
Meaning |
|---|---|---|---|---|
| ENABLED | VALIDATED | true |
enforced |
Enforced for all data |
| ENABLED | NOT VALIDATED | true |
enforced_new_data_only |
New rows only. Old rows can violate it |
| DISABLED | either | false |
not_enforced |
Documented intent only |
Agent 5 maps this value to (signal_strength, confidence, requires_sme_review):
"enforced" → (5, "confirmed", False)
"enforced_new_data_only" → (4, "high", True)
"not_enforced" → (2, "low", True)
Design decision. Agent 3 keeps a DISABLED constraint as a rule. A dropped constraint hides documented business intent.
The rule gets a low score and a review flag. Its BRD statement says that the database does not enforce it.
Whitespace defect, corrected. The ANTLR getText() removes whitespace. It gave account_statusIN('ACTIVE') and a rule with the name "Restrict Account Statusin". Now original_text_of() uses the token stream to keep the source text.
Name trap. The foreign-key field is references_table, not referenced_table. A query for the incorrect name returns None, and the result looks like absent data.
Type triple. NUMBER(18,2) → DECIMAL → DecimalType(18,2). Agent 7 publishes the third value as the Target type column for a rebuild.
Tests. 76 checks, the second largest suite.
Purpose. Make the control flow readable and measurable.
| Input | Output |
|---|---|
The parser records, and the inventory abs_path to read the source again |
logic_artifact.json and program_logic/*_logic.json |
flowchart TD
PAR["parser records"] --> LOOP{"for each object"}
INV["inventory abs_path"] --> SRC["cut the raw .sql again<br/>for condition text"]
LOOP --> TREE["walk parent_id tree"]
SRC --> PSEUDO
TREE --> PSEUDO["render_object_pseudocode()<br/>keeps IF/ELSIF/ELSE nesting"]
TREE --> CYC["compute_cyclomatic()<br/>M = D + 1"]
TREE --> COG["compute_cognitive()<br/>nesting-weighted"]
TREE --> SLICE["slice_for_variable()<br/>backward slice + control ancestors"]
TREE --> TX["analyse_transactions()<br/>hazards"]
TREE --> SHAPE["classify_shape()"]
TREE --> LOOPS["loop termination"]
PSEUDO --> REC["record for each object"]
CYC --> REC
COG --> REC
SLICE --> REC
TX --> REC
SHAPE --> REC
LOOPS --> REC
REC --> ART["logic_artifact.json"]
Cyclomatic complexity (McCabe 1976): M = D + 1, where
The threshold is 10. Live example: the breakdown if=2, elsif=2, loop=1, exception_handler=2 gives D = 7 and M = 8. The highest value in the corpus is SP_TRANSFER_FUNDS with M = 16.
Cognitive complexity (Campbell): one increment for each break in the flow, plus a nesting increment. The threshold is 15. No object in the corpus is above it.
Backward slices (Weiser 1981): for each variable, each statement that sets its value, with the control ancestors. The corpus has 25 slices. They supply the variable_derivation rules of Agent 5.
Transaction hazards
| Hazard | Found in the corpus | Severity |
|---|---|---|
COMMIT_INSIDE_LOOP |
0 | — |
SAVEPOINT_PARTIAL_ROLLBACK |
1 | high |
NO_TRANSACTION_CONTROL |
2 | info |
Process shapes. BATCH_PROCESSOR, SINGLE_RECORD_TRANSACTION, CALCULATION and QUERY_ONLY. Each shape has a rationale string.
Explicit non-goal: dead-code detection. Schedulers or code outside the repository often call PL/SQL objects. Thus, Agent 4 reports "no internal callers" as information only, never as a finding.
Tests. 46 checks.
Purpose. Find the business decisions and state them as requirements. This agent is the intellectual core of the pipeline. It is also the only agent with a measured accuracy.
| Input | Output |
|---|---|
ddl_rule_candidates (Agent 3), statements and CFG (Agent 2), variable_slices (Agent 4, optional) |
rules_artifact.json with rule_sets, business_rules and error_handling_catalogue |
flowchart TD
DDL["ddl_rule_candidates<br/>(Agent 3)"] --> M1["mine_from_ddl_candidates()"]
PAR["statements + CFG<br/>(Agent 2)"] --> M2["mine_from_statements()"]
LOG["variable_slices<br/>(Agent 4) — OPTIONAL"] --> M3["mine_from_variable_slices()"]
M2 --> C1["cursor WHERE → eligibility"]
M2 --> C2["IF/ELSIF/ELSE → one rule per branch"]
M2 --> C3["CASE WHEN/ELSE → one rule per branch"]
M2 --> C4["RAISE → obligation (SBVR)"]
M2 --> C5["handlers → predefined / failure_isolation /<br/>error_contract / generic"]
M1 --> ENF["_ENFORCEMENT_TO_CONFIDENCE"]
M3 --> DER{"_is_derivation AND<br/>_assigns_variable?"}
DER -->|no| DROP["dropped"]
C1 --> RULE["raw rules"]
C2 --> RULE
C3 --> RULE
C4 --> RULE
C5 --> SPLIT{"generic handler?"}
SPLIT -->|yes| CAT["error_handling_catalogue"]
SPLIT -->|no| RULE
ENF --> RULE
DER -->|yes| RULE
RULE --> NAME["describe_comparison()<br/>subject + qualifier from ONE clause"]
NAME --> DEDUP["deduplicate()<br/>obligation wins, then signal_strength"]
DEDUP --> SORT["assign BR-nnn"]
SORT --> ART["rules_artifact.json"]
classDef drop fill:#f5f5f5,stroke:#999,stroke-dasharray: 4 2
class DROP,CAT drop
| Source kind | Trigger |
|---|---|
conditional_branch |
Each IF, ELSIF or ELSE branch |
case_branch |
Each CASE WHEN or ELSE |
cursor_eligibility |
The WHERE clause of a cursor: which records the process applies to |
named_exception |
A guarded RAISE, stated again as the obligation that it enforces |
predefined_exception |
An Oracle exception that the database found |
failure_isolation |
WHEN OTHERS that logs and continues: a resilience requirement |
error_contract |
WHEN OTHERS that raises a specific error that callers depend on |
variable_derivation |
Business formulas from backward slices |
ddl_* |
CHECK, virtual column, unique constraint or view filter |
The diagram shows which function mines each source kind, and how the category is set.
flowchart LR
subgraph ST["mine_from_statements()"]
K1["conditional_branch"]
K2["case_branch<br/>mine_from_case_expression()"]
K3["cursor_eligibility<br/>mine_from_cursors()"]
K4["named_exception<br/>_raise_to_obligation()"]
K5["predefined_exception"]
K6["failure_isolation"]
K7["error_contract"]
K0["generic_exception"]
end
K8["variable_derivation<br/>mine_from_variable_slices()"]
K9["ddl_check_constraint, ddl_virtual_column,<br/>ddl_unique_*, ddl_view_filter<br/>mine_from_ddl_candidates()"]
K1 --> CC["classify_category():<br/>keyword score, CATEGORY_PRIORITY"]
K2 --> CC
K4 --> CC
K9 --> CC
K3 --> VAL["VALIDATION"]
K5 --> EH["ERROR_HANDLING"]
K6 --> EH
K7 --> EH
K8 --> CALC["CALCULATION"]
K0 --> CAT[("error_handling_catalogue,<br/>never a BR-nnn rule")]
CC --> RULES["Raw rules"]
VAL --> RULES
EH --> RULES
CALC --> RULES
Live distribution. VALIDATION 19 · LIMIT_CHECK 12 · CALCULATION 6 · ERROR_HANDLING 4. The committed run also merged 5 duplicates and marked 3 rules for SME review.
SBVR states: "there are no exceptions; instead, there are well stated business rules."
flowchart TD
R[/"RAISE e_x statement"/] --> N{"Exception name is<br/>empty or OTHERS?"}
N -- "yes" --> NONE["No obligation"]
N -- "no" --> IF{"An IF is a control<br/>ancestor of the RAISE?"}
IF -- "yes" --> COND["extract_if_condition():<br/>the violation condition"]
COND --> INV["describe_comparison(invert=True):<br/>negate the operator"]
INV --> KEY["raw_key = IF::object::line<br/>is_obligation = true"]
IF -- "no" --> H{"Inside a NO_DATA_FOUND<br/>handler?"}
H -- "yes" --> EXIST["Existence check:<br/>the queried row must exist"]
KEY --> DED["deduplicate(): same raw_key<br/>as the IF condition rule"]
DED --> WIN[/"One rule, obligation phrasing wins<br/>p_amount <= 0 becomes Validate Amount above 0"/]
Agent 5 merges a guarded RAISE with the IF that guards it. It states the rule as the condition that must be true.
The IF condition is the violation, so the rule is its negation:
p_amount <= 0 → "Validate Amount above 0"
tests/evaluate_rules.py compares the extracted rules with 4 hand-annotated procedures. It matches on the source-line distance (LINE_TOLERANCE = 2), not on the words.
flowchart TD
B["build_pipeline(): run Agents 1 to 5<br/>on src/ in a temporary folder"] --> RA[/"rules_artifact.json"/]
GT[/"fixtures/ground_truth/*.json<br/>4 procedures"/] --> EO["evaluate_object()<br/>for each procedure"]
RA --> EX["extracted_rules_for(object_id)"]
EX --> EO
EO --> CL{"Closest unmatched extracted rule<br/>within 2 lines?"}
CL -- "yes" --> M["Match.<br/>Record subject agreement separately"]
CL -- "no" --> MISS["Missed ground-truth rule"]
M --> PRF["P = matched / extracted<br/>R = matched / ground truth<br/>F1 = 2PR / (P + R)"]
MISS --> PRF
PRF --> MODE{"Option"}
MODE -- "none" --> REP[/"Report, with COBREX, COBRAIN<br/>and A-COBREX for reference"/]
MODE -- "--baseline" --> BL[("BASELINE.json")]
MODE -- "--compare" --> DIFF[/"Change against BASELINE.json"/]
| Measurement | Precision | Recall | F1 |
|---|---|---|---|
Baseline (before the redesign, BASELINE.json) |
0.615 | 0.571 | 0.593 |
| Current (tuned) | 1.000 | 1.000 | 1.000 |
| First blind held-out procedure | — | — | 0.588 |
| Second blind held-out procedure | 1.000 | 0.400 | 0.571 |
The 1.000 is contaminated. The ground truth covers 4 of 5 procedures, and the team used each of them to correct the extractor. The blind figures are the defensible estimates of generalisation.
The harness cites these published baselines for comparison: COBREX F1 0.59 · COBRAIN 0.73 · A-COBREX P 0.62 / R 0.74.
flowchart TD
S[/"Statement in a backward slice<br/>of variable v"/] --> T{"Statement type"}
T -- "ASSIGNMENT" --> AG{"Aggregate function<br/>on the right side?"}
AG -- "yes" --> AS
AG -- "no" --> SC{"Arithmetic operators plus<br/>function calls at or above 2?"}
SC -- "no" --> D1["Dropped: mechanics, not a formula"]
SC -- "yes" --> AS{"_assigns_variable:<br/>target of the assignment is v?"}
T -- "SELECT_INTO" --> AGG{"Aggregate function?"}
AGG -- "no" --> D1
AGG -- "yes" --> AS
AS -- "no" --> D2["Dropped: formula belongs<br/>to another variable"]
AS -- "yes" --> R[/"variable_derivation rule,<br/>CALCULATION"/]
C[/"IF or guard condition"/] --> Z{"_is_zero_guard:<br/>comparison with literal 0?"}
Z -- "yes" --> V[/"Category VALIDATION"/]
Z -- "no" --> K[/"classify_category():<br/>keyword score"/]
_is_derivation. The score is the number of arithmetic operations plus the number of function calls. A score of 2 or more means a business formula. An assignment with an aggregate function is always a formula. ASELECT INTOis a formula only when it has an aggregate function.ROUND(bal*(rate/100)*(days/365),2)scores 5 ✅.rec.balance + v_interest_amountscores 1 ❌. Only 2 examples support this threshold._assigns_variable. The statement that derives the value must assign the slice variable. Without this guard,v_new_balancetook the formula that calculatesv_interest_amount, and one formula became two rules._is_zero_guard. A comparison with the literal zero is a sanity guard, not a business tier. Without this guard,v_interest_amount > 0matched the CALCULATION keywords and gave "Calculate Interest Amount above 0".
CASE decomposition. Agent 5 changes each branch of a CASE expression into one rule. On a blind procedure, a CASE that stayed whole lost 6 of 10 rules (recall 0.400).
Tests. 33 checks.
Purpose. Make the visual layer. Agent 6 builds a model that is independent of the renderer, then writes Mermaid.
| Input | Output |
|---|---|
| The parser artifact (required). The data, logic, rules and inventory artifacts (optional) | diagrams_artifact.json and diagrams/*.mmd |
flowchart TD
ART["parser (required)<br/>+ data / logic / rules / inventory (optional)"] --> IDX["Indexes<br/>join on statement_id"]
IDX --> M1["build_dataflow_spec()"]
IDX --> M2["build_state_specs()"]
IDX --> M3["build_flow_spec() per object"]
M3 --> LAB["label_for_statement()<br/>tier1 rule / tier2 structured / tier3 fallback"]
M3 --> RED["collapse_runs()<br/>contiguous straight-line ONLY"]
RED --> INV{"decisions / loops / errors /<br/>terminals kept?"}
INV -->|always| ORD["order by source line"]
RED --> OVER{"still over budget?"}
OVER -->|yes| DECL["declare OVERSIZE<br/>→ BRD gaps register"]
M1 --> SPEC["DiagramSpec"]
M2 --> SPEC
ORD --> SPEC
DECL --> SPEC
SPEC --> VS["validate_spec()"]
VS -->|problem| FAIL["stderr + exit 1<br/><b>agent fails</b>"]
VS --> REND["MermaidRenderer.render()"]
REND --> VM["validate_mermaid()"]
VM --> W["diagrams/*.mmd"]
ERD["Agent 3 erd.mmd"] -.->|indexed, never generated again| W
classDef err fill:#fdecea,stroke:#c0392b
class FAIL err
Procedure. LOAD → RESOLVE → MODEL → REDUCE → ORDER → RENDER → VALIDATE → WRITE. The steps up to ORDER write no Mermaid.
What Agent 6 draws
| Diagram | Source |
|---|---|
| ERD | Agent 3. Agent 6 indexes it and never generates it again |
| System data-flow map | Agent 4 CRUD, Agent 2 calls, rule counts and complexity |
| Process flow (one for each object) | Agent 2 CFG ⋈ Agent 5 rules on statement_id |
| Entity state model | Agent 3 CHECK IN-list and the UPDATEs that write that column |
| CRUD matrix | Agent 4. It is a table, because tabular data belongs in a table |
Labels carry meaning. A decision reads Balance below 100,000?. Each branch carries the rule that it enacts (BR-041). 100 % of decisions and 100 % of decision branches get business text.
The node budget is real. DEFAULT_NODE_BUDGET = 40. Contiguous straight-line runs collapse. Decisions, loops, error paths and terminals never collapse.
A second collapse tier was built and then removed. It merged each collapsible child of a parent, also children that were not adjacent. It joined statements at lines 33 and 124 into one node. Thus, the diagram showed that they run together. A diagram that meets its budget with a false flow is worse than a large diagram. When the structure cannot fit, Agent 6 writes the diagram and declares it oversize.
Rules
- This is the only agent that fails on an internal quality problem. A half-valid diagram must not get to the BRD.
- Agent 6 never draws a fact that an earlier agent did not find.
Tests. 52 checks (before: 7). The checks assert the model, not the strings.
Purpose. Assemble the BRD: one document that four different readers can each use.
| Input | Output |
|---|---|
All six upstream artifacts (required) and brd_annotations.json (optional, read-only) |
brd.md, brd_index.json and gaps_register.json |
flowchart TD
A1..6["all six upstream artifacts"] --> CTX["build_context()<br/>capabilities / glossary / tx / loops /<br/>flow map / stats"]
ANN[("brd_annotations.json<br/>read-only")] --> CTX
CTX --> GAPS["detect_gaps()<br/>across all artifacts"]
CTX --> DOC["build_document()"]
GAPS --> DOC
DOC --> BL["lib_business_language<br/>humanise / object_title /<br/>humanise_condition / plain_type"]
DOC --> MOD["rule_modality()<br/>alethic vs deontic"]
DOC --> VER["verification_method()<br/>Inspection vs Test"]
DOC --> PROV["humanise_description()<br/>remove provenance from prose"]
DOC --> TOC["contents_block()<br/>depth-limited"]
DOC --> MD["brd.md"]
CTX --> IDX["brd_index.json"]
GAPS --> REG["gaps_register.json"]
| Machine | Business |
|---|---|
PROC-.SP_TRANSFER_FUNDS |
Transfer Funds |
v_from_balance < p_amount |
From Balance is below Amount |
e_insufficient_balance |
Insufficient Balance |
LAST_TXN_DATE |
Last Transaction Date |
NUMBER(18,2) |
Decimal number (18 digits, 2 decimal places) |
The BRD shows the machine identifier together with the prose. It never replaces the prose with it. The pseudocode and the diagrams stay technical by design.
The diagram shows how humanise_condition() in lib_business_language.py changes a condition:
flowchart LR
IN[/"v_from_balance < p_amount"/] --> L["Keep quoted literals aside"]
L --> ID["For each identifier, humanise():<br/>keep the part after the last dot,<br/>remove P_, V_, L_ and other prefixes,<br/>split on _, expand abbreviations,<br/>title case"]
ID --> OP["Then replace the operators with words.<br/>IS NULL becomes is missing,<br/>IS NOT NULL becomes is present"]
OP --> RL["Put the literals back"]
RL --> OUT[/"From Balance is below Amount"/]
Two defects that a review of the output found
- The table mapped
NOtoNumber. Thus, "no preceding condition matched" became "Number Preceding Condition Matched". Keep ordinary English words out of the abbreviation table. - The operator substitution ran before the identifier substitution. The inserted words went through the identifier pass again and gave "is Below".
| Part | Audience | Chapters |
|---|---|---|
| I — Business View | Sponsors | Executive summary · Scope (in and out) · Glossary · Program units · Data flow · CRUD matrix |
| II — Rules and Behaviour | Analysts | Rules catalogue · Entity state models · Error contracts |
| III — Build Specification | Developers | Data model with target types · Interface contracts · Process specs · Operational characteristics |
| IV — Assurance | Auditors | Traceability matrix · Gaps register · Rebuild checklist |
Each audience needs different documentation for its task. One voice for all readers serves no reader.
flowchart LR
BD["build_document()"] --> P1["Part I: Business View"]
BD --> P2["Part II: Rules and Behaviour"]
BD --> P3["Part III: Build Specification"]
BD --> P4["Part IV: Assurance"]
P1 --> R1(["Sponsors"])
P2 --> R2(["Analysts"])
P3 --> R3(["Developers"])
P4 --> R4(["Auditors"])
RA[/"rules_artifact.json"/] --> P2
DA[/"data_artifact.json"/] --> P3
DG[/"diagrams_artifact.json"/] --> P1
DG --> P2
GP["detect_gaps()"] --> P4
flowchart TD
R[/"One business rule"/] --> F1["Exact condition in code:<br/>condition_text in a code span"]
R --> F2["In plain terms:<br/>humanise_condition()"]
R --> K{"source.kind in _DDL_KINDS?"}
K -- "yes" --> E{"is_enforced is False?"}
E -- "yes" --> DIS["Formal statement: INTENDED,<br/>DISABLED, NOT enforced<br/>Verify: Inspection, DISABLED"]
E -- "no" --> ALE["alethic: It is necessary that...<br/>Verify: Inspection of the schema"]
K -- "no" --> DEO["deontic: It is obligatory that...<br/>Verify: Test of the code path"]
DIS --> F3["Formal statement"]
ALE --> F3
DEO --> F3
F1 --> BLK[/"Rule block in brd.md,<br/>Owner and Priority from annotations"/]
F2 --> BLK
F3 --> BLK
BR-002 — Enforce From Balance at or above Amount
| Exact condition in code | `v_from_balance < p_amount` |
| How to verify | Test (exercise the code path) |
| Owner | _to be assigned_ |
In plain terms. From Balance is below Amount.
Formal statement. It is obligatory that the operation is rejected
when From Balance is below Amount, raising Insufficient Balance.
SBVR modality separates two different kinds of truth:
| Modality | Phrasing | Meaning |
|---|---|---|
| Alethic | "It is necessary that…" | The database makes a violation impossible |
| Deontic | "It is obligatory that…" | A violation is possible. This is why the code checks it |
Agent 7 derives the verification method. A rule that the schema enforces gets Inspection. A rule that the code enforces gets Test.
These are two of the eleven attributes of ISO/IEC/IEEE 29148. The other attributes (Owner, Priority) show as visible blanks. An empty column is an action item. An absent column is invisible.
Traceability matrix. For twenty years, IR-based traceability research got only 19–32 % precision on inferred links. STATUTE builds its links from statement_id, so they are exact.
Tests. 86 checks, the largest suite. The checks assert these properties:
- readability (no identifiers in the prose)
- navigation (each contents link resolves)
- completeness and traceability
- honesty
- annotations in both outputs
Purpose. Make all results queryable. Agent 8 is optional and terminal. No other agent depends on it.
| Input | Output |
|---|---|
| Seven artifacts (the parser artifact is required, six are optional) | graph_artifact.json, import.cypher, nodes/*.csv, rels/*.csv, README.md |
flowchart TD
ART["seven artifacts<br/>(parser required, six optional)"] --> BUILD["lib_graph_model.build_graph()<br/>13 labels, 22 relationship types"]
BUILD --> GUARD["g.rel() endpoint guard<br/>drops edges to absent nodes"]
GUARD --> BS["BlindSpot nodes<br/>DYNAMIC_SQL / EXTERNAL_CALLERS /<br/>UNRESOLVED_CALLS / TRIGGER_SIDE_EFFECTS"]
BS --> MODE{"CLI mode"}
MODE -->|--list-questions| LIST["print 12 intents"]
MODE -->|--ask| ASK["resolve_entity() → match intent"]
ASK -->|no match| REFUSE["refusal + suggestions<br/>exit 2"]
ASK -->|matched| ANSWER["rows + equivalent Cypher<br/>exit 0"]
MODE -->|default| EXP["import.cypher (MERGE)<br/>nodes/*.csv, rels/*.csv<br/>README with cookbook"]
classDef refuse fill:#f5f5f5,stroke:#999,stroke-dasharray: 4 2
class REFUSE refuse
The diagram shows the relationship types that lib_graph_model.build_graph() makes. CALLS, POPULATES, CONSTRAINS and ON_ERROR_REACHES exist in the code but have no instance in the committed run. BlindSpot nodes have no relationships.
erDiagram
File ||--o{ Object : "CONTAINS"
Object ||--o{ Parameter : "HAS_PARAMETER"
Object ||--o{ Statement : "CONTAINS_STATEMENT"
Statement }o--o{ Statement : "FOLLOWS, BRANCHES_TO, LOOPS_BACK_TO, ON_ERROR_REACHES"
Statement }o--o{ Object : "DETERMINES"
Object }o--o{ Object : "CALLS"
Table ||--o{ Column : "HAS_COLUMN"
Table }o--o{ Table : "REFERENCES"
Table }o--o{ Column : "FOREIGN_KEY_ON"
Index }o--o{ Column : "COVERS"
Sequence }o--o{ Column : "POPULATES"
Object }o--o{ Table : "TOUCHES, READS, WRITES"
Object }o--o{ Column : "READS_COLUMN, WRITES_COLUMN"
Statement }o--o{ Column : "READS_COLUMN, WRITES_COLUMN"
BusinessRule }o--|| RuleSet : "BELONGS_TO"
BusinessRule }o--o{ Object : "ENFORCED_IN"
BusinessRule }o--o{ Statement : "IMPLEMENTED_AT"
BusinessRule }o--o{ Table : "CONSTRAINS_TABLE"
BusinessRule }o--o{ Column : "CONSTRAINS"
Column ||--o{ State : "HAS_STATE"
Gap }o--o{ BusinessRule : "AFFECTS"
| Node label | Count | Relationship | Count | Relationship | Count | ||
|---|---|---|---|---|---|---|---|
| Statement | 124 | CONTAINS_STATEMENT | 124 | HAS_PARAMETER | 20 | ||
| Column | 105 | DETERMINES | 119 | AFFECTS | 7 | ||
| BusinessRule | 41 | HAS_COLUMN | 105 | FOREIGN_KEY_ON | 7 | ||
| Gap | 21 | FOLLOWS | 86 | REFERENCES | 7 | ||
| Parameter | 20 | WRITES_COLUMN | 69 | TOUCHES | 7 | ||
| Table | 15 | BELONGS_TO | 41 | WRITES | 6 | ||
| File | 7 | ENFORCED_IN | 40 | CONTAINS | 5 | ||
| Object | 5 | BRANCHES_TO | 40 | READS | 4 | ||
| RuleSet | 4 | IMPLEMENTED_AT | 39 | HAS_STATE | 3 | ||
| BlindSpot | 4 | READS_COLUMN | 36 | COVERS | 2 | ||
| State | 3 | CONSTRAINS_TABLE | 1 | ||||
| Sequence | 3 | LOOPS_BACK_TO | 1 | ||||
| Index | 1 |
Three modelling decisions
Columnis a node, not a property ofTable. Code reads, writes, constrains and indexes a column: four independent relationships. A column property cannot answer an impact question.Statementis a node. This gives a Code Property Graph layer (Yamaguchi et al., IEEE S&P 2014). The statement tree of Agent 2 is an AST. Itscontrol_flow_graphis a CFG. The slices of Agent 4 are dependence facts. Together, they answer questions that no single source can answer.BlindSpotis a node. A graph that looks authoritative is more dangerous than a document that looks uncertain.
The module docstring states:
"The obvious way to turn English into Cypher is to ask a language model. This pipeline does not… a fabricated Cypher query returns a plausible, wrong answer, and the user has no way to tell."
Agent 8 uses 12 named intents in place of a model. Each intent has trigger patterns, a resolver and the equivalent Cypher. Agent 8 refuses a question with no match and shows the supported list.
The diagram shows ask() in lib_graph_language.py:
flowchart TD
Q[/"Question in plain English"/] --> E{"Empty?"}
E -- "yes" --> RF1[/"Refusal: empty question,<br/>suggestions"/]
E -- "no" --> RE["resolve_entity(): BR-nnn, then TABLE.COLUMN,<br/>then an exact name or title,<br/>then a bare column name"]
RE --> MT["Intents whose trigger patterns match,<br/>the entity type that was found first"]
MT --> NE{"Intent needs an entity<br/>of a different type?"}
NE -- "yes" --> NEXT["Try the next intent"]
NEXT --> NE
NE -- "no" --> ANS{"Resolver gives<br/>an answer?"}
ANS -- "yes" --> OK[/"Rows, subject and<br/>the equivalent Cypher"/]
ANS -- "no" --> NEXT
MT -- "match, but no entity found" --> RF2[/"Refusal: name it exactly,<br/>for example ACCOUNTS.BALANCE"/]
MT -- "no match" --> RF3[/"Refusal: no supported question,<br/>suggestions"/]
Total precision, imperfect recall. This is the correct trade, because a wrong impact answer is worse than no answer. The negative tests are the most important tests in the suite.
A review of the output added an invariant: each advertised question, asked word for word, must have a match. The cause: the list showed "Which columns are used most widely?", but Agent 8 refused it. A pattern expected the word order "most widely used".
| # | Question | Intent |
|---|---|---|
| 1 | What breaks if I change a given column? | impact_of_column |
| 2 | Which business rules apply to a program unit, table or column? | rules_of |
| 3 | Where does a given rule live in the source? | where_is_rule |
| 4 | Which program units read or write a given table? | who_touches_table |
| 5 | What is the calling interface of a program unit? | interface_of |
| 6 | Which rules still need a person to confirm them? | needs_review |
| 7 | Which rules are recorded but not enforced by the database? | unenforced |
| 8 | Which program units are most complex? | most_complex |
| 9 | Which columns are used most widely? | hot_columns |
| 10 | Which tables are never touched by any program unit? | orphan_tables |
| 11 | What open questions remain for the business? | open_gaps |
| 12 | What can this graph NOT see? | blind_spots |
The diagram shows the graph path that the equivalent Cypher of each intent reads.
flowchart LR
I1["impact_of_column"] --> COL(["Column"])
I9["hot_columns"] --> COL
COL -- "READS_COLUMN, WRITES_COLUMN" --- OBJ(["Object"])
COL -- "CONSTRAINS" --- BR(["BusinessRule"])
COL -- "COVERS" --- IDX(["Index"])
I2["rules_of"] --> BR
I3["where_is_rule"] --> BR
I6["needs_review"] --> BR
I7["unenforced"] --> BR
BR -- "ENFORCED_IN" --- OBJ
BR -- "IMPLEMENTED_AT" --- STM(["Statement"])
I4["who_touches_table"] --> TAB(["Table"])
I10["orphan_tables"] --> TAB
TAB -- "READS, WRITES" --- OBJ
I5["interface_of"] --> OBJ
I8["most_complex"] --> OBJ
OBJ -- "HAS_PARAMETER" --- PAR(["Parameter"])
I11["open_gaps"] --> GAP(["Gap"])
I12["blind_spots"] --> BSP(["BlindSpot"])
Give the exact entity names: ACCOUNTS.BALANCE, SP_TRANSFER_FUNDS, BR-014.
Tests. 68 checks (before: 13).
This is the complete set of numeric thresholds in the repository. A grep confirmed that the list is complete.
| Constant | Value | Location | Purpose |
|---|---|---|---|
CYCLOMATIC_THRESHOLD |
10 | 04_logic.py:71 |
Complexity warning |
COGNITIVE_THRESHOLD |
15 | 04_logic.py:73 |
Cognitive complexity warning |
_DERIVATION_COMPLEXITY_THRESHOLD |
2 | 05_rules.py:603 |
Business formula or mechanics |
DEFAULT_NODE_BUDGET |
40 | 06_diagram.py:99 |
Diagram readability budget |
LINE_TOLERANCE |
2 | tests/evaluate_rules.py:45 |
Match window for the ground truth |
The diagram shows where each threshold and formula acts in the pipeline.
flowchart LR
A1["Agent 1<br/>file_id = SLUG + SHA256 of the path"] --> A4["Agent 4<br/>M = D + 1, warning above 10<br/>cognitive, warning above 15"]
A4 --> A5["Agent 5<br/>derivation score 2 or more<br/>clause score for rule names"]
A5 --> A6["Agent 6<br/>DEFAULT_NODE_BUDGET 40<br/>tier1_pct, traceability"]
A6 --> A7["Agent 7<br/>SBVR modality:<br/>alethic or deontic"]
A5 --> EV["tests/evaluate_rules.py<br/>LINE_TOLERANCE 2<br/>P, R, F1"]
A6 --> OV{"Over the node budget<br/>after collapse?"}
OV -- "yes" --> GAP[/"OVERSIZE in the gaps register"/]
How the clause score names a rule (describe_comparison() in 05_rules.py):
flowchart TD
C[/"Condition text"/] --> F{"Comparison clauses found?"}
F -- "no" --> FB["Subject from guess_subject(),<br/>no qualifier"]
F -- "yes" --> S["Score each clause: +2 for = with a literal,<br/>+1 for a known field on the left"]
S --> MX["Keep the clause with the highest score"]
MX --> INV{"Obligation from a guard?"}
INV -- "yes" --> NEG["Invert the operator"]
INV -- "no" --> PH["Operator phrase"]
NEG --> PH
PH --> CMP{"AND or OR in the condition,<br/>and the operator is not =?"}
CMP -- "yes" --> NQ["Subject only, no qualifier"]
CMP -- "no" --> LEN{"Qualifier longer than<br/>40 characters?"}
LEN -- "yes" --> NQ
LEN -- "no" --> NM[/"Verb + subject + qualifier,<br/>for example Enforce Balance at or above Amount"/]
Stable file identity (Agent 1)
Cyclomatic complexity (Agent 4)
Derivation complexity (Agent 5)
Clause score for rule names (Agent 5)
SBVR modality (Agent 7)
Evaluation (harness of Agent 5)
Diagram quality (Agent 6)
Purpose. Add business knowledge that static analysis cannot find.
Static analysis can prove that a 365-day threshold exists. It can never find that a regulation sets the threshold. This is the concept assignment problem (Biggerstaff et al., 1993). The annotation layer exists for this problem.
flowchart TD
P{{"A person writes<br/>brd_annotations.json"}} --> F[/"--annotations path,<br/>default brd_annotations.json"/]
F --> EX{"File exists and<br/>is valid JSON?"}
EX -- "no" --> EMP["Empty dict"]
EX -- "yes" --> AN["The annotations block,<br/>or the full object"]
AN --> K{"Stable ID key"}
K -- "BR-nnn" --> R["Rule block: note,<br/>Owner, Priority"]
K -- "table:NAME" --> T["Data model table note"]
K -- "term:TABLE.COLUMN" --> G["Glossary term note"]
K -- "object:ID" --> O["Program unit note"]
K -- "param:ID:NAME" --> PA["Parameter note"]
K -- "executive_summary" --> E["Executive summary note"]
EMP --> PH["Visible placeholders:<br/>to be assigned, to be supplied"]
R --> BRD[/"brd.md"/]
T --> BRD
G --> BRD
O --> BRD
PA --> BRD
E --> BRD
PH --> BRD
R --> IDX[/"brd_index.json:<br/>owner, priority"/]
classDef human fill:#fff3cd,stroke:#b8901f,color:#3d2f00,font-weight:bold
class P human
Procedure
- Make a file with the name
brd_annotations.json(the default of--annotations). - Add one entry for each stable ID that you want to annotate.
- Run
07_synthesis.pyagain.
{
"annotations": {
"BR-001": {
"note": "365-day threshold is set by regulation, not policy.",
"owner": "Head of Retail Operations",
"priority": "Must have"
},
"table:ACCOUNTS": { "note": "Master record for every customer account." },
"term:ACCOUNTS.BALANCE":{ "note": "Cleared balance, excluding pending items." },
"object:PROC-.SP_TRANSFER_FUNDS": { "note": "Used by the mobile channel." },
"executive_summary": { "note": "Core retail ledger, in service since 2004." }
}
}Rules
- The keys are stable IDs. Agent 7 merges the notes at synthesis time.
- The pipeline never writes this file. The machine facts change in each run. Your notes stay.
- If the file is not valid, Agent 7 uses an empty dict and shows placeholders.
Warning
Use only trusted text in brd_annotations.json. Agent 7 puts the annotation text into the BRD word for word, with no sanitisation.
flowchart LR
subgraph IN["Inputs, read only"]
SQL[/"src/*.sql"/]
ANN[/"brd_annotations.json"/]
end
subgraph WS["One workstation"]
S["8 pipeline scripts<br/>no environment variables, no network,<br/>no subprocess, os.system, eval or exec,<br/>no model"]
end
subgraph OUT["output/ on the local file system"]
ART[("Versioned artifacts<br/>with upstream provenance")]
BRD[("brd.md, import.cypher,<br/>CSV files")]
end
SQL --> S
ANN --> S
S --> ART
S --> BRD
BRD -. "a person copies or imports" .-> EXT["Readers or Neo4j"]
| Control | Status |
|---|---|
| Secrets, credentials, tokens | None in the pipeline. No environment variables, confirmed by grep |
| Network access | None. No pipeline script imports a network library |
| Command execution | None. No pipeline script uses subprocess, os.system, eval or exec |
| Model and prompt-injection surface | None. There is no model |
| Auditability | Run versions, and upstream provenance in each artifact |
The diagram shows where each risk enters the flow.
flowchart LR
DEP[/"antlr4-python3-runtime, sqlglot"/] -- "Unpinned dependencies:<br/>no manifest, no lockfile" --> S["Pipeline scripts"]
ANN[/"brd_annotations.json"/] -- "Annotation injection:<br/>inserted word for word" --> S
SQL[/"src/*.sql"/] --> S
S --> ART[("Artifacts<br/>abs_path leakage")]
S --> BRD[("brd.md and graph export<br/>sensitive output, no redaction")]
ART -. "No output schema validation,<br/>only the tests check the shape" .-> NEXT["Next agent"]
| Risk | Detail |
|---|---|
| Unpinned dependencies | Two third-party libraries, no manifest, no lockfile. This is the largest supply-chain exposure |
| Annotation injection | Agent 7 inserts the content of brd_annotations.json word for word, with no sanitisation |
| Sensitive output | brd.md and the graph export contain the complete business logic, schema, interfaces and error contracts. There is no classification or redaction mechanism |
abs_path leakage |
The artifacts contain local file system paths. The committed output/ files contain the absolute paths of the author's workstation |
| No validation of the output schema | Only the tests enforce the shape of the artifacts |
The repository has no threat model.
flowchart TD
subgraph M["One workstation"]
PY["Python 3.11+"]
DEPS["antlr4-python3-runtime + sqlglot<br/><i>installed manually, no manifest</i>"]
SCR["8 scripts + 3 libraries"]
FS[("local file system")]
end
OPT["Neo4j<br/><i>optional, separate, manual import</i>"]
PY --> SCR
DEPS --> SCR
SCR <--> FS
FS -.->|a person runs cypher-shell| OPT
classDef opt stroke-dasharray: 4 2
class OPT opt
STATUTE runs as short sequential CLI processes on one machine. There is no Dockerfile, no CI/CD, no health check, no service and no separation of environments. The local file system is the only persistence.
docs/architecture-decisions.md has eleven ADRs in the Nygard format:
| # | Decision | Key consequence |
|---|---|---|
| 001 | No language model in the generation path | The output is a pure function of the input |
| 002 | A pipeline through the file system, with no orchestrator | You can run each agent again alone. A person controls the sequence |
| 003 | Stable identifiers from the path | A file keeps its identity after an edit. A move changes it |
| 004 | Versioned runs, pointer after the write | A stop cannot corrupt downstream. The disk use grows with no limit |
| 005 | Formal grammar, not regex | Correct nesting. The generated code needs a patch |
| 006 | Two-axis constraint enforcement | The BRD never states a guarantee that the database does not give |
| 007 | Diagram model separate from the renderer | The budget is enforceable. Tests assert the meaning |
| 008 | Stop at documentation | The scope agrees with the demonstrated capability |
| 009 | Deterministic intent catalogue, not generated Cypher | Total precision, imperfect recall |
| 010 | Annotations in a read-only sidecar file | Human knowledge stays after each regeneration |
| 011 | One collapse tier | A diagram never shows a sequence that does not exist |
These sources directly influenced the implementation (the code or the specifications name them):
| Area | Sources |
|---|---|
| Metrics and program analysis | McCabe (1976) cyclomatic complexity · Campbell cognitive complexity · Weiser (1981) program slicing · Yamaguchi, Golde, Arp and Rieck (2014) Code Property Graphs, IEEE S&P · Lehnert, A review of software change impact analysis |
| Requirements and documentation | ISO/IEC/IEEE 29148:2018 · OMG SBVR 1.5 · IIBA BABOK v3 · Mavin et al. (EARS) · Chikofsky and Cross (1990) · Biggerstaff, Mitbander and Webster (1993) · Aghajani et al. (ICSE 2020) · Lethbridge, Singer and Forward (2003) · Cosentino et al. (WCRE 2013) |
| Visualisation | Moody (2009) The "Physics" of Notations · Shneiderman (1996) The Eyes Have It · Shneiderman et al. (1977) CACM 20(6) · Purchase (1997/2002) · VEIL (arXiv 2511.05066) |
| Tool precedent | jQAssistant · Neo4j property-graph modelling guidance · CAST Imaging / Thoughtworks CodeConcise |
| Evaluation baselines (comparison only) | COBREX F1 0.59 · COBRAIN 0.73 · A-COBREX P 0.62 / R 0.74 |
| Industrial motivation | Sneed, From COBOL to Business Rules |
These sources gave only the structure of the documentation:
- arc42: the structure of the architecture documentation
- C4 model: the selection of diagram levels
- Michael Nygard, Documenting Architecture Decisions: the ADR format
- GitHub collapsed sections: the
<details>technique of the earlier README
docs/references.md gives the full list with four classes.
Software licences. Vendored ANTLR4 Oracle PL/SQL grammar: Apache 2.0 (antlr/grammars-v4, see .claude/scripts/vendor/plsql_grammar/NOTICE.md) · antlr4-python3-runtime: BSD · sqlglot: MIT · this project: MIT.
Each agent writes a new timestamped run directory and then updates its latest.json. The output/ folder is committed. It holds the latest run of each agent.
| Path | Committed? | Contents |
|---|---|---|
output/inventory/<run>/inventory-artifact.json |
Yes | File index, file metadata, summary |
output/parser/<run>/parser_artifact.json |
Yes | Objects, statements, CFG, issues[] |
output/parser/<run>/raw_structure/*.json |
Yes | The statement tree of each object |
output/data/<run>/data_artifact.json, erd.mmd |
Yes | Schema, enforcement state, column catalogue, ERD |
output/logic/<run>/logic_artifact.json |
Yes | Pseudocode, complexity, slices, hazards, shapes |
output/logic/<run>/program_logic/*_logic.json |
Yes | The logic record of each object |
output/rules/<run>/rules_artifact.json |
Yes | rule_sets, business_rules, error_handling_catalogue |
output/diagram/<run>/diagrams_artifact.json, diagrams/*.mmd |
Yes | Diagram index and Mermaid files |
output/final_report/<run>/brd.md |
Yes | The Business Requirements Document |
output/final_report/<run>/brd_index.json, gaps_register.json |
Yes | Machine-readable index and gaps register |
output/graph/<run>/import.cypher, nodes/*.csv, rels/*.csv |
Yes | The graph export (MERGE) |
output/graph/<run>/README.md |
Yes | Load guide, query cookbook, concepts, constraints, blind spots |
output/<stage>/latest.json |
Yes | run_version, path and updated_at of the current run |
output/<stage>/<run>/run_meta.json |
Yes | Metadata of the run |
brd_annotations.json |
No (not in the repository) | Optional business notes. Agent 7 only reads it |
reference/ |
No (git ignores it) | COBOL reference harness, design guidance only |
| Path | Contents |
|---|---|
src/00_ddl_create_schema.sql |
The DDL of the reference schema (15 tables) |
src/00_dml_load_sample_schema.sql |
Sample data |
src/01_simple_calculate_simple_interest.sql |
FN_CALCULATE_SIMPLE_INTEREST |
src/02_simple_update_dormant_account_status.sql |
SP_UPDATE_DORMANT_ACCOUNT_STATUS |
src/03_simple_check_minimum_balance.sql |
SP_CHECK_MINIMUM_BALANCE |
src/04_medium_process_monthly_interest_credit.sql |
SP_PROCESS_MONTHLY_INTEREST_CREDIT |
src/05_medium_fund_transfer_with_validation.sql |
SP_TRANSFER_FUNDS |
tests/fixtures/ground_truth/*.json |
Hand annotations of 4 procedures and BASELINE.json |
tests/fixtures/expected-inventory-artifact.json |
Golden fixture of Agent 1 |
tests/fixtures/sample_plsql/, sample_ddl/, sample_logic/ |
Inputs for the test suites |
The docs/ folder has 15 technical documents (5,945 lines) and the STE style guide. They give more detail for each topic.
| Document | Purpose |
|---|---|
| System overview | Orientation in 5 minutes |
| Complete system technical documentation | Master document, 36 sections |
| Agent documents | One document for each agent, 28 sections each |
| Architecture decisions | 11 ADRs |
| Traceability matrix | Each claim → repository evidence |
| Known gaps and open questions | Verified gaps and questions for stakeholders |
| References | References in four classes |
| Documentation review report | Self-review against 19 criteria |
| STE style guide | Writing rules and project vocabulary of this README |
| Need | For |
|---|---|
| Python 3.11+ | All agents |
antlr4-python3-runtime 4.13.2 |
Agents 2 and 3. The version must agree with the ANTLR tool version of the vendored parser (see NOTICE.md) |
sqlglot |
Agent 2. No version is pinned |
Neo4j and cypher-shell |
Optional. Only to load the graph export |
Java and tools_antlr_build/antlr4-complete.jar |
Optional. Only to regenerate the grammar |
Important
The repository has no dependency manifest. There is no requirements.txt, pyproject.toml or lockfile, and no version is pinned. This is the largest operational risk (see 21).
git clone https://github.com/KrishnaAnnavaram/statute.git
cd statute
pip install antlr4-python3-runtime==4.13.2 sqlglotRun all eight agents in this sequence:
python .claude/scripts/01_inventory.py src
python .claude/scripts/02_parser.py
python .claude/scripts/03_data.py
python .claude/scripts/04_logic.py
python .claude/scripts/05_rules.py
python .claude/scripts/06_diagram.py
python .claude/scripts/07_synthesis.py
python .claude/scripts/08_graph.py # optionalEach agent prints the location of its output and the exact next command.
Find your output:
cat output/final_report/latest.json # names the run directory that holds brd.mdWarning
Always open the run that latest.json names. Each run makes a new timestamped directory, and nothing removes old directories. An old directory has old content. This mistake occurred before.
You do not need Neo4j for these commands:
python .claude/scripts/08_graph.py --list-questions
python .claude/scripts/08_graph.py --ask "what breaks if I change ACCOUNTS.BALANCE"
python .claude/scripts/08_graph.py --ask "..." --json # for scriptsExample answer (abbreviated):
Question type : What breaks if I change a given column?
Subject : ACCOUNTS.BALANCE — NUMBER(18,2) — NOT NULL
Results : 10
BR-014 Restrict Account Status to allowed values constrains this table business rule
Check Minimum Balance — line 21 reads SELECT_INTO
Process Monthly Interest Credit — line 58 writes UPDATE
Transfer Funds — line 95 writes UPDATE
Transfer Funds — line 109 writes UPDATE
...
Equivalent Cypher:
MATCH (c:Column {column_id: $column}) ...
To load the graph into Neo4j:
cd output/graph/<run-from-latest.json>/
cat import.cypher | cypher-shell -u neo4j -p <password>The export uses MERGE for all items, so you can import it again safely. The generated README.md in that directory has the query cookbook, the derived-view concepts, the validation constraints and the declared blind spots.
for t in tests/test_*.py; do python "$t"; done # 414 checks
python tests/evaluate_rules.py # rule extraction against the ground truthEach suite is a standalone script with a shared check(condition, label) convention. There is no pytest or test-runner configuration.
Each suite runs the real pipeline into a temporary directory. Thus, the suites are integration tests with unit-style assertions.
STATUTE uses no environment variables and no configuration files. CLI arguments are the only configuration.
| Pattern | Purpose |
|---|---|
--<stage>-root, --<stage>-run |
Find the upstream artifacts (latest or a pinned run) |
--run |
Agents 7 and 8: one run version for all upstream artifacts. Default latest |
--output-root, --output |
Where to write. --output stops the run versions |
--verbose |
Status of each item on stderr (Agents 1–4) |
--max-nodes |
Diagram node budget, default 40. This is the only numeric setting |
--system-name, --annotations |
Agent 7. Defaults PL/SQL Banking System and brd_annotations.json |
--ask, --list-questions, --json |
Agent 8 |
All other thresholds are hard-coded module constants. To change one, edit the code.
Do this only if the grammar must change.
-
Get the ANTLR 4.13.2 tool jar.
-
Run the regeneration command from
.claude/scripts/vendor/plsql_grammar/NOTICE.md:java -jar antlr4-4.13.2-complete.jar -Dlanguage=Python3 -visitor -no-listener \ PlSqlLexer.g4 PlSqlParser.g4 -
Patch the generated Python. Replace
this.withself.. -
Run all test suites.
tools_antlr_build/ has a copy of the ANTLR jar, the grammar files and explore_ddl.py, a script that prints the parse tree of a DDL file.
Logs. The agents use only print(). They do not use logging. There are no levels, no structured fields, no metrics export and no alerts. The run_version is the correlation identifier across agents.
| Symptom | Where to look |
|---|---|
| A table or an object is absent downstream | Examine file_role first. Routing is the most frequent cause, and it caused a real defect |
| Parse problems | parser_artifact.json → issues[], the best diagnostic data |
| Agent 6 exits with a non-zero code | It fails intentionally on each validation problem. The list is on stderr |
| The BRD shows old content | You opened an old run. Use output/final_report/latest.json |
| A rule looks incorrect | Each rule cites a file and a line. Open the source |
--ask refuses a question |
Give the exact entity name. Run --list-questions |
| All results are unresolved after a new run | A stopped agent leaves latest.json on the previous run. Compare the timestamps |
| Agent 2 cannot find a source file on a new clone | The committed inventory has the absolute paths of another machine. Run Agent 1 first |
The five changes with the largest effect on other agents:
| Change | Effect on |
|---|---|
The file_id scheme (Agent 1) |
Each downstream ID, the golden fixture, a full new run |
The statement_id format (Agent 2) |
Agents 5, 6, 7 and 8. It is the join key of the pipeline |
| The statement type strings (Agent 2) | Agents 4, 6 and 8 all switch on them |
| A new kind of rule source (Agent 5) | The provenance labels of Agent 7 and the origin of Agent 8 |
| The enforcement confidence strings (Agent 3) | Agent 5 uses the string as a key |
To add a ninth agent:
- Use the same pattern:
main(),argparse,load_run, a versioned write andlatest.json. - Read the upstream artifacts from
latest.json. Degrade gracefully when an optional input is absent. - Write the standard envelope:
pipeline_stage,schema_version,generated_at,upstream,stats. - Add
tests/test_<stage>.pywith the sharedcheck(condition, label)convention. - Add
.claude/agents/<n>_<name>_agent.mdanddocs/agents/agent-0<n>-<name>.md.
To add ground truth for Agent 5: add a file to tests/fixtures/ground_truth/. Annotate it blind first. Then measure. Then correct the extractor.
| Validation | Result | Command |
|---|---|---|
Test suites (run on 2026-10-08 with Python 3.11.9, antlr4-python3-runtime 4.13.2 and sqlglot 30.18.0) |
414 checks passed, 0 failed | for t in tests/test_*.py; do python "$t"; done |
| Rule extraction, current extractor (run on 2026-10-08) | Precision 1.000 (34/34), recall 1.000 (34/34), F1 1.000 | python tests/evaluate_rules.py |
| Rule extraction, baseline before the redesign | Precision 0.615, recall 0.571, F1 0.593 (8 matched, 13 extracted, 14 in the ground truth) | tests/fixtures/ground_truth/BASELINE.json |
| Rule extraction, blind procedures | First: F1 0.588. Second: precision 1.000, recall 0.400, F1 0.571 | Recorded in docs/agents/agent-05-rules.md and in 05_rules.py (recall 0.400) |
| Committed pipeline run (2026-07-29) | See the table below | output/*/latest.json |
| Suite | Checks | Focus |
|---|---|---|
test_synthesis |
86 | Readability, navigation, completeness, traceability, honesty |
test_data |
76 | Enforcement state, type mapping, cross-validation |
test_graph |
68 | Schema, coverage, loadability, refusal to guess |
test_diagram |
52 | Collapse invariants, label coverage, budget declaration |
test_logic |
46 | Complexity, slices, transaction hazards |
test_rules |
33 | Obligation form, branch decomposition, deduplication |
test_parser |
31 | Grammar, CFG edges, sqlglot enrichment |
test_inventory |
22 | Routing, stable IDs, golden diff |
| Agent | Result of the committed run |
|---|---|
| 1 Inventory | 7 files, 940 lines, 603 lines of code. Roles: 1 schema_ddl, 1 seed_data, 1 function, 4 procedure |
| 2 Parser | 5 objects, 124 statements, 0 parse errors, 0 unresolved calls, 0 dynamic SQL blocks |
| 3 Data | 15 tables, 105 columns, 7 declared and 1 inferred foreign keys, 1 CHECK, 1 index, 3 sequences, 6 enums in comments only |
| 4 Logic | 151 pseudocode lines, 25 slices, 1 object above the cyclomatic threshold, 0 above the cognitive threshold, 1 SAVEPOINT hazard |
| 5 Rules | 41 rules (19 validation, 12 limit check, 6 calculation, 4 error handling). Confidence: 18 confirmed, 16 high, 4 medium, 3 low |
| 6 Diagram | 7 diagrams: 1 data flow, 1 state model, 5 process flows |
| 7 Synthesis | 41 requirements, 21 glossary terms, 41 traceability rows, 21 gaps. brd.md has 2,549 lines |
| 8 Graph | 353 nodes, 769 relationships, 13 labels, 22 relationship types, 4 blind spots |
Only Agent 5 has an evaluation harness. Seven of the eight agents have no measured accuracy. The tests prove that the pipeline is connected and that the invariants hold. They do not prove the accuracy of the output on other codebases.
Read these problems before you use a BRD from STATUTE or extend the pipeline.
| # | Area | Problem | Impact and action |
|---|---|---|---|
| 1 | Rule accuracy | The rule extraction F1 of 1.000 is tuned, not blind. The ground truth covers 4 of 5 procedures, and the team used each to correct the extractor | Use the blind figures: F1 0.588 and recall 0.400. Have an expert review each rule |
| 2 | Metrics | Coverage metrics prove completeness, not usefulness. No automated documentation metric correlates well with expert judgement | Get expert review of the BRD |
| 3 | Concepts | Code does not contain concepts. v_days_inactive > 365 becomes a lexical transformation. "Dormancy policy" is not in the source |
Add the concepts with the annotation layer |
| 4 | Graph | The graph gives a lower bound on dependencies, never an upper bound. It declares four classes of blind spot | Ask What can this graph NOT see? before you trust an impact answer |
| 5 | Corpus | The corpus has one domain: 5 objects, 15 tables, one banking schema. Nobody has run STATUTE on other PL/SQL. The category keywords of Agent 5 are banking words | Run STATUTE on PL/SQL from a different domain and measure again |
| 6 | Dependencies | No dependency manifest and no pinned versions. The ANTLR runtime must be 4.13.2 | Add requirements.txt or pyproject.toml with pinned versions |
| 7 | CI | No CI runs the tests. The only workflow file, src/.github/workflows/openwiki-update.yml, is inside src/. GitHub Actions does not run it there, and it does not run tests |
Add a CI workflow at the repository root that runs the 414 checks |
| 8 | Operations | No container and no logging framework. The agents use only print() |
Add them before a shared deployment |
| 9 | Graph | No state transition edges (TRANSITIONS_TO) are in the graph, although Agent 6 calculates transitions. The loop in lib_graph_model.py contains only pass |
Add the edges, or delete the incorrect comment |
| 10 | Duplicate code | Agent 2 stores no condition text. Agents 4, 5 and 6 each cut the condition from the source again | Store the condition text in Agent 2 |
| 11 | Requirement type | The BRD gives Solution and Functional requirements (BABOK). It describes what the system does, not why the business wanted it. Its scope chapter states this | Get the business reasons from stakeholders |
| 12 | Flowcharts | Detailed flowcharts have weak empirical support (Shneiderman et al., 1977). The counter-argument (the reader cannot read PL/SQL) is an argument, not evidence | Use the rule catalogue as the primary source |
| 13 | Annotations | Agent 7 inserts annotation text word for word, with no sanitisation (Markdown injection) | Use only trusted annotation files. Add sanitisation |
| 14 | Sensitive output | The BRD and the graph export contain the complete business logic. There is no classification or redaction | Control the distribution of the output |
| 15 | Committed paths | The committed output/ artifacts contain the absolute paths of the author's workstation (abs_path). Agent 2 reads source files from these paths |
Run Agent 1 first on a new clone. Remove abs_path from shareable artifacts |
| 16 | Output schema | No JSON Schema exists for any artifact. Only the tests enforce the shape | Publish a JSON Schema for each artifact |
| 17 | Disk use | Nothing removes old run directories. The output/ folder is committed, so each committed run makes the repository larger |
Add a removal step for old run directories |
| 18 | Graph edges | g.rel() drops an edge to an absent node with no message and no count |
Report a count of dropped edges |
| 19 | Identity | A file_id comes from the path. A file that moves gets a new identity, and its annotations and links break |
Do not move source files between runs, or map the old IDs |
| 20 | File roles | No labelled corpus measures the accuracy of file-role classification. A file with the incorrect role is dropped with no message | Add a labelled corpus for classification |
| 21 | Grammar | The generated ANTLR Python uses this.. You must patch it after each regeneration |
Follow 18.7 |
| 22 | Agent 1 scope | .claude/skills/file-catalog and reference-graph describe an object_registry and a dependency_graph. Agent 1 does not make them. Agent 2 does its own object discovery |
Treat the skills as targets, not as the current behaviour |
| 23 | OpenWiki pages | A language model generated the pages in src/openwiki/ (.last-update.json names the model). They are not pipeline output and the no-LLM guarantee does not apply to them |
Use the BRD as the reference. Treat the OpenWiki pages as notes |
| 24 | Security | The repository has no threat model | Write a threat model before a shared deployment |
- Why does Agent 2 not store the condition text, so that three agents must implement it again?
- Why are there no
TRANSITIONS_TOedges when Agent 6 calculates transitions? - Which Python and dependency versions are supported?
- Are the complexity thresholds (10 and 15) organisational standards or defaults?
- Why is PySpark the assumed rebuild target?
- Which retention policy applies to run directories?
- Who owns
brd_annotations.jsonin operation? - Must the BRD have a classification label?
- What is the largest codebase that STATUTE has analysed?
- Which recall of intent matches is acceptable for Agent 8?
docs/known-gaps-and-open-questions.md gives the full detail.
- No language model is in the generation path. A grammar, sqlglot and rules make all output. The same source always gives the same document.
- Eight agents, one direction. Inventory → Parser → Data → Logic → Rules → Diagram → Synthesis → Graph. A person runs each agent.
- The file system holds all state. Each agent writes a versioned run, then moves
latest.json. A stop cannot corrupt downstream. - Each object has a stable identity.
statement_idis the join key. Each rule traces to a file and a line. - The database enforcement state is data. A
DISABLEDconstraint stays in the BRD, with a low score and a warning. - Agent 5 is the core, and its honest accuracy is the blind figure: F1 0.588 and recall 0.400. The tuned 1.000 is contaminated.
- Diagrams never show a false flow. If a diagram cannot meet its budget, Agent 6 declares it oversize.
- The BRD has four parts for four audiences. Each rule has three forms and an SBVR modality.
- The graph refuses a question that it cannot answer. It declares its own blind spots.
- People add business knowledge in a read-only sidecar file. The pipeline never writes it.
| Term | Meaning |
|---|---|
| Agent | One stage of the pipeline: an independent Python CLI program in .claude/scripts/. Not an autonomous LLM agent |
| Alethic | The SBVR modality of a rule that the database enforces. A violation is impossible |
| Annotation | A business note in brd_annotations.json, keyed by a stable ID. Agent 7 merges it into the BRD |
| Artifact | A JSON file that an agent writes into its run directory, for example parser_artifact.json |
| Backward slice | For one variable, each statement that sets its value, with the control ancestors |
| Blind spot | A class of dependency that static analysis cannot see. The graph declares it as a BlindSpot node |
| BRD | Business Requirements Document: brd.md, the main output of Agent 7 |
| CFG | Control flow graph of one object, from Agent 2 |
| Cognitive complexity | A nesting-weighted count of breaks in the flow (Campbell). Threshold 15 |
| Cyclomatic complexity | M = D + 1, where D is the number of decisions (McCabe). Threshold 10 |
| Deontic | The SBVR modality of a rule that only the code enforces. A violation is possible |
| Enforcement state | The STATUS and VALIDATED values of a constraint: enforced, enforced_new_data_only or not_enforced |
| Gap | An open matter that the pipeline cannot resolve, with an ID GAP-nnn. The gaps register lists all gaps |
| Ground truth | Hand-annotated rules of 4 procedures in tests/fixtures/ground_truth/ |
| Intent | One of the 12 question types of Agent 8, with trigger patterns, a resolver and equivalent Cypher |
| Knowledge graph | The property graph of Agent 8: 13 node labels and 22 relationship types |
| Node budget | The maximum number of nodes in one diagram. Default 40 |
| Obligation | A rule stated as the condition that must be true. It comes from a guarded RAISE |
| Oversize | The status of a diagram that is above its node budget after collapse |
| Pointer | The latest.json file of an agent. It names the last good run |
| Rule | A business rule with an ID BR-nnn, a source line and a confidence |
| Run | One timestamped output directory of one agent |
| SBVR | OMG Semantics of Business Vocabulary and Business Rules. The BRD uses its modalities |
statement_id |
The stable ID of one statement: file_id__object_id__STMT_nnnn. The join key of the pipeline |
| Traceability matrix | The BRD table that links each rule to its file and line |
| Transaction hazard | A risky transaction pattern, for example SAVEPOINT_PARTIAL_ROLLBACK |
MIT © 2026 KrishnaAnnavaram