Skip to content

Discovery and units

Discovery answers one question: what documentable things exist in this repo? It is the first step of the production loop and the thing a survey configures.

Two planes, one rule between them

Discovery splits into two planes, and the rule between them is what keeps the system maintainable:

  • The scanner plane is code - small, deterministic, pluggable. A scanner answers "what candidates exist here?" through a narrow interface, and nothing more.
  • The data plane is YAML - reviewable, agent-draftable, human-committed. Unit definitions group raw candidates into named, typed units.

The hard rule: scanners never absorb extraction logic, prompts, or doc shapes, and layout knowledge never migrates into code. Everything involving judgment stays in reviewable data. This is why a survey drafts YAML, not TypeScript.

Repos already declare their own layout

The key insight behind the scanner plane: a repo usually tells you its own structure. The js-workspace scanner reads pnpm-workspace.yaml globs and each member's package.json name - enumeration follows the workspace manifest, which is ground truth, not folder-name guessing. The same fact generalizes across ecosystems: .sln, settings.gradle, Cargo.toml, go.work.

An accuracy ladder handles repos that declare less:

  1. Ecosystem scanner - the repo declares itself (a workspace manifest, a solution file). Most accurate.
  2. glob - paths predict units, so a glob maps directories to units.
  3. inventory - nothing predicts units, so a committed units.yaml lists them by hand, once.

Any language works on the lower rungs from day one; a dedicated scanner is an accuracy upgrade, not a prerequisite.

A unit is not a folder

This is the idea that makes Alloy fit real repos. A unit is one documentable thing, and the mapping to the filesystem is whatever your repo actually is:

  • One unit can span several packages - a service doc that covers seven slice packages of one service.
  • One unit can be keyed on two variables - an adapter named from a {concern} and a {provider}.
  • One folder can hold several units, and several folders can make one unit.

Unit definitions express this in YAML:

yaml
units:
  - kind: service                    # one doc unit spans all slices of a service
    match: backend/services/{name}/*
    groupBy: name
  - kind: adapter                    # two variables → one name
    match: backend/adapters/{concern}/{provider}
    nameTemplate: "{concern}-{provider}"

Kinds fix shape and rule pack

A unit's kind (service, lib, adapter, ...) is a claim that every member of the kind is the same sort of thing. It fixes two downstream facts: the doc shape (which sections the gate requires) and the rule pack (which files feed the doc). Getting kinds right is why the survey tests each kind's definition against sampled members - a kind that does not partition cleanly is a bundle, and a bundle documents two things as if they were one.

Discovery reports what it could not match

Discovery is honest about its own blind spots. It reports not just the units it grouped but the count of unmatched candidates - files and directories no unit definition claimed. That count is the input to coverage accounting (see Views and coverage): an unmatched candidate becomes UNKNOWN, which forces a decision, so a new directory in your repo announces itself instead of waiting to be missed.

E11 Alloy - knowledge as a build artifact.