etymd 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +71 -2
  2. package/README.md +276 -61
  3. package/dist/{approve-X3AW7KLD.js → approve-JEOEC2MV.js} +6 -4
  4. package/dist/audit-GYZ5YZ7U.js +12 -0
  5. package/dist/{brief-RT6KE4CN.js → brief-ZK7TXAC4.js} +6 -4
  6. package/dist/{chunk-4UZBCCFJ.js → chunk-3NYDVB4Z.js} +123 -25
  7. package/dist/chunk-CRJHIXJH.js +5 -0
  8. package/dist/chunk-EF6BPBG6.js +22 -0
  9. package/dist/chunk-LT67FU2Y.js +59 -0
  10. package/dist/{chunk-E2Q7WFPN.js → chunk-MKYPLNLH.js} +8 -5
  11. package/dist/{chunk-EUIHYD6F.js → chunk-MMPV67FW.js} +2 -2
  12. package/dist/{chunk-C4Z6NXDO.js → chunk-NFENQ3IK.js} +1 -1
  13. package/dist/{chunk-ANCCGYSH.js → chunk-NMZ3RHWW.js} +31 -59
  14. package/dist/{chunk-FZSIOCOZ.js → chunk-R74SJ7HS.js} +1 -1
  15. package/dist/{chunk-LBRNZZZF.js → chunk-TADKOW6P.js} +1 -99
  16. package/dist/{chunk-SS7VGLJ5.js → chunk-V3DPLOOM.js} +71 -15
  17. package/dist/{chunk-KBF3SW3N.js → chunk-WWBF4Y7K.js} +1 -1
  18. package/dist/{chunk-6W3J4PJ6.js → chunk-XTAX4YNT.js} +2 -2
  19. package/dist/chunk-YLNQQZ5F.js +102 -0
  20. package/dist/cli.js +50 -16
  21. package/dist/config-MX7OYI7F.js +4 -0
  22. package/dist/{context-GYCLR3TG.js → context-4DGXO7JR.js} +5 -3
  23. package/dist/doctor-C2RTL2QV.js +19 -0
  24. package/dist/{fleet-IS6YWDDV.js → fleet-YZGR4UC2.js} +243 -10
  25. package/dist/gates-ILE4PXDA.js +272 -0
  26. package/dist/generate-IYCQETQB.js +4 -0
  27. package/dist/index.d.ts +113 -33
  28. package/dist/index.js +777 -267
  29. package/dist/{init-ISEFZWPV.js → init-OVLO4OBZ.js} +8 -5
  30. package/dist/ledger-TMIV45AA.js +5 -0
  31. package/dist/{scan-ADZ2RKYY.js → scan-RAJUFTQR.js} +6 -4
  32. package/dist/scan-ZYZ7XDSJ.js +5 -0
  33. package/dist/screen-U7JMWZPS.js +134 -0
  34. package/package.json +2 -2
  35. package/dist/audit-YH74YZFQ.js +0 -9
  36. package/dist/doctor-EAA4IP4Y.js +0 -16
  37. package/dist/gates-3NPKF4IZ.js +0 -71
  38. package/dist/ledger-RHQJH5EA.js +0 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,74 @@
1
1
  # etymd
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Content screening, a registration gate, and gates that can be regenerated without loss.
8
+
9
+ **`etymd screen` — a content screen with no opinions of its own.** Four scopes for the four ways
10
+ content leaves a repository: `--staged` (a commit), `--message` (the message, which the staged
11
+ scan cannot see), `--tree` (everything tracked), and `--dir` (an unpacked build artifact). The
12
+ last exists because the others share a blind spot — they answer "what is in the repository?",
13
+ while `npm` and `vsce` ignore `.gitignore`, so a local file can ship to users while every
14
+ git-scoped check passes forever.
15
+
16
+ It ships **no patterns and never will**: the strings worth screening for are themselves the
17
+ sensitive material. You supply a pattern file; without one the command is inert and says so,
18
+ rather than reporting clean for a check it did not run. A repository naming itself is exempt, and
19
+ a repo-level allow file covers lines that cannot carry an inline marker — a scanner's own source
20
+ necessarily contains the patterns it screens for.
21
+
22
+ **`etymd gates` generates all four doors**, including a `commit-msg` hook and a publish-time
23
+ screen wired to the key the project's publish route actually runs (`vsce` ignores
24
+ `prepublishOnly`). Generated hooks resolve the screener at run time and no-op when it is absent,
25
+ so the same file is safe to commit to a public repository.
26
+
27
+ **Your own checks live beside the generated ones.** Each hook calls `.githooks/<hook>.local` if it
28
+ exists — a file etymd never reads, writes, or regenerates. Regeneration no longer forces a choice
29
+ between accepting the pack and keeping your own guards, and it will not drop a test step an
30
+ existing hook already ran.
31
+
32
+ **Setup is one keystroke.** `gates` shows a plan with every derivation stated and asks once;
33
+ `customize` reaches each choice. Answers record to `.etymd/config.json`, where
34
+ `gates._why.<field>` can carry the reason a value is what it is — dropped automatically when the
35
+ value it explains changes, because a stale reason misleads exactly where it meant to inform.
36
+
37
+ **`etymd fleet add`** registers a project, prompting for what no scan can derive and refusing to
38
+ write an incomplete entry. Fleet manifests gain a mandatory `trust` level on non-corp entries —
39
+ absence is a finding, never a silent default — a manifest-level `orientation.root` replacing
40
+ per-entry links, and a gate-drift check that reports a repository missing a gate its siblings
41
+ install.
42
+
43
+ ## 0.2.2
44
+
45
+ ### Patch Changes
46
+
47
+ - 80db483: Gate integrity: detect scripts regardless of how the package manager is invoked.
48
+
49
+ Script expansion guessed the script name positionally — the token straight after the
50
+ manager. That only holds for `npm run x`, `yarn x` and `pnpm x`. Every other live shape
51
+ expanded to nothing, so the tool was never detected and the gate read as absent:
52
+
53
+ | invocation | captured as the script name |
54
+ | ------------------------------------ | --------------------------- |
55
+ | `pnpm run typecheck` | `run` |
56
+ | `pnpm -s typecheck` | `-s` |
57
+ | `pnpm -r --if-present run typecheck` | `-r` |
58
+ | `pnpm --filter @scope/pkg test` | `--filter` |
59
+
60
+ A pre-push hook running `pnpm run typecheck` therefore reported as having no typecheck at
61
+ all, and `audit` said "type checking is enforced only in CI — no local hook runs it" while
62
+ the hook ran it on every push.
63
+
64
+ Expansion no longer guesses position — it cannot, since the position depends on each
65
+ manager's own flag grammar. It scans the invocation's tokens for a name that is a known
66
+ script, which needs no grammar at all. Package-manager built-ins are excluded so `npm ci`
67
+ is never read as "runs the `ci` script"; that direction matters, because expanding it would
68
+ claim a gate is covered by a line that only installs dependencies. `test` and `start` stay
69
+ recognised — those genuinely are script shortcuts. `exec`/`dlx` end the scan, since what
70
+ follows is a binary, and `npx` is only scanned for `run-s`/`run-p`/`npm-run-all`.
71
+
3
72
  ## 0.2.1
4
73
 
5
74
  ### Patch Changes
@@ -22,7 +91,7 @@
22
91
 
23
92
  ### Minor Changes
24
93
 
25
- - a58d3bd: Fleet mode — the truth guard across your repositories (design record `docs/design/004-fleet-truth-guard.md`; registry + fleet `--json` schemas EXPERIMENTAL through 0.2.x).
94
+ - a58d3bd: Fleet mode — the truth guard across your repositories (design record `docs/decisions/004-fleet-truth-guard.md`; registry + fleet `--json` schemas EXPERIMENTAL through 0.2.x).
26
95
 
27
96
  - New `state-freshness` truth lens: state/decisions artifacts dated by git committer dates only (never mtime); staleness is relative, so a dormant repo's old state is current; state char budget against the ~10k session-hook truncation; marker-gated decisions format checks (`Scope:`, duplicate/out-of-order `D-NNN` ids, past `Revisit:` dates as due review debt); ADR conventions (`docs/adr/`, `docs/decisions/`, `NNNN-*.md`) recognized natively.
28
97
  - New `etymd fleet` command family. The sweep runs a read-only audit per registered repo (`--manifest` required unless the cwd holds `registry.json` — no env var, no global pointer) and renders one line per project with a delta against `last.fleet.json`; detail only for new or risk findings. `fleet check` validates the manifest pair alone (dangling mappings, duplicate names, privacy leaks, machine paths). `fleet dismiss`/`fleet accept` resolve a project's finding from any cwd.
@@ -37,7 +106,7 @@ First public release.
37
106
 
38
107
  The truth guard for agent instruction files — **keep your agent instructions true**. (Formerly
39
108
  prototyped as "clothaid", a broader workflow installer; the pivot and its state-of-the-field
40
- rationale are recorded in `docs/design/003-truth-guard-pivot.md`.)
109
+ rationale are recorded in `docs/decisions/003-truth-guard-pivot.md`.)
41
110
 
42
111
  - `etymd audit` — verify every instruction claim against the actual repo, through three lenses:
43
112
  - **instruction-truth**: command claims vs `package.json` scripts, path claims vs the tree,
package/README.md CHANGED
@@ -1,54 +1,117 @@
1
- # etymd
1
+ # Etymd
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/etymd.svg)](https://www.npmjs.com/package/etymd)
4
- [![CI](https://img.shields.io/github/actions/workflow/status/triartleet/etymd/ci.yml?branch=main&label=CI)](https://github.com/triartleet/etymd/actions/workflows/ci.yml)
3
+ <div align="center">
4
+ <img src="media/etymd-logo.png" width="520" alt="Etymd — a papyrus of written instructions, each line checked against the repository it describes">
5
+ <p>
6
+ <a href="https://www.npmjs.com/package/etymd"><img src="https://img.shields.io/npm/v/etymd.svg?label=npm&color=cb3837" alt="npm version"></a>
7
+ <a href="https://github.com/triartleet/etymd/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/triartleet/etymd/ci.yml?branch=main&label=CI" alt="CI"></a>
8
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="MIT license"></a>
9
+ </p>
10
+ </div>
5
11
 
6
12
  **Keep your agent instructions true.**
7
13
 
8
- Your `AGENTS.md` is the interface between your team and every coding agent — and it rots silently.
9
- Scripts get renamed, directories move, rules go stale, and the file keeps instructing agents with
10
- confidence. etymd continuously **verifies the agent context layer against the actual repo**:
14
+ You wrote rules for your AI months ago. Since then a script got renamed, a folder moved, a habit
15
+ changed — and the AI still trusts every word. The file never complains when it goes stale; it just
16
+ keeps instructing, confidently, and you live with the results.
17
+
18
+ Etymd reads those instruction files and checks every claim in them against your actual project.
19
+
20
+ One command, run in your project's folder (needs Node ≥ 18.17, nothing else): `npx etymd audit`.
21
+ Here it is on a small demo project whose AGENTS.md still tells the AI to run `npm run start` and
22
+ `npm run lint` and points at a `src/legacy/` folder — none of which exist any more:
11
23
 
12
24
  ```
13
- $ etymd audit
25
+ $ npx etymd audit
26
+
27
+ RISK AGENTS.md tells agents to run `start` — no such script exists
28
+ evidence AGENTS.md: `npm run start` · package.json scripts (root + workspaces)
29
+ why An agent following this instruction runs a command that fails — or silently skips the check it was meant to run.
30
+ action Update the instruction to the current script name (or restore the script).
31
+ effort S · confidence high · instruction-truth · instruction-truth/stale-command:AGENTS.md:start
14
32
 
15
- RISK AGENTS.md tells agents to run `pnpm dev` — no such script exists
16
- evidence AGENTS.md: `pnpm dev` · package.json scripts
17
- action Update the instruction to the current script name.
33
+ ...
18
34
 
19
- GAP AGENTS.md references `src/legacy/` — it does not exist in the repo
20
- GAP Type checking is enforced only in CI — no local hook runs it
21
- GAP AGENTS.md loads 13,809 words (~18k tokens) into every session
35
+ GAP AGENTS.md references `src/legacy` — it does not exist in the repo
36
+ evidence AGENTS.md · missing: src/legacy
37
+ why Agents navigate by these references; a dead path wastes a lookup and erodes trust in the rest of the file.
38
+ action Fix or remove the reference.
39
+ effort S · confidence medium · instruction-truth · instruction-truth/stale-path:AGENTS.md:src/legacy
22
40
 
23
- since last audit: 1 new · 3 still open · 1 resolved · 1 REGRESSED
41
+ since last audit: 3 still open
24
42
  ```
25
43
 
44
+ Each entry names something that is no longer true, shows the evidence it found, and suggests the
45
+ smallest fix. And it remembers between runs: a problem you fixed — or looked at and deliberately
46
+ waved off — never nags you twice.
47
+
48
+ That's the whole deal. It works with zero configuration and never rewrites your files — the
49
+ memory it keeps between runs lives in one small folder of its own (`.etymd/`). Everything below
50
+ the line is reference — read it when you need it.
51
+
26
52
  _From Greek **étymon** — a word's true, original sense (→ etymology) — clipped to **etym.** + the
27
53
  **.md** family it guards._
28
54
 
55
+ ---
56
+
29
57
  ## Why this exists
30
58
 
31
- - Instruction files are now load-bearing: 20+ agents (Claude Code, Codex, Cursor, Copilot,
32
- Gemini, …) read `AGENTS.md` natively. A stale claim doesn't error — it silently misleads every
33
- session.
34
- - Linters exist for these files, but they check **a point in time**. Truth is a property **over
35
- time**: etymd measures drift against a **committed baseline**, remembers findings in a
36
- **ledger** (fixed things stay fixed; a returning problem is named a _regression_, and a finding
37
- you dismissed with a reason never resurfaces), and gates CI on it.
59
+ - **Truth is a property over time, not a point in time.** Instruction files are load-bearing now —
60
+ coding agents (Claude Code, Codex, Cursor, Copilot, Gemini, …) read `AGENTS.md` natively, and a
61
+ stale claim doesn't error, it silently misleads every session. Linters for these files check a
62
+ moment; Etymd measures _drift_ against a committed _baseline_ and remembers findings in a
63
+ _ledger_, so fixed things stay fixed and a returning problem is named a _regression_ (all four
64
+ words defined just below). It runs when you invoke it — or when a hook or CI job you wire up
65
+ does.
38
66
  - **Honesty is structural.** Every report declares what it could NOT see — CI jobs inherited from
39
- unreadable org templates, server-side quality-gate thresholds, skipped heuristics. No guess is
40
- ever dressed as a fact.
67
+ unreadable org templates, server-side quality-gate thresholds, heuristics it skipped. No guess
68
+ is ever dressed as a fact.
69
+ - **Precision over recall.** A false "your file is lying" costs more trust than a missed lie, so
70
+ the checks filter aggressively — and every class of claim they skip is counted and disclosed,
71
+ never silently dropped.
72
+
73
+ ## The words Etymd uses
74
+
75
+ | The docs say | It means |
76
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
77
+ | **finding** | One verified problem, ranked **RISK** (an agent acting on this does the wrong thing) → **GAP** (a dead reference or missing safeguard) → **POLISH** (worth tidying). Within a tier, cheapest fix first. |
78
+ | **claim** | Anything an instruction file asserts about the project that can be checked: a command it tells agents to run, a path it points at, a rule about tooling. |
79
+ | **lens** | One self-contained checker for one kind of truth (are the commands real? is the state doc current?). An audit is all lenses run together. |
80
+ | **baseline** | A snapshot of the repo's checkable facts that you approved and committed. Drift is measured against this — not against whatever yesterday's cache happened to hold. |
81
+ | **drift** | The distance between the baseline and the repo today: what existed at approval and is now gone, renamed, or moved. |
82
+ | **ledger** | The committed memory of findings — each one's status and history. A finding that was fixed and comes back is a **regression**, and the report names it as one rather than re-introducing it as new. |
83
+ | **dismiss vs accept** | Two deliberate ways to close a finding. _Dismiss_ = "not a real problem, here's why" — it never resurfaces unless it regresses. _Accept_ = "true, and we're living with it" — kept in the ledger, out of the report. |
84
+ | **gate** | A check that can actually fail a change: a git hook, a CI job. A job marked `allow_failure` is advisory, not a gate — a check that cannot fail anything is an opinion. |
85
+ | **disclosure** | The report's account of what it could not see or refused to guess about. Every report carries one; a clean result with no disclosures would be the exact dishonesty this tool exists to catch. |
86
+ | **fleet** | Your fleet of **repositories** — every repo you registered in one manifest, swept by one command. Not a fleet of AI agents. |
87
+ | **context economy** | The words your instruction files load into every single session, measured against a budget. Context is a cost you pay per conversation; leaner files are cheaper and better obeyed. |
88
+
89
+ ### Things that surprise first-run users
90
+
91
+ - **"It missed an obvious stale command."** Without `node_modules` installed, command claims are
92
+ skipped — and the skip is disclosed in the report. A command might resolve to an installed
93
+ binary, and Etymd would rather say "couldn't check" than accuse an honest file. Install
94
+ dependencies and run again.
95
+ - **`etymd audit` works without `etymd init`.** You only lose drift-over-time measurement — with
96
+ no committed baseline, there is nothing to measure drift against. Everything else runs.
97
+ - **`etymd init` never overwrites an existing `AGENTS.md`.** It scaffolds a minimal one only if
98
+ you have none. The feared overwrite path simply does not exist.
99
+
100
+ ---
41
101
 
42
102
  ## Quick start
43
103
 
44
104
  ```bash
45
105
  cd your-project
46
- npx etymd init # approve the baseline (+ scaffold AGENTS.md only if you have none)
47
106
  npx etymd audit # verify every instruction claim against the repo
107
+ npx etymd init # opt in to drift: approve the baseline (+ scaffold AGENTS.md only if you have none)
48
108
  npx etymd audit --fail-on risk # the CI gate
49
109
  ```
50
110
 
51
- Requires Node ≥ 18.17. On npm since v0.1.0 — `npx etymd` just works.
111
+ `audit` needs no setup — without `init` you get the full findings report and lose only drift
112
+ measured against a committed baseline. `init` is that opt-in, not a prerequisite, and it never
113
+ overwrites an existing `AGENTS.md`. On npm since v0.1.0 — `npx etymd` just works. To wire the
114
+ gate into a pipeline, see [In CI](#in-ci).
52
115
 
53
116
  ## What it checks
54
117
 
@@ -90,27 +153,29 @@ personal and employer repos. See [the fleet manifest](#the-fleet-manifest-experi
90
153
 
91
154
  ## Commands
92
155
 
93
- | Command | What it does |
94
- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
95
- | `etymd audit` | Verify every claim; ranked findings (risk → gap → polish) + ledger diff. `--lens`, `--truth`, `--json`, `--no-ledger`, `--fail-on <tier>`. |
96
- | `etymd init` | Onboard: approve the committed baseline; scaffold a minimal AGENTS.md **only if missing**. Never overwrites. |
97
- | `etymd doctor` | Alias for `audit --truth`. |
98
- | `etymd context` | The economy view: per-file always-loaded footprint + extraction candidates. |
99
- | `etymd gates` | Install local git-hook gates (pre-commit / pre-push) built from your own check scripts. |
100
- | `etymd scan` | The deterministic reckoning behind everything. `--json`. |
101
- | `etymd brief` | A grounded briefing your in-repo agent completes to author the semantic layer. |
102
- | `etymd approve` | Refresh the committed baseline non-interactively after intentional structural changes. |
103
- | `etymd ledger` | The findings memory: every tracked finding with status and history. |
104
- | `etymd dismiss` | `dismiss <id> --reason <text>` — a dismissed finding never resurfaces without regressing. |
105
- | `etymd accept` | `accept <id>` — record a finding as accepted reality; visible in the ledger, out of the report. |
106
- | `etymd fleet` | Sweep every project in a fleet manifest: read-only per-repo audits + manifest/wall checks. `--manifest`, `--only`, `--profile`, `--truth`, `--persist-ledgers`, `--json`, `--fail-on`. |
107
- | `etymd fleet check` | Validate the manifest pair alone (no lenses): dangling mappings, duplicate names, privacy leaks, machine paths. Non-zero exit on any finding. |
108
- | `etymd fleet dismiss` / `accept` | `<name> <id>` — resolve a project's finding from any cwd; corp findings persist beside the manifest, never in the corp worktree. |
156
+ | Command | What it does |
157
+ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
158
+ | `etymd audit` | Verify every claim; ranked findings (risk → gap → polish) + ledger diff. `--lens`, `--truth`, `--json`, `--no-ledger`, `--fail-on <tier>`. |
159
+ | `etymd init` | Onboard: approve the committed baseline; scaffold a minimal AGENTS.md **only if missing**. Never overwrites. |
160
+ | `etymd doctor` | Alias for `audit --truth`. |
161
+ | `etymd context` | The economy view: per-file always-loaded footprint + extraction candidates. |
162
+ | `etymd gates` | Install local git-hook gates (pre-commit / commit-msg / pre-push, plus a publish screen where something ships) built from your own check scripts. |
163
+ | `etymd screen` | Content screen: find text that must never be published. Four scopes — `--staged`, `--message`, `--tree`, `--dir`. Bring your own patterns; etymd ships none. |
164
+ | `etymd scan` | The deterministic reckoning behind everything. `--json`. |
165
+ | `etymd brief` | A grounded briefing your in-repo agent completes to author the semantic layer. |
166
+ | `etymd approve` | Refresh the committed baseline non-interactively after intentional structural changes. |
167
+ | `etymd ledger` | The findings memory: every tracked finding with status and history. |
168
+ | `etymd dismiss` | `dismiss <id> --reason <text>` — a dismissed finding never resurfaces without regressing. |
169
+ | `etymd accept` | `accept <id>` — record a finding as accepted reality; visible in the ledger, out of the report. |
170
+ | `etymd fleet` | Sweep every project in a fleet manifest: read-only per-repo audits + manifest/wall checks. `--manifest`, `--only`, `--profile`, `--truth`, `--persist-ledgers`, `--json`, `--fail-on`. |
171
+ | `etymd fleet check` | Validate the manifest pair alone (no lenses): dangling mappings, duplicate names, privacy leaks, undeclared trust, machine paths. Non-zero exit on any finding. |
172
+ | `etymd fleet add` | `add <dir>` — register a project: scans it, asks for what no scan can derive, and refuses to write an entry missing a mandatory field. `--name`, `--kind`, `--profile`, `--trust`, `-y`. |
173
+ | `etymd fleet dismiss` / `accept` | `<name> <id>` — resolve a project's finding from any cwd; corp findings persist beside the manifest, never in the corp worktree. |
109
174
 
110
175
  `--cwd <dir>` targets another directory. Read-only probing of any repo leaves **zero trace**
111
176
  (`audit --no-ledger` writes nothing).
112
177
 
113
- ## The files etymd keeps
178
+ ## The files Etymd keeps
114
179
 
115
180
  | Path | Lifecycle | Role |
116
181
  | ---------------------- | ------------- | ------------------------------------------------ |
@@ -122,6 +187,10 @@ personal and employer repos. See [the fleet manifest](#the-fleet-manifest-experi
122
187
  The committed files are written to be publishable: the baseline records `"."` as its scan root, never
123
188
  your absolute machine path. Only the gitignored cache keeps the real one.
124
189
 
190
+ Formatter interop: if your Prettier (or similar formatter) checks JSON, add `.etymd` to
191
+ `.prettierignore` — Etymd writes its own JSON style, and a format gate fighting the ledger is
192
+ noise (this repo does exactly that).
193
+
125
194
  ### `.etymd/config.json` (optional)
126
195
 
127
196
  Every key is optional; omit the file entirely and the defaults below apply.
@@ -130,7 +199,7 @@ Every key is optional; omit the file entirely and the defaults below apply.
130
199
  {
131
200
  "instructions": {
132
201
  // Audit these too — files detection would not find on its own.
133
- "include": ["design/**/*.md"],
202
+ "include": ["docs/handbook/**/*.md"],
134
203
  // Leave these out. The classic case: a fork that inherits upstream's skills
135
204
  // and will never fix them, but must keep its OWN instruction layer honest.
136
205
  "exclude": [".claude/skills/**"],
@@ -139,23 +208,134 @@ Every key is optional; omit the file entirely and the defaults below apply.
139
208
  "perFileWords": 4000, // extraction candidate above this
140
209
  "totalWords": 8000, // always-loaded footprint budget
141
210
  },
211
+ "gates": {
212
+ // What `etymd gates` generates. Written for you on first run from what the scan
213
+ // finds — edit it here rather than editing the generated hook, so the next run
214
+ // agrees with you instead of arguing.
215
+ "commands": ["typecheck", "lint"], // pre-push steps, in order
216
+ "failOn": "risk", // audit tier that fails the push: risk | gap | polish
217
+ "publishGate": true, // screen the published artifact
218
+ "allowWriting": [], // commands allowed into a gate despite writing
219
+ // Why a value here is what it is. Each key mirrors the field it explains, so the
220
+ // note says what it refers to instead of sitting near it and hoping. Etymd keeps
221
+ // these, and DROPS one whose field it changes — a reason attached to a value it no
222
+ // longer explains is worse than no reason at all.
223
+ "_why": { "failOn": "no build and no tests here; only docs drift can fire" },
224
+ },
142
225
  }
143
226
  ```
144
227
 
145
228
  Globs are repo-relative: `*` within a path segment, `**` across segments, `?` one character. A
146
229
  pattern with no wildcard is a **path prefix**, so `.claude/skills` covers everything beneath it.
147
230
 
148
- Narrowing an audit can hide findings, so etymd never lets it happen quietly: **every excluded file
149
- is counted and named in the lens disclosures**, and a config that fails to parse is reported as a
150
- disclosure rather than silently falling back to defaults.
231
+ Narrowing an audit can hide findings, so Etymd never lets it happen quietly. Honesty is
232
+ structural: **every excluded file is counted and named in the lens disclosures**, and a config
233
+ that fails to parse is reported as a disclosure rather than silently falling back to defaults.
234
+
235
+ ## In CI
236
+
237
+ The gate is one command:
238
+
239
+ ```bash
240
+ npx etymd audit --no-ledger --fail-on risk
241
+ ```
242
+
243
+ Exit-code contract: without `--fail-on`, `audit` reports and exits 0 no matter what it found.
244
+ With `--fail-on <tier>` (`risk` | `gap` | `polish`) it exits non-zero when any finding at or
245
+ above that tier exists — so `--fail-on risk` blocks on risks only, `--fail-on polish` blocks on
246
+ everything. `--no-ledger` keeps the CI run read-only: the throwaway checkout is never written.
247
+
248
+ The ledger and baseline are not CI by-products — they are **committed, reviewable state**,
249
+ updated locally and read in CI. A dismissal (with its reason), an accepted finding, a baseline
250
+ refresh after an intentional restructure: each lands in `.etymd/` and shows up in the pull
251
+ request diff like any other change. Keep `.etymd` out of your formatter's reach (see
252
+ [the files Etymd keeps](#the-files-etymd-keeps)).
253
+
254
+ A check that runs only in CI is itself a finding: the failure surfaces after the agent finished.
255
+ `etymd gates` installs the local pre-commit / pre-push mirror built from your own check scripts,
256
+ and the `gate-integrity` lens flags whatever still runs in CI alone.
257
+
258
+ ### Your own checks, beside the generated ones
259
+
260
+ Generated hooks are overwritten on every `etymd gates` run, so nothing hand-written belongs in
261
+ them. Each one calls a companion instead — `.githooks/pre-commit.local`, `commit-msg.local`,
262
+ `pre-push.local` — that etymd **never reads, writes, or regenerates**. Make it executable and it
263
+ runs; a non-zero exit stops the commit or push exactly as the generated checks do.
264
+
265
+ > **Commit the companion, and check your `.gitignore` first.** A `*.local` rule — common for env
266
+ > files, and shipped by some framework templates — silently swallows these too. The guard then
267
+ > works on the machine that wrote it and is absent for everyone who clones, which looks identical
268
+ > to having no guard at all. Add `!.githooks/*.local` if that rule exists.
269
+
270
+ ```sh
271
+ cat > .githooks/pre-commit.local <<'EOF'
272
+ #!/usr/bin/env sh
273
+ # Whatever this project needs — etymd will not touch this file.
274
+ ./scripts/check-changelog.sh || exit 1
275
+ EOF
276
+ chmod +x .githooks/pre-commit.local
277
+ ```
278
+
279
+ Two files, two owners. The generated half stays byte-identical to what the pack produces, which
280
+ is what lets drift detection say something precise: a difference there means the _managed_ part
281
+ was edited or went stale, never that you added a check of your own. Delete the companion and its
282
+ checks stop running — that is what deleting a file means, and etymd does not police a file it
283
+ does not own.
284
+
285
+ ### The content screen (`etymd screen`)
286
+
287
+ A separate question from "are the instructions true?": **does this repo carry text that must
288
+ never be published?** Absolute home paths, an employer's name, an internal hostname, an account
289
+ identifier — permanent the moment they are committed, because publishing exposes all history,
290
+ not the current tree.
291
+
292
+ Etymd ships the mechanism and **no patterns, ever**. The strings worth screening for are
293
+ themselves the sensitive material, so a built-in list would be useless to everyone else and a
294
+ leak for whoever wrote it. You supply a pattern file (one regex or literal per line, `#` for
295
+ comments) at `~/.config/etymd/screen-patterns` or via `--patterns`. Without one the command is
296
+ inert and says so — it never reports "clean" for a check it did not run.
297
+
298
+ `etymd gates` wires it into four doors, because a leak walks through whichever is unguarded:
299
+
300
+ | door | scope | what only it can catch |
301
+ | ---------------- | ------------------- | ------------------------------------------------------------- |
302
+ | `pre-commit` | staged file bytes | the ordinary case, at the cheapest moment to fix |
303
+ | `commit-msg` | the message itself | the staged scan reads file bytes and never sees the message |
304
+ | `pre-push` | every tracked file | anything committed with `--no-verify`, or merged in from else |
305
+ | `prepublishOnly` | the packed artifact | **a gitignored file that still ships** — see below |
306
+
307
+ That last door exists because the first three share a blind spot: they all answer "what is in
308
+ the repository?". `npm` and `vsce` do not honour `.gitignore`, so a local cache file can be
309
+ packaged into a published release while every git-scoped check passes forever.
310
+
311
+ Every generated hook resolves the screener at run time and **no-ops when it is absent**, so the
312
+ same hook file is safe to commit to a public repo: it carries no patterns and imposes no policy
313
+ on anyone who clones it. A deliberate exception is marked inline with `allow-published-string`,
314
+ visible in the diff rather than hidden in an allowlist.
315
+
316
+ Modeled on this repo's own workflow (Etymd guards its own instructions with Etymd — its CI runs
317
+ the same gate against its own freshly built CLI):
318
+
319
+ ```yaml
320
+ steps:
321
+ - uses: actions/checkout@v4
322
+ - uses: actions/setup-node@v4
323
+ with:
324
+ node-version: 20
325
+ cache: npm
326
+ - run: npm ci
327
+ - run: npx etymd audit --no-ledger --fail-on risk
328
+ ```
151
329
 
152
330
  ## The fleet manifest (EXPERIMENTAL)
153
331
 
154
332
  `etymd fleet` extends the one objective across every repository you work in — your fleet of
155
333
  **repositories**, not a fleet of agents. The manifest, `registry.json`, is itself an
156
- agent-context file: claims about your fleet (what exists, where, under which profile). It rots
157
- like any AGENTS.md does, and `etymd fleet` keeps it true. Design record:
158
- [`docs/design/004-fleet-truth-guard.md`](docs/design/004-fleet-truth-guard.md). Both the
334
+ agent-context file: claims about your fleet (what exists, where, under which **profile** — the
335
+ side of the wall an entry belongs to, `personal` or `corp`; the **wall** is the placement
336
+ boundary between personal and employer content that the sweep polices). It rots like any
337
+ AGENTS.md does, and `etymd fleet` keeps it true. Decision record:
338
+ [`docs/decisions/004-fleet-truth-guard.md`](docs/decisions/004-fleet-truth-guard.md). Both the
159
339
  registry schema and the fleet `--json` schema are **experimental through 0.2.x**.
160
340
 
161
341
  Two files beside each other — the split is the privacy model:
@@ -166,13 +346,21 @@ Two files beside each other — the split is the privacy model:
166
346
  {
167
347
  "registryVersion": 1,
168
348
  "root": "~/projects", // ~ expands on the consumer side — never a machine home
349
+ "orientation": { "root": "north" }, // optional: the entry every other entry is guided by
169
350
  "projects": [
170
- { "name": "web-app", "kind": "repo", "profile": "personal", "path": "web-app" },
351
+ {
352
+ "name": "web-app",
353
+ "kind": "repo",
354
+ "profile": "personal",
355
+ "path": "web-app",
356
+ "trust": "private",
357
+ },
171
358
  {
172
359
  "name": "notes",
173
360
  "kind": "docs",
174
361
  "profile": "personal",
175
362
  "path": "notes",
363
+ "trust": "private", // mandatory on every non-corp entry — see below
176
364
  "staleAfterDays": 45, // per-entry freshness window
177
365
  "contract": { "state": "STATUS.md" }, // native conventions register, never migrate
178
366
  },
@@ -194,7 +382,7 @@ Two files beside each other — the split is the privacy model:
194
382
 
195
383
  ```jsonc
196
384
  {
197
- "machineProfile": "corp", // "personal" resolves corp entries disclosed-absent
385
+ "machineProfile": "corp", // which profile this machine resolves; "personal" resolves corp entries disclosed-absent
198
386
  "root": "~/projects", // optional per-machine root override
199
387
  "dirs": { "c-one": "~/projects/real-corp-dir" },
200
388
  "labels": { "c-one": "real-corp-dir" },
@@ -202,6 +390,26 @@ Two files beside each other — the split is the privacy model:
202
390
  }
203
391
  ```
204
392
 
393
+ Two fields the scan can never derive, so the manifest must declare them:
394
+
395
+ - **`trust` — mandatory on every non-corp entry** (`public-repo` | `public-bound` | `private`).
396
+ It is a _safety predicate_, not a label: it decides whether content screening applies, so an
397
+ absent value is reported (`fleet check` flags it), never read as a silent `private`.
398
+ `public-bound` means private today, plausibly public later — screened exactly as hard as
399
+ public, because publishing exposes _all_ history: the scrub has to precede the first commit,
400
+ not the visibility flip. A value outside the vocabulary is flagged rather than coerced, so a
401
+ typo can never quietly disable screening. Corp entries omit it — `profile: "corp"` already
402
+ implies the answer.
403
+ - **`orientation.root` — optional, declared once.** Names the one entry every other entry is
404
+ guided by. Declared at the manifest level rather than repeated per entry, because a per-entry
405
+ link carries no information and can be forgotten: hoisting it makes an unoriented project
406
+ unrepresentable instead of merely detectable. Fleets without an orientation root omit the
407
+ block — etymd never assumes one.
408
+
409
+ `etymd fleet add <dir>` is the gate that keeps both true: it scans the project, prompts for what
410
+ no scan can derive, and **refuses to write an incomplete entry**. Non-interactive runs (`--yes`,
411
+ CI) must pass every mandatory value as a flag — there is deliberately no default.
412
+
205
413
  How the sweep behaves:
206
414
 
207
415
  - **Read-only by default, everywhere.** The sweep never creates `.etymd` anywhere.
@@ -212,17 +420,24 @@ How the sweep behaves:
212
420
  - **Deltas.** Each sweep compares against `last.fleet.json` stored beside the manifest and
213
421
  renders `Δ +new −resolved` per project. Add `*.fleet.json` to the manifest repo's
214
422
  `.gitignore` — sweep output is local-only and never tracked.
423
+ - **Recurring classes.** A finding class open in two or more projects renders as its own section
424
+ of class-fix candidates (worst tier first) — the sweep asking "repo bug or fleet bug?", a
425
+ fleet-level lesson no per-repo audit can see. The sweep only groups; the class vocabulary is
426
+ minted by the engine's lenses.
427
+ - **Declared absence is honored.** An entry whose contract declares `"placement": "none"` states
428
+ that instruction files are legitimately absent in that project — the sweep drops its
429
+ missing-contract finding instead of re-reporting a decision every run. Absence disclosed on
430
+ purpose is a state, not a gap.
215
431
  - **Wall checks.** Corp contract files found inside a corp worktree, unregistered checkouts
216
432
  under the fleet root whose remotes match `corpHosts`, tracked `/Users/` paths in the manifest
217
- repo, private needles (labels, dir names, hosts) inside `trust: "public-repo"` entries, and
218
- corp-host commit emails on personal entries — each a risk finding; each check that cannot run
219
- is disclosed.
433
+ repo, private **needles** — the identifiers the local file holds (labels, dir names, hosts) —
434
+ inside `trust: "public-repo"` entries, and corp-host commit emails on personal entries — each
435
+ a risk finding; each check that cannot run is disclosed.
220
436
  - **No global pointer.** `--manifest` is required unless the cwd holds `registry.json` — there
221
437
  is deliberately no env var and no home-directory pointer.
222
438
 
223
- Interop note: if a repo's Prettier (or similar formatter) checks JSON, add `.etymd` to its
224
- `.prettierignore` — etymd writes its own JSON style, and a format gate fighting the ledger is
225
- noise (this repo does exactly that).
439
+ Formatter interop for the `.etymd` state the sweep resolves: same rule as everywhere — see
440
+ [the files Etymd keeps](#the-files-etymd-keeps).
226
441
 
227
442
  ## Programmatic use
228
443
 
@@ -235,7 +450,7 @@ console.log(audit.findings) // one schema: claim · evidence · why · action ·
235
450
 
236
451
  ## The corpus (how this is validated)
237
452
 
238
- etymd is developed against a corpus of real sibling repos rather than fixtures alone —
453
+ Etymd is developed against a corpus of real sibling repos rather than fixtures alone —
239
454
  [`sources.json`](sources.json) lists them by shape. Every heuristic here exists because a real
240
455
  repo proved the previous one wrong, and each skip class in the truth lens is a false positive that
241
456
  a corpus run caught.
@@ -248,13 +463,13 @@ mapping each name to its sibling directory:
248
463
  { "dirs": { "nx-monorepo": "my-monorepo-checkout" } }
249
464
  ```
250
465
 
251
- Each value is resolved as a sibling of the etymd checkout.
466
+ Each value is resolved as a sibling of the Etymd checkout.
252
467
 
253
468
  Without it — on a fresh clone or in CI — those suites skip cleanly and the rest still run.
254
469
 
255
- ## Design record & roadmap
470
+ ## Decision record & roadmap
256
471
 
257
- [`docs/design/`](docs/design/) — 001 founding · 002 foundation re-lock · **003 the truth-guard
472
+ [`docs/decisions/`](docs/decisions/) — 001 founding · 002 foundation re-lock · **003 the truth-guard
258
473
  pivot** (the current identity; includes the state-of-the-field investigation it rests on) ·
259
474
  **004 fleet mode** (the truth guard across your repositories).
260
475
  [`ROADMAP.md`](ROADMAP.md) — what's now / next / later, the pre-publish checklist, and the
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { scanProject, PACK_VERSION } from './chunk-E2Q7WFPN.js';
3
- import { VERSION } from './chunk-C4Z6NXDO.js';
4
- import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-FZSIOCOZ.js';
5
- import { section, theme, print, renderBaselineDrift, glyph } from './chunk-LBRNZZZF.js';
2
+ import { section, theme, print, renderBaselineDrift, glyph } from './chunk-TADKOW6P.js';
3
+ import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-R74SJ7HS.js';
4
+ import { scanProject } from './chunk-MKYPLNLH.js';
5
+ import { VERSION } from './chunk-NFENQ3IK.js';
6
+ import { PACK_VERSION } from './chunk-CRJHIXJH.js';
7
+ import './chunk-YLNQQZ5F.js';
6
8
 
7
9
  // src/commands/approve.ts
8
10
  async function run(opts) {
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ export { run } from './chunk-XTAX4YNT.js';
3
+ import './chunk-V3DPLOOM.js';
4
+ import './chunk-LT67FU2Y.js';
5
+ import './chunk-WWBF4Y7K.js';
6
+ import './chunk-NMZ3RHWW.js';
7
+ import './chunk-TADKOW6P.js';
8
+ import './chunk-R74SJ7HS.js';
9
+ import './chunk-MKYPLNLH.js';
10
+ import './chunk-NFENQ3IK.js';
11
+ import './chunk-CRJHIXJH.js';
12
+ import './chunk-YLNQQZ5F.js';
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { scanProject } from './chunk-E2Q7WFPN.js';
3
- import './chunk-C4Z6NXDO.js';
4
- import { ETYMD_DIR } from './chunk-FZSIOCOZ.js';
5
- import { print, theme } from './chunk-LBRNZZZF.js';
2
+ import { print, theme } from './chunk-TADKOW6P.js';
3
+ import { ETYMD_DIR } from './chunk-R74SJ7HS.js';
4
+ import { scanProject } from './chunk-MKYPLNLH.js';
5
+ import './chunk-NFENQ3IK.js';
6
+ import './chunk-CRJHIXJH.js';
7
+ import './chunk-YLNQQZ5F.js';
6
8
  import { promises } from 'node:fs';
7
9
  import path from 'node:path';
8
10