Tools › Understanding code

Tools

Understanding code: who wrote it, what it does, what its architecture is

Three ways to read code that already exists, written by people, by archec gen, or by an AI assistant.

Who wrote the code: the model, an AI assistant, or a person#

With AI writing code, a reviewer needs to know who wrote what. archec provenance main.arche (or archec audit main.arche --authors) gives each line of code one origin:

  • generated: archec gen wrote it and it is unchanged (gen leaves .archegen.json beside its output, a fingerprint per line);
  • AI: an AI assistant wrote it: its commit has a Co-Authored-By: Claude…, Generated-by: or AI-assisted: trailer, or an assistant's address, or a marker in .arche/provenance.jsonl says so;
  • edited after gen: gen wrote the file and a person changed the line;
  • human: the rest.

It reads git blame, so each line counts for the commit that last changed it. It sums the lines by file and by part of the model (the files a part names with source or code, and those of its parts), and lists the files an assistant wrote that no person committed to since. archec diagram main.arche Car --overlay provenance colours each block by who wrote most of its code; archec doc --html --provenance adds the tables to the specification.

For Claude Code, add the hook to .claude/settings.json: it marks the lines Claude writes as it writes them.

{ "hooks": { "PostToolUse": [ { "matcher": "Edit|MultiEdit|Write",
    "hooks": [ { "type": "command", "command": "archec provenance hook" } ] } ] } }

Other assistants can call archec provenance mark <file> --lines 10-40 --tool <name>. See the commands.

Behaviour from traces: what the code actually does#

Code written fast, or by an AI, does things nobody wrote down. archec observe reads what it did — its OpenTelemetry traces (OTLP JSON, as the SDK's or the collector's file exporter writes them), or a Jaeger or Zipkin export — and writes it as a model to read and draw.

cd test/fixtures/observe
archec observe shop.otlp.jsonl --states -o observed.arche
archec diagram observed.arche post-checkout-error      # a sequence diagram
archec diagram observed.arche Observed.orders.states   # a state diagram

Each service is a part. Each call from one service to another is a message on a link, with how many were seen, how long they took (p50, p95) and how many failed. A database or a cache called with no trace of its own is a part too. The traces are grouped by the way they went: one scenario per shape, with how often it occurred. Calls that overlap are drawn in a par, the same call in a row in a loop.

// observed: 9 of 64 trace(s) went this way (14%)
scenario post-checkout-error "POST /checkout (payment declined)" {
  sequence post-checkout-error-steps "POST /checkout (payment declined)" {
    Observed.outside -> Observed.gateway : POST_CHECKOUT
    Observed.gateway -> Observed.orders : POST_ORDERS
    Observed.orders -> Observed.orders : validate_cart
    par {
      when "RESERVE" { … }
      when "CHARGE" {
        Observed.orders -> Observed.payments : CHARGE
        Observed.payments -> Observed.orders : CHARGE "error: card declined" { reply }
      }
    }
    Observed.orders -> Observed.inventory : RELEASE
    …

--states adds a state machine to each service that waits on another. Its states are the replies it awaits; a comment says in how many traces each transition was taken:

    // observed: in 9 of 64 trace(s)
    AwaitingCharge -> AwaitingRelease on CHARGE when "the reply is an error" / "sends RELEASE"

With a model, --model main.arche compares instead: services no part is, calls the model has no link or message for, links never seen, a link's deadline against the time on the wire, and the checks of archec replay. archec replay reads the same traces. See the commands.

From code to model#

The rest of arche-lang goes from the model to code. archec recover goes the other way, for code nobody drew — written over years, or by an AI in an afternoon:

archec recover src -o recovered.arche      # TypeScript, JavaScript, Go, Python, C/C++, Rust
archec diagram recovered.arche --md        # see it

It reads each file's imports with a small scanner per language (no compiler, nothing installed), and what the file does with them: how often it uses each name it imports, and how — a call, a new X (a struct literal in Go and Rust), a type, another value. An import used often weighs more (about 1 + log₂ of its uses); one that takes only types weighs half, a re-export (export … from) ½, one never used ¼–½. TypeScript and JavaScript are read with the TypeScript compiler's parser when it is there (archec's own, or the working folder's node_modules), else lexically like the other languages: the file's names, strings and comments left out, matched with what each import binds (import { a as b } binds b, import x.y binds x.y, Go's package name is used as pkg.Name, a C header binds what it declares). In Go the files of one package use each other with no import at all: what a file uses of the names another file of its package declares is a reference between the two, weighed the same way.

It makes a part of each folder, cuts a folder of many files into groups — first families of names that import one another (websim*, parser*), then the rest by coupling (the weights above; a file many import counts less towards where each of them goes), each group named after its stem or the file others import most — and writes one link per pair of parts with the number of imports (and references). A file many import that imports at most two files of its folder is a library: kept apart as common; one many import that imports much (a program's centre) is placed by coupling like the others. A file beside folders that one of them uses, and that has most of its links with it, joins that folder's part (// recovered: says which). Among sibling parts, one that two thirds of the others use and that uses them a quarter as much or less is written kind library: what it imports back is said on it, and left out of the cycles, so that a cycle is the knot of the design and not a helper imported back. Cycles are broken by their lightest links by weight: an import of types only goes first. The standard library (Node's own modules, Python's, Go's, libc, Rust's std) is left out: every part would link to it (--std keeps it). Each part says its files with source, and the names others import from it are its functions. What it had to guess is a // recovered: comment:

/// 14 files, 3 996 lines.
component Importer "importer" {
  kind module
  source "src/importer/**"

  in api: call
  out uses: call

  fn import_project "importProject"
  fn import_sysml "importSysml"
  …
  /// 4 files, 1 182 lines.
  component sysml "importer/sysml*" {
    kind module
    source "src/importer/sysml.ts"
    …
  }

  sysml.uses -> ir.api "8 imports"
}

system ArcheLang "arche-lang" {
  prop recovered = "archec recover src"
  …
  // recovered: a cycle: 25 parts import one another; 53 light links break it (archec recover --json lists them). To shrink it first, cut:
  // recovered:   what common imports from generics, libraries, parser (+2) (11 imports, src/program.ts:18…): 22 parts left in a cycle
  // recovered: 107 weak links (fewer than 3 imports) not drawn: weak_links lists them, archec conform allows them (--min-imports 1 draws all)
  prop weak_links = [
    "board -> gen: 1 import",
    …
  ]
  cli.uses -> importer.api "8 imports"
  …
}

A block of more than 12 links keeps as links those of 3 imports or more; the weaker ones are listed in prop weak_links, so nothing is lost (--min-imports N sets the bar in every block, --min-imports 1 draws every link). A cycle is said by the light links that break it, each with an import's file and line (a feedback arc set weighted by imports), or, for a big one, by what to cut first. archec diagram draws at most the 30 heaviest links of a recovered model's block (prop recovered says it is one; each block its heaviest link first) and says how many it left out; --max-links N sets it for any model whose links say their weight ("12 imports"), --max-links 0 draws all.

archec recover --json gives each link its imports, references, weight and uses. How close the grouping comes to a model drawn by hand: node tools/recover-score.mjs compares archec recover src with arche-lang's own model (examples/arche-lang), pairs of files placed together by both, as precision and recall.

Rename the parts, regroup them, remove the links you do not want: that is now the architecture you mean. archec conform keeps it true — the reflexion model: each import of the code between two parts must be a link of the model (else an error at the import's file and line), each link should have imports under it (else a warning), and each file should belong to a part. A pair of parts listed in weak_links is allowed: its imports are counted apart, neither convergence nor divergence (remove the line to forbid it).

Divergences: imports the model has no link for (1, between 1 pair(s) of parts)
  error Code.core → Code.util: 1 import(s)
      src/core/engine.ts:1 imports '../util/log' (Code.util)

Code: 5 of 5 files mapped; 4 link(s) converge, 1 import(s) diverge, 0 link(s) absent
✗ the code does what the model does not say

archec ci runs it as a gate when a source names a file of the code. archec arch-diff src main..HEAD --md recovers two revisions from git and writes, for the pull request, the parts and links the branch added or removed, the files it moved between parts, and with --model the divergences it brought in. The second revision is grouped as the first: a folder that crossed 12 files is cut into groups, or kept whole, as before, and said under Folders rather than as files moved.

Its limits: imports are read, not types or calls (--graph gitnexus adds calls from a GitNexus index); an import of a computed name is only said; the grouping is a first proposal, not a design.