Skip to content

Latest commit

 

History

History
169 lines (139 loc) · 10.8 KB

File metadata and controls

169 lines (139 loc) · 10.8 KB

Rules for structure/

This file applies to structure/ and inherits the repository-wide rules in AGENTS.md. INDEX.md is the reading order and the source-to-doc map; it is generated from manifest.json, so it is never the file you edit to record something.

What belongs here

A doc in this folder states the contract that holds right now, in the present tense, for one subsystem. That is the whole job.

  • Public user workflows belong in docs-site/.
  • Open work, triage, and investigation belong in devlog/.
  • Superseded or alternative reasoning belongs in decisions/, not in the doc body.
  • Unreleased security findings belong in scratch space and nowhere in this repository. The rule in the root AGENTS.md binds this folder without exception.

If you cannot write a sentence in the present tense about how the system behaves today, it is not a structure doc.

Layout rules

  • File names are kebab-case, start with a letter, and sit at most one directory deep: providers/google.md, not providers/google/wire.md, and not 04_transports.md or 04-transports.md.
  • Ordering lives in manifest.json, never in a filename. Leading digits are rejected outright. The old NN_topic.md scheme produced two 09_ files and made splitting a doc cost a renumber, which is how 04_transports-and-sidecars.md reached 1,860 lines before this folder was reorganised.
  • A doc stays under the line budget in manifest.json. Over budget, split it along a topic boundary and give each half its own manifest entry. A grace.oversizeDocs entry is for a split already planned; the gate drops it again once the doc is back under budget.
  • Stage a new file before running the gate. Repository paths are resolved through the git index, so a file you have written but not git added does not exist as far as the check is concerned. That is deliberate: CI runs on a clean checkout, and a gate that passed on untracked local files would disagree with it.
  • Know what the budget does and does not do: it is a line count, so a doc written as a wide table can carry far more prose per line than one written as paragraphs. It bounds the runaway-file failure, not density.

The source-to-doc map

Each doc lists the source areas it describes, in its documents array. INDEX.md publishes the inverse.

  • An area can be described by more than one doc, and usually is. These docs are organised by topic; src/ is organised by module. src/server/ is genuinely described by the management-API doc, the Responses transport doc and the Images doc. An earlier revision of this folder demanded exactly one owner per area, and that rule was simply false here — a false rule is worse than none, because the gate reports green while the map sends a maintainer to the wrong doc.
  • Changing an area obliges the same change to review every doc listed for it. Review fan-out is unchanged by contract authority: update the authority when the shared contract changes, and update a dependent only when its local explanation or consequence changes. A reviewed document whose content remains accurate does not need copied unchanged prose.
  • Describing an area means naming a path inside it. If a doc explains a subsystem without ever citing a path, the map cannot see it, and the area lands in grace.undocumentedSourceAreas instead — which is a signal to add the path reference, not a place to park work. The gate checks the weak form of this: a documents entry is rejected when the doc never names the area or any path under it. Naming the directory itself passes, which a table of directory names does, so the check catches an invented claim but does not prove the doc says anything useful about the area. That part is review.
  • A new src/<area>/ or top-level src/*.ts either joins a doc's documents list or is recorded in grace.undocumentedSourceAreas with a reason. The gate rejects one that is neither.

What the map still does not do: it cannot tell you that two docs describe the same behavior in contradictory words. Avoiding that is a review judgement. Prefer one statement and a link over two statements that will drift apart.

Cross-cutting authority is declared by the optional versioned contracts object in manifest.json. Each entry has a stable kebab-case id, one owner with a manifest-declared document and heading anchor, and a dependents array of manifest-declared documents. Every dependent links the exact owner anchor. The registry supplements the source map; it never narrows which documents a source change requires review of.

The gate proves declared topology: identifiers are unique, files and anchors exist, and each dependent carries the declared link. It does not compare prose or prove that the owner statement is behaviorally correct. Those remain review judgements.

Decision records

decisions/ADR-NNNN-<slug>.md holds the reasoning: intent, prior constraints, alternatives, the choice, why, and consequences.

  • One record has exactly one owning doc, which links it with a > Decision record: line.
  • Numbers are permanent and unique. They are deliberately not required to be contiguous: two branches that each add a record would otherwise both take the next number and collide on merge.
  • Records are historical. When the contract changes, edit the doc body and add a new record; do not rewrite an old one to match. For the same reason the gate does not validate the repository paths a record names — a record describes a past tree, and holding it against the present one would force you to falsify it.
  • A record's title names the doc section it was recorded under, not the decision it contains. That is why every title reads decision recorded under "<section>": the records extracted during the 2026-09-11 reorganisation took their heading from the section they sat in, and a title that looked like a decision name but was not would send a maintainer to the wrong record. Read the body.
  • Ownership is the > Decision record: link, and nothing else. A record path mentioned in prose or shown inside a fenced example is not a claim, so an illustration cannot make a doc a second owner. The link has to land inside decisions/; pointing it elsewhere is rejected rather than matched on the filename.

Invariants

overview.md is the invariant index. Each entry needs a stable INV-<AREA>-NN id, and a bound entry adds an Enforced by line naming exactly one tests/**.test.ts path, with that id repeated in a comment inside the test.

Be precise about the strength of that binding, because it is easy to overstate:

  • It proves the test file exists and claims the id. Deleting or renaming the file fails the gate.
  • It does not prove the assertions inside still cover the rule. Moving the assertions to another file while leaving the comment behind passes. Only review catches that.

An invariant with no honest test goes in the index without an Enforced by line and with an entry in grace.unboundInvariants explaining why. Naming a test that would pass while the rule was violated is worse than admitting the gap: it converts an open question into false assurance.

Adding or changing a doc

  1. Write or move the file.
  2. Add or update its manifest.json entry: path, tier, title, scope, documents.
  3. Add or update any affected contract authority and dependent links in manifest.json; review every document mapped to the changed source area even when its text remains accurate.
  4. bun run structure:index to regenerate INDEX.md.
  5. bun run structure:check until it is green.

What the gate checks

bun run structure:check — also run by tests/ci-workflows/structure-ssot.test.ts, so it blocks CI — verifies that:

  • every doc on disk is in the manifest and every manifest doc exists, exactly once;
  • file names are kebab-case, letter-initial, and at most one directory deep;
  • no doc exceeds the line budget, and no grace entry outlives the split it promised;
  • every relative link resolves, including its #anchor;
  • a fragment-only link resolves against its own document, which is where one broken anchor was hiding;
  • every backticked repository path a doc names is real, checked against the git index rather than the filesystem — existsSync cannot tell a tracked file from untracked local leftovers, and it is case-insensitive on Windows and case-sensitive on Linux CI, which would make the gate mean something different on each machine;
  • no doc body carries either inline decision-log marker: the bracketed Decision-Log heading that the old layout used, or the Korean bullet template that followed it. Reasoning written as ordinary prose is not detectable and stays a review judgement. The check is a literal match, which is why this line describes the marker instead of quoting it;
  • every decision record is linked from exactly one doc, and no number is reused;
  • every bound invariant names an existing test that names the id back, and every unbound one is recorded with a reason;
  • every src/ directory and top-level module is described by a doc or recorded as undescribed;
  • contract ids are unique, owner and dependent documents exist in the manifest, owner anchors exist, and every dependent links its declared authority without listing the owner as a dependent;
  • the manifest itself parses and has the shape the gate expects, reported as a failure rather than a stack trace;
  • overview.md exists, because its absence would otherwise silence every invariant check at once;
  • INDEX.md matches what the manifest generates, compared after newline normalisation so a CRLF checkout is not a failure.

Contract checks establish declared authority and links only. They make no automatic claim that an owner's prose is semantically complete or that a dependent's explanation is behaviorally correct.

Checks are scanned with fenced code blocks removed, so an example inside a fence does not trip a rule it is only illustrating.

One boundary worth stating, because it looks like a gap and is a deliberate one: a backticked token is treated as a repository path only when its first segment is a top-level entry this repository has or used to have. That covers root files too, so package.json and MAINTAINERS.md are checked directly, not only through the links that point at them. What is NOT checked is a bare filename that was never a top-level entry, because these docs name runtime files that live in a user's home rather than in the repository — config.toml, models_cache.json, ocx.pid — and validating every filename-shaped token would reject them.

The top-level set deliberately includes roots that no longer exist, such as go/. Deriving it from the current tree alone would make every reference to a deleted directory invisible at exactly the moment those references go stale.