Delphi-RAG-Lint

drag-lint help

Circular Dependency Report (worked example)

Since: 0.0.0

A worked example: a real four-unit uses cycle, and the report drag-lint produces for it.

Details

A worked example: a real four-unit uses cycle, and the report drag-lint produces for it.

**This page's report excerpt is generated by drag-lint, not written by an AI.** Every line quoted under "The report (excerpt)" below is literal stdout of one command run against an indexed project (a line that is only ... marks an elision). Every file name, line number and symbol name in it was read out of the index -- which was itself built by parsing the source with tree-sitter -- not inferred from prose. The full, unabridged report is circular-demo/CYCLE-REPORT.md. tests\autotest\run_cycle_examples_fresh.ps1 re-runs the command against the current engine and fails the test battery if either document drifts from it.

The sample project

circular-demo/ (repo root, alongside README.md) is a small, deliberately minimal Delphi console project: 10 units, ~135 symbols, holding three dependency cycles of three different shapes that all still compile. This page is about the first and largest, a four-unit cycle in which one of the four edges is an implementation uses rather than an interface uses:

DemoConfig  -> DemoLogger    interface
DemoLogger  -> DemoSession   interface
DemoSession -> DemoAudit     interface
DemoAudit   -> DemoConfig    IMPLEMENTATION  <-- why this compiles at all

A cycle closed entirely through interface uses would not compile -- Delphi's compiler forbids it. This project is shaped so that exactly one edge is an implementation uses, which is legal, and that is the edge that lets the other three close the loop.

The project also declares seven module-level globals shared across the four units, each written by one unit and read by another:

GlobalDeclared inWritten byRead by
GDemoSettingsDemoConfigDemoConfigDemoAudit
GDemoMinLevelDemoLoggerDemoConfigDemoLogger, DemoConfig
GDemoLogCountDemoLoggerDemoLoggerDemoConfig
GDemoSessionCountDemoSessionDemoSessionDemoLogger
GDemoActiveSessionDemoSessionDemoSessionDemoLogger
GDemoAuditCountDemoAuditDemoAuditDemoSession
GDemoAuditTrailDemoAuditDemoAuditDemoSession

Shared mutable globals are what make a cycle like this expensive in practice: they create ordering dependencies between units that the compiler will not complain about, so the cycle is not merely a tidiness problem -- it is a real coupling the reader has to hold in their head.

The other two cycles are implementation-only and modelled on real client code: a three-unit one (`DemoBlueprint -> DemoBlueprint.ViewModel -> DemoControlPlan -> DemoBlueprint`) whose only cheap cut is a single routine call, and a two-unit one (DemoUIConnect <-> DemoGagePort) whose only cheap cut is a member access behind is / as. Nothing on either is a plain constant or variable, so the playbook's "move the globals into a leaf" recipe does not apply; the report says so per edge, and refuses them. What a wider recipe would do with them is written up in circular-demo/CYCLE-REPORT.md.

Reproduce it yourself

drag-lint index  --project circular-demo\CircularDemo.dproj --db circular-demo\_D-RAG\CircularDemo.sqlite
drag-lint cycles --db circular-demo\_D-RAG\CircularDemo.sqlite --edges --causes --plan --format text

Indexing this project takes well under a second (11 files, 134 symbols, 451 refs) -- small enough to run on a whim to check the claims on this page yourself.

Can a model follow it?

That is the question the report is built to answer, and it has been measured. Haiku, at its lowest effort, was given a fresh copy of this project and the report, with the prompt *"This report describes a circular unit dependency in this project. Break the cycle."* -- no drag-lint, no index, no repository conventions. Four things were checked independently: the cycle broken exactly as predicted, the project compiling, the nominated cut, no unrelated edits.

  • The previous report shape: FAILED. Haiku moved the class TDemoSession without its method bodies (E2065 x5), and the report never defined "done".
  • The report shown here: PASSED 4/4 -- 0 compile errors, program output byte-identical, cycles printing exactly the predicted result, only the named edits. Re-run on 2026-09-28 against engine 1.19.0-alpha: PASSED 4/4 again.

The protocol and both runs are recorded in docs\INBOX-test-cycle-playbook-followability-haiku-flash.md.

The report (excerpt)

The playbook is long -- it carries the full text of every new unit and every edit -- so this is an excerpt of the real output:

<!-- dl:excerpt-cycles --edges --causes --plan --format text -->

## Cycle 1: demologger <-> democonfig <-> demoaudit <-> demosession

Status: **interface coupling** -- units of this cycle use each other in their INTERFACE uses clauses.

### Why it cycles
- `demologger` interface uses `TDemoSession` (class) at `DemoLogger.pas:40`; declared in `DemoSession.pas:20`.
- `democonfig` interface uses `TDemoLogLevel` (enum) at `DemoConfig.pas:23`; declared in `DemoLogger.pas:15`.
- `demosession` interface uses `TDemoAuditKind` (enum) at `DemoSession.pas:35`; declared in `DemoAudit.pas:24`.

### Step 1: what moves where
1. `TDemoSession` -- **class with methods**, declared at `DemoSession.pas` lines 14-47; method bodies at `DemoSession.pas` lines 64-73, 75-80, 82-86, 88-95, 97-100.
   - Recipe: **extract a base class** and keep `TDemoSession` where it is. Why: its method bodies use `GDemoSessionCount`, `GDemoAuditCount`, `GDemoAuditTrail`, which live in units of this cycle; moving the class would mean moving those bodies (lines 64-73, 75-80, 82-86, 88-95, 97-100) AND everything they use.
   - New class `TDemoSessionBase` in `DemoSession.Untangle` carries exactly what the units below use: `FId`, `FUser`, `Id`, `User`.
2. `TDemoLogLevel` -- **enum**, declared at `DemoLogger.pas` lines 14-15.
   - Recipe: an enum has no code of its own -- move the declaration, unchanged, into `DemoLogger.Untangle`.

### Step 2: uses clauses -- keep, move or remove the old unit
- `DemoConfig.pas` / `DemoLogger`: the interface no longer needs it; the implementation still does (`GDemoMinLevel` line 56, `DemoLog` line 58, `DemoLogLevelName` line 59, `GDemoLogCount` line 67) -> **move** it from the interface uses to the implementation uses.

### Checklist
...
12. [ ] Run `drag-lint cycles --db "circular-demo\_D-RAG\CircularDemo.sqlite"`. It must print exactly:
3 circular unit group(s) found:
...
  [4 units] demologger <-> democonfig <-> demoaudit <-> demosession   (implementation-only -- legal, lower impact)

### Part B (optional): remove the implementation-only cycle too
- `demoaudit -> democonfig`: `GDemoSettings (var)` -- mechanical: these can move into a new leaf unit

Applied literally (a script that only knows the playbook's own markup), Part A compiles and cycles prints the predicted three groups, the four-unit one now implementation-only; re-running --plan then prints Part B, which moves TDemoSettings + GDemoSettings into DemoConfig.Untangle, compiles, and leaves only the two cycles the playbook refuses. The program's output is unchanged throughout. The full, unabridged report -- regenerated from the current engine and checked for drift -- lives in the repository at circular-demo/CYCLE-REPORT.md. That is the file to hand to a model.

What is worth noticing in that output

It names the exact symbol that forces each edge, not just the unit pair. "DemoLogger depends on DemoSession" is easy to say. Saying why -- TDemoSession, a class, referenced at DemoLogger.pas:40, declared at DemoSession.pas:20 -- is what makes the cycle actionable. That is the difference between a warning and a fix plan, and it is only possible because the underlying index is AST-exact rather than a text/grep match.

It reports three edges, not four. The project has a four-unit cycle, but drag-lint lists only the three interface edges under "Why it cycles", because those are the ones that constrain compilation -- the ones you actually have to break. The fourth, implementation-level edge (DemoAudit -> DemoConfig) is exactly what makes the project legal Delphi in the first place, so it is reported separately (see --edges) rather than folded into the "why" list. The tool is not merely detecting a loop in a graph; it is distinguishing the edges that matter from the edge that makes the loop survivable.

The fix it proposes fits the kind of each symbol. TDemoLogLevel and TDemoAuditKind are enums, so they move unchanged. TDemoSession is a class whose method bodies use the cycle's globals, so moving it would drag them along; the playbook extracts a base class with only the members DemoLogger uses and keeps the class where it is. Either way no interface edge survives, and behaviour does not change. --causes and --plan are what produce this level of detail; see Circular Uses Report (cycles + fix plan) for the general feature page.

Honest caveats

  • The report's own preamble says the index can miss references such as set types, so its checklist ends with a compile and a re-run of cycles against a predicted output, and lists what to do for each compile error -- that caveat is drag-lint's own, not added for this page.
  • This project is deliberately small. It exists to make the output legible, not to prove the tool scales -- for cycle reports on real, larger projects, run the same two commands against your own index.

See also


All features | CLI reference | IDE menus