runward

RW™ · V0.29.0

Docs · Operate · Rule territory

Rule territory.

64 rules, a three-file diff: which ones concern you? `runward rules --for` answers per path, citing the declaration that bit. Three declared states, never silence.

The problem: 64 rules, a three-file diff

The catalog holds 64 craft rules. Your change touches three files. Which ones concern you?

Without an answer, only two behaviours remain, and both are bad. Re-read all 64 every time, which nobody sustains. Or read none and trust the green gate, which confuses "the evidence holds" with "the code is good". The gate verifies that a decision is traced to evidence that resolves. It never claimed to read your code.

runward rules --for src/entry.serve.ts src/db/migrations/003_add_index.sql

The command answers, per path, which rules have that file in their territory — and why.

Three declared states, never silence

A rule is always in one of three states. The third is the one that matters.

  • Declared territory: the rule names the paths it governs, by globs (appliesTo) or by a category a mission binds to its files (governs). 14 rules of 64.
  • No territory, declared as such: the rule states openly that it has no file territory, and says why. A pre-production checklist gates the system as a whole, not a class of files. 50 rules of 64.
  • Unreviewed: nobody has ruled yet. Today: zero.

That is the whole point. Writing nothing is not the same as declaring there is nothing to write. An empty field reads as "we don't know", "we didn't get to it", or "there is nothing" — all three at once, so none of them. Which is why the catalog renders the three states as three distinct things, and why the unreviewed counter is published even when it is zero. The day it stops being zero, it will show.

Where a match comes from, and why it is rendered

runward never infers from the content of your files. A match comes from a declaration, and the declaration is cited.

1. The rule itself. It carries its globs. A match shows the glob that bit, on the git check-ignore -v model: not "this file is ignored", but "that line, in that file, ignores it".

2. Derivation from a deployment manifest. A wrangler.jsonc, wrangler.json or wrangler.toml declares an execution topology: an entry module, a cron, a queue consumer. runward reads it and derives a category.

async-job-guardrails   HIGH   governs=background-work ← wrangler.serve.jsonc triggers.crons

That is a manifest you wrote, not your code. An unknown framework yields no derivation — never a guess.

3. The mission map. Because a deployment manifest describes a topology and never the nature of the code behind it. An entry module that derives a token from an environment secret: no wrangler.toml can say that. You can.

runward/territory.md

Four columns, inside a ## Territory section (any heading level works).

## Territory

| Pattern | Category | Effect | Why |
|---|---|---|---|
| `src/entry.*.ts` | `secret-boundary` | declare | derives a token from an environment secret |
| `src/entry.browser.ts` | `background-work` | remove | the framework's browser bundle runs nothing in the background |

Precedence is named in three places rather than inferred: the last matching row wins, per (path, category); and the order is derivation < map < a rule's own appliesTo, which the map may never narrow. The map corrects what runward guessed; never what the maintainer decided.

remove is a column rather than a ! prefix, for a precise reason: a character that inverts the meaning of a line disappears in a diff.

Every refused row is named with its line number. An invented category, an empty why, a path escaping the project — each is reported, never silently dropped. A row the operator believes is working and runward ignored is the worst state of all. And if the whole file is unreadable, the output says so first, above everything else:

This map was not read
  ✗ runward/territory.md: no `Territory` heading — the map needs a `## Territory` section (any heading level) above its table
  Everything below is derivation only — your declarations had no effect.

runward never writes this map. init does not scaffold it, update does not refresh it. A file the tool rewrites is a file the operator stops owning, and that ownership is what makes a stale row visible in a diff.

Maps rot: the measure lives in status

A map maintained in a repository goes stale. That is documented, not assumed: an FSE 2025 study of static-analysis suppressions measures 50.8% of suppressions inert, monotonic growth, and stale suppressions masking future warnings.

So runward status reports coverage, writing nothing:

Territory coverage
  1 of 109 walked file(s) carry a category · background-work 1 · secret-boundary 1
  runward/territory.md: 1 row(s) declared.
  ! 1 row(s) matched no walked file:
      territory.md:7  src/legacy/*.ts → startup
  A row that affects nothing today. Dead or merely early is your call, not the tool's.

A row that touches no file is named, with its line number. runward does not rule between "dead" and "merely early": that judgement is yours. A one-row map describing a reality beats a thirty-row map describing an intention.

What it does not do

  • It never reads your project's code. Only declarations: a rule's globs, a deployment manifest you wrote, your map.
  • It does not change the gate. runward check --strict returns exactly the verdict it returned before. Territory tells you what to confront at the point of action, not how to cross a step.
  • An unknown category is not a guess. The vocabulary is closed (eight categories). A rule governing a category that no declaration binds to your files does not surface — and it is not counted as evaluated either: runward rules --for --json files it under unresolved, distinct from rules genuinely ruled out. The difference between "does not apply" and "cannot be decided" is kept, not smoothed over.
← Docs