etymd 0.1.0 → 0.2.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.
- package/CHANGELOG.md +14 -1
- package/README.md +111 -12
- package/dist/{approve-WM6OJHQG.js → approve-EJJDCNYV.js} +4 -4
- package/dist/audit-ATHB6FHN.js +9 -0
- package/dist/{brief-2MPNTITQ.js → brief-GFR3YOZT.js} +4 -4
- package/dist/{chunk-I566LDX7.js → chunk-3LBIBVX3.js} +10 -3
- package/dist/chunk-3WOORFOW.js +60 -0
- package/dist/{chunk-WVYKYPKS.js → chunk-ASKJFMSX.js} +28 -1
- package/dist/{ledger-YFTYCOIC.js → chunk-B4FYQE7S.js} +2 -2
- package/dist/{chunk-SMM2TW35.js → chunk-DJ2DDFDK.js} +1 -1
- package/dist/{chunk-U33XLT5I.js → chunk-JQHGV74L.js} +1 -1
- package/dist/{chunk-TCWKIMCU.js → chunk-WNMA2FYM.js} +75 -4
- package/dist/{chunk-FXL4554F.js → chunk-WQXMJUIA.js} +2 -2
- package/dist/{chunk-5D7WNJ6N.js → chunk-XGH2DTW4.js} +251 -82
- package/dist/{chunk-N64ZTBCW.js → chunk-XW5XQN23.js} +1 -1
- package/dist/cli.js +73 -13
- package/dist/{context-TT5H76LA.js → context-5HAWI2RD.js} +3 -3
- package/dist/doctor-WTYZSJ5X.js +16 -0
- package/dist/fleet-2GXLNLJE.js +973 -0
- package/dist/{gates-KPDEUC36.js → gates-LD276XN5.js} +4 -4
- package/dist/index.d.ts +214 -3
- package/dist/index.js +1038 -13
- package/dist/{init-V627UXKU.js → init-WLYWEWRQ.js} +5 -5
- package/dist/ledger-ENFHLNOG.js +4 -0
- package/dist/{scan-YL7JHLUA.js → scan-62KGD2V3.js} +4 -4
- package/package.json +1 -1
- package/dist/audit-OHOSWOWA.js +0 -8
- package/dist/doctor-NNRJTNIY.js +0 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# etymd
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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).
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
- The manifest loader (`src/core/fleet.ts`) resolves both the fleet registry pair (`registry.json` + gitignored `registry.local.json`) and the legacy corpus pair (`sources.json` + `sources.local.json`); corp entries are opaque aliases resolved only through the local file, and every resolution failure is disclosed, never silently skipped.
|
|
12
|
+
- Persistence invariants, pinned by tests: the sweep never creates `.etymd` anywhere; `--persist-ledgers` only persists into personal repos that already opted in; corp worktrees take zero writes under every flag combination — corp findings persist (and stay dismissible) at `<manifestDir>/corp/<name>/.etymd/`; zero corp-resolved content under the manifest repo's tracked paths.
|
|
13
|
+
- Fleet-scope wall findings (lens id `fleet-manifest`): corp contract files inside a corp worktree, unregistered corp-remote checkouts under the fleet root, tracked `/Users/` paths in the manifest's own repo, private needles inside `trust: "public-repo"` entries, corp-host commit emails on personal entries.
|
|
14
|
+
- Fork-aware freshness: entries with `upstream` are dated on fork-authored commits only (`HEAD --not --remotes=<upstream>`), with a disclosed fallback when the remote is absent.
|
|
15
|
+
|
|
3
16
|
## 0.1.0 — 2026-07-31
|
|
4
17
|
|
|
5
18
|
First public release.
|
|
@@ -22,7 +35,7 @@ rationale are recorded in `docs/design/003-truth-guard-pivot.md`.)
|
|
|
22
35
|
absent because nobody looked, not because it was fixed. The ledger holds those entries open
|
|
23
36
|
(`lastSeen` untouched) and the diff reports them as held rather than folding them into
|
|
24
37
|
"N resolved".
|
|
25
|
-
- **`init` baselines the repo it leaves behind.** It used to approve the scan taken
|
|
38
|
+
- **`init` baselines the repo it leaves behind.** It used to approve the scan taken _before_ its
|
|
26
39
|
own scaffold, so the first baseline recorded AGENTS.md and the hooks as absent — and deleting
|
|
27
40
|
them later never registered as drift, which is the baseline's whole job. It now re-scans after
|
|
28
41
|
writing.
|
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# etymd
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/etymd)
|
|
4
|
+
[](https://github.com/triartleet/etymd/actions/workflows/ci.yml)
|
|
5
|
+
|
|
3
6
|
**Keep your agent instructions true.**
|
|
4
7
|
|
|
5
8
|
Your `AGENTS.md` is the interface between your team and every coding agent — and it rots silently.
|
|
@@ -45,8 +48,7 @@ npx etymd audit # verify every instruction claim against the repo
|
|
|
45
48
|
npx etymd audit --fail-on risk # the CI gate
|
|
46
49
|
```
|
|
47
50
|
|
|
48
|
-
Requires Node ≥ 18.17.
|
|
49
|
-
node dist/cli.js …`) until the first npm release.
|
|
51
|
+
Requires Node ≥ 18.17. On npm since v0.1.0 — `npx etymd` just works.
|
|
50
52
|
|
|
51
53
|
## What it checks
|
|
52
54
|
|
|
@@ -72,17 +74,38 @@ installed but unwired). `allow_failure` jobs count as advisory, never as gates.
|
|
|
72
74
|
`alwaysApply` Cursor rules count), flagging files worth extracting into on-demand skills. Context
|
|
73
75
|
is the dominant cost of the loop; a lean contract is a correctness feature.
|
|
74
76
|
|
|
77
|
+
**`state-freshness`** — the layer that claims "this describes now" (`PROJECT_CONTEXT.md`,
|
|
78
|
+
`DECISIONS.md`, ADR dirs), judged by git committer dates only, never mtime. Staleness is
|
|
79
|
+
_relative_ — a state doc is stale only when the repo moved past it, so a dormant repo's old
|
|
80
|
+
state is current; a tracked file with uncommitted edits is treated fresh-now (the refresh is
|
|
81
|
+
already on disk) and disclosed. Decisions records get format checks (`Scope:` presence, a
|
|
82
|
+
`Revisit:` date that, once past, becomes a finding) — opt in by adding the literal marker
|
|
83
|
+
`<!-- decisions-format: 1 -->` anywhere in the file; forward-only, never retroactive. Duplicate
|
|
84
|
+
or out-of-order `D-NNN` ids are flagged with a rename action even without the marker — an
|
|
85
|
+
append race is a defect in the file's own convention, not a format opinion.
|
|
86
|
+
|
|
87
|
+
**`fleet-manifest`** (via `etymd fleet`) — one truth guard across every repo you registered:
|
|
88
|
+
per-repo audits plus checks on the fleet manifest itself and on the placement wall between
|
|
89
|
+
personal and employer repos. See [the fleet manifest](#the-fleet-manifest-experimental) below.
|
|
90
|
+
|
|
75
91
|
## Commands
|
|
76
92
|
|
|
77
|
-
| Command
|
|
78
|
-
|
|
|
79
|
-
| `etymd audit`
|
|
80
|
-
| `etymd init`
|
|
81
|
-
| `etymd doctor`
|
|
82
|
-
| `etymd context`
|
|
83
|
-
| `etymd gates`
|
|
84
|
-
| `etymd scan`
|
|
85
|
-
| `etymd brief`
|
|
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. |
|
|
86
109
|
|
|
87
110
|
`--cwd <dir>` targets another directory. Read-only probing of any repo leaves **zero trace**
|
|
88
111
|
(`audit --no-ledger` writes nothing).
|
|
@@ -126,6 +149,81 @@ Narrowing an audit can hide findings, so etymd never lets it happen quietly: **e
|
|
|
126
149
|
is counted and named in the lens disclosures**, and a config that fails to parse is reported as a
|
|
127
150
|
disclosure rather than silently falling back to defaults.
|
|
128
151
|
|
|
152
|
+
## The fleet manifest (EXPERIMENTAL)
|
|
153
|
+
|
|
154
|
+
`etymd fleet` extends the one objective across every repository you work in — your fleet of
|
|
155
|
+
**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
|
|
159
|
+
registry schema and the fleet `--json` schema are **experimental through 0.2.x**.
|
|
160
|
+
|
|
161
|
+
Two files beside each other — the split is the privacy model:
|
|
162
|
+
|
|
163
|
+
`registry.json` (tracked; safe to publish by construction):
|
|
164
|
+
|
|
165
|
+
```jsonc
|
|
166
|
+
{
|
|
167
|
+
"registryVersion": 1,
|
|
168
|
+
"root": "~/projects", // ~ expands on the consumer side — never a machine home
|
|
169
|
+
"projects": [
|
|
170
|
+
{ "name": "web-app", "kind": "repo", "profile": "personal", "path": "web-app" },
|
|
171
|
+
{
|
|
172
|
+
"name": "notes",
|
|
173
|
+
"kind": "docs",
|
|
174
|
+
"profile": "personal",
|
|
175
|
+
"path": "notes",
|
|
176
|
+
"staleAfterDays": 45, // per-entry freshness window
|
|
177
|
+
"contract": { "state": "STATUS.md" }, // native conventions register, never migrate
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"name": "my-fork",
|
|
181
|
+
"kind": "tool",
|
|
182
|
+
"profile": "personal",
|
|
183
|
+
"path": "my-fork",
|
|
184
|
+
"upstream": "origin", // freshness measured on fork-authored commits only
|
|
185
|
+
"trust": "public-repo", // hygiene needles apply (see below)
|
|
186
|
+
},
|
|
187
|
+
// Corp entries: opaque alias, private, NO path — real dirs live only in the local file.
|
|
188
|
+
{ "name": "c-one", "kind": "repo", "profile": "corp", "private": true, "staleAfterDays": 45 },
|
|
189
|
+
],
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`registry.local.json` (gitignored; this machine's facts — each one an identifier you don't ship):
|
|
194
|
+
|
|
195
|
+
```jsonc
|
|
196
|
+
{
|
|
197
|
+
"machineProfile": "corp", // "personal" resolves corp entries disclosed-absent
|
|
198
|
+
"root": "~/projects", // optional per-machine root override
|
|
199
|
+
"dirs": { "c-one": "~/projects/real-corp-dir" },
|
|
200
|
+
"labels": { "c-one": "real-corp-dir" },
|
|
201
|
+
"corpHosts": ["git.example-corp.com"],
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
How the sweep behaves:
|
|
206
|
+
|
|
207
|
+
- **Read-only by default, everywhere.** The sweep never creates `.etymd` anywhere.
|
|
208
|
+
`--persist-ledgers` persists only into personal repos that already opted in, and a **corp
|
|
209
|
+
worktree is never written** — regardless of flags, even if a stray `.etymd` exists inside it
|
|
210
|
+
(pinned by test). Corp findings stay dismissible: their ledger lives at
|
|
211
|
+
`<manifest-dir>/corp/<name>/.etymd/`, beside the manifest.
|
|
212
|
+
- **Deltas.** Each sweep compares against `last.fleet.json` stored beside the manifest and
|
|
213
|
+
renders `Δ +new −resolved` per project. Add `*.fleet.json` to the manifest repo's
|
|
214
|
+
`.gitignore` — sweep output is local-only and never tracked.
|
|
215
|
+
- **Wall checks.** Corp contract files found inside a corp worktree, unregistered checkouts
|
|
216
|
+
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.
|
|
220
|
+
- **No global pointer.** `--manifest` is required unless the cwd holds `registry.json` — there
|
|
221
|
+
is deliberately no env var and no home-directory pointer.
|
|
222
|
+
|
|
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).
|
|
226
|
+
|
|
129
227
|
## Programmatic use
|
|
130
228
|
|
|
131
229
|
```ts
|
|
@@ -157,7 +255,8 @@ Without it — on a fresh clone or in CI — those suites skip cleanly and the r
|
|
|
157
255
|
## Design record & roadmap
|
|
158
256
|
|
|
159
257
|
[`docs/design/`](docs/design/) — 001 founding · 002 foundation re-lock · **003 the truth-guard
|
|
160
|
-
pivot** (the current identity; includes the state-of-the-field investigation it rests on)
|
|
258
|
+
pivot** (the current identity; includes the state-of-the-field investigation it rests on) ·
|
|
259
|
+
**004 fleet mode** (the truth guard across your repositories).
|
|
161
260
|
[`ROADMAP.md`](ROADMAP.md) — what's now / next / later, the pre-publish checklist, and the
|
|
162
261
|
accepted heuristic trade-offs.
|
|
163
262
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { scanProject, PACK_VERSION } from './chunk-
|
|
3
|
-
import { VERSION } from './chunk-
|
|
4
|
-
import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-
|
|
5
|
-
import { section, theme, print, renderBaselineDrift, glyph } from './chunk-
|
|
2
|
+
import { scanProject, PACK_VERSION } from './chunk-WNMA2FYM.js';
|
|
3
|
+
import { VERSION } from './chunk-JQHGV74L.js';
|
|
4
|
+
import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-DJ2DDFDK.js';
|
|
5
|
+
import { section, theme, print, renderBaselineDrift, glyph } from './chunk-ASKJFMSX.js';
|
|
6
6
|
|
|
7
7
|
// src/commands/approve.ts
|
|
8
8
|
async function run(opts) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export { run } from './chunk-3WOORFOW.js';
|
|
3
|
+
import './chunk-XGH2DTW4.js';
|
|
4
|
+
import './chunk-XW5XQN23.js';
|
|
5
|
+
import './chunk-3LBIBVX3.js';
|
|
6
|
+
import './chunk-WNMA2FYM.js';
|
|
7
|
+
import './chunk-JQHGV74L.js';
|
|
8
|
+
import './chunk-DJ2DDFDK.js';
|
|
9
|
+
import './chunk-ASKJFMSX.js';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { scanProject } from './chunk-
|
|
3
|
-
import './chunk-
|
|
4
|
-
import { ETYMD_DIR } from './chunk-
|
|
5
|
-
import { print, theme } from './chunk-
|
|
2
|
+
import { scanProject } from './chunk-WNMA2FYM.js';
|
|
3
|
+
import './chunk-JQHGV74L.js';
|
|
4
|
+
import { ETYMD_DIR } from './chunk-DJ2DDFDK.js';
|
|
5
|
+
import { print, theme } from './chunk-ASKJFMSX.js';
|
|
6
6
|
import { promises } from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { ETYMD_DIR } from './chunk-
|
|
3
|
-
import { pathExists, readText, wordCount, approxTokens } from './chunk-
|
|
2
|
+
import { ETYMD_DIR } from './chunk-DJ2DDFDK.js';
|
|
3
|
+
import { pathExists, readText, wordCount, approxTokens } from './chunk-ASKJFMSX.js';
|
|
4
4
|
import path2 from 'node:path';
|
|
5
5
|
import { promises } from 'node:fs';
|
|
6
6
|
|
|
7
7
|
var CONFIG_FILE = path2.join(ETYMD_DIR, "config.json");
|
|
8
8
|
var DEFAULT_CONFIG = {
|
|
9
9
|
instructions: { include: [], exclude: [] },
|
|
10
|
-
context: { perFileWords: 4e3, totalWords: 8e3 }
|
|
10
|
+
context: { perFileWords: 4e3, totalWords: 8e3 },
|
|
11
|
+
state: { staleAfterDays: 30, maxChars: 9500 }
|
|
11
12
|
};
|
|
12
13
|
function configPath(root) {
|
|
13
14
|
return path2.join(root, CONFIG_FILE);
|
|
@@ -51,6 +52,7 @@ async function readConfig(root) {
|
|
|
51
52
|
const obj = parsed;
|
|
52
53
|
const instructions = obj.instructions ?? {};
|
|
53
54
|
const context = obj.context ?? {};
|
|
55
|
+
const state = obj.state ?? {};
|
|
54
56
|
return {
|
|
55
57
|
present: true,
|
|
56
58
|
problems,
|
|
@@ -62,12 +64,17 @@ async function readConfig(root) {
|
|
|
62
64
|
context: {
|
|
63
65
|
perFileWords: readBudget(context.perFileWords, "context.perFileWords", problems) ?? DEFAULT_CONFIG.context.perFileWords,
|
|
64
66
|
totalWords: readBudget(context.totalWords, "context.totalWords", problems) ?? DEFAULT_CONFIG.context.totalWords
|
|
67
|
+
},
|
|
68
|
+
state: {
|
|
69
|
+
staleAfterDays: readBudget(state.staleAfterDays, "state.staleAfterDays", problems) ?? DEFAULT_CONFIG.state.staleAfterDays,
|
|
70
|
+
maxChars: readBudget(state.maxChars, "state.maxChars", problems) ?? DEFAULT_CONFIG.state.maxChars
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
};
|
|
68
74
|
}
|
|
69
75
|
var ALWAYS_LOADED = [
|
|
70
76
|
{ path: "AGENTS.md", role: "operating contract" },
|
|
77
|
+
{ path: "PROJECT_CONTEXT.md", role: "ground-truth state" },
|
|
71
78
|
{ path: "CLAUDE.md", role: "Claude Code pointer" },
|
|
72
79
|
{ path: "GEMINI.md", role: "Gemini pointer" },
|
|
73
80
|
{ path: ".github/copilot-instructions.md", role: "Copilot instructions" },
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-XGH2DTW4.js';
|
|
3
|
+
import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-ASKJFMSX.js';
|
|
4
|
+
|
|
5
|
+
// src/commands/audit.ts
|
|
6
|
+
async function run(opts) {
|
|
7
|
+
const failOn = opts.failOn === void 0 ? void 0 : parseFailOnTier(opts.failOn);
|
|
8
|
+
const result = await runAudit(opts.cwd, {
|
|
9
|
+
kind: opts.truth ? "truth" : void 0,
|
|
10
|
+
lensIds: opts.lens ? [opts.lens] : void 0,
|
|
11
|
+
persistLedger: !opts.noLedger
|
|
12
|
+
});
|
|
13
|
+
if (failOn && meetsFailOn(result.findings, failOn)) {
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
}
|
|
16
|
+
if (opts.json) {
|
|
17
|
+
print(
|
|
18
|
+
JSON.stringify(
|
|
19
|
+
{
|
|
20
|
+
profile: result.profile,
|
|
21
|
+
baseline: result.baseline ? { packVersion: result.baseline.packVersion, approvedAt: result.baseline.approvedAt } : null,
|
|
22
|
+
reports: result.reports.map(({ findings: _findings, ...r }) => r),
|
|
23
|
+
findings: result.findings,
|
|
24
|
+
diff: {
|
|
25
|
+
new: result.diff.new.map((f) => f.id),
|
|
26
|
+
stillOpen: result.diff.stillOpen.map((f) => f.id),
|
|
27
|
+
regressed: result.diff.regressed.map((f) => f.id),
|
|
28
|
+
resolved: result.diff.resolved.map((e) => e.id),
|
|
29
|
+
dismissed: result.diff.dismissed.map((f) => f.id),
|
|
30
|
+
accepted: result.diff.accepted.map((f) => f.id)
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
null,
|
|
34
|
+
2
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
section(
|
|
40
|
+
`${opts.truth ? "Doctor \u2014 is this still true?" : "Audit"} ${theme.dim(`\xB7 ${result.facts.name} \xB7 ${result.profile} profile`)}`
|
|
41
|
+
);
|
|
42
|
+
if (!result.baseline && !opts.truth) {
|
|
43
|
+
print(
|
|
44
|
+
` ${theme.dim("no committed baseline \u2014 run `etymd init` to approve one; drift checks are limited")}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
renderLensCoverage(result.reports);
|
|
48
|
+
section(`Findings ${theme.dim("(ranked: risk \u2192 gap \u2192 polish, cheapest first)")}`);
|
|
49
|
+
renderFindings(result.findings);
|
|
50
|
+
renderLedgerDiff(result.diff);
|
|
51
|
+
const top = result.findings[0];
|
|
52
|
+
if (top) {
|
|
53
|
+
print();
|
|
54
|
+
print(
|
|
55
|
+
` ${theme.dim("next:")} ${theme.heading(top.claim)}${top.action ? theme.dim(` \u2014 ${top.action}`) : ""}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { run };
|
|
@@ -280,6 +280,33 @@ function renderLedgerDiff(diff) {
|
|
|
280
280
|
print(` ${theme.dim("since last audit:")} ${parts.join(theme.dim(" \xB7 "))}`);
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
|
+
function renderFleetRows(rows) {
|
|
284
|
+
if (!rows.length) {
|
|
285
|
+
print(` ${theme.dim("no entries matched \u2014 nothing swept")}`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const nameWidth = maxWidth(rows.map((r) => r.name));
|
|
289
|
+
const ageWidth = maxWidth(rows.map((r) => r.age));
|
|
290
|
+
const countStr = (c) => c.risk + c.gap + c.polish === 0 ? theme.ok("clean") : [
|
|
291
|
+
c.risk ? theme.bad(`${c.risk} risk`) : "",
|
|
292
|
+
c.gap ? theme.warn(`${c.gap} gap`) : "",
|
|
293
|
+
c.polish ? theme.dim(`${c.polish} polish`) : ""
|
|
294
|
+
].filter(Boolean).join(theme.dim(" \xB7 "));
|
|
295
|
+
const countsWidth = maxWidth(rows.filter((r) => !r.note).map((r) => countStr(r.counts)));
|
|
296
|
+
for (const r of rows) {
|
|
297
|
+
if (r.note) {
|
|
298
|
+
print(` ${pad(theme.info(r.name), nameWidth)} ${glyph.bad} ${theme.dim(r.note)}`);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
print(
|
|
302
|
+
` ${pad(theme.info(r.name), nameWidth)} ${pad(theme.dim(r.age), ageWidth)} ${pad(countStr(r.counts), countsWidth)} ${theme.dim(r.delta)}`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function renderFleetNotes(problems, disclosures) {
|
|
307
|
+
for (const p of problems) print(` ${glyph.bad} ${theme.bad(p)}`);
|
|
308
|
+
for (const d of disclosures) print(` ${theme.dim(`\u25E6 ${d}`)}`);
|
|
309
|
+
}
|
|
283
310
|
var STATUS_ORDER = ["regressed", "open", "accepted", "dismissed", "done"];
|
|
284
311
|
var STATUS_LABEL = {
|
|
285
312
|
regressed: theme.bad,
|
|
@@ -329,4 +356,4 @@ function renderLedger(ledger) {
|
|
|
329
356
|
}
|
|
330
357
|
}
|
|
331
358
|
|
|
332
|
-
export { approxTokens, git, glyph, isCiEnvironment, isDirectory, matchesAnyGlob, normalizeRelPath, pathExists, print, readJson, readText, renderBaselineDrift, renderContext, renderFacts, renderFindings, renderLedger, renderLedgerDiff, renderLensCoverage, renderPlan, section, theme, wordCount };
|
|
359
|
+
export { approxTokens, git, glyph, isCiEnvironment, isDirectory, matchesAnyGlob, normalizeRelPath, pathExists, print, readJson, readText, renderBaselineDrift, renderContext, renderFacts, renderFindings, renderFleetNotes, renderFleetRows, renderLedger, renderLedgerDiff, renderLensCoverage, renderPlan, section, theme, wordCount };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readLedger, resolveEntry, writeLedger } from './chunk-
|
|
3
|
-
import { print, renderLedger, glyph, theme } from './chunk-
|
|
2
|
+
import { readLedger, resolveEntry, writeLedger } from './chunk-XW5XQN23.js';
|
|
3
|
+
import { print, renderLedger, glyph, theme } from './chunk-ASKJFMSX.js';
|
|
4
4
|
|
|
5
5
|
// src/commands/ledger.ts
|
|
6
6
|
async function list(opts) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { VERSION } from './chunk-
|
|
3
|
-
import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-
|
|
2
|
+
import { VERSION } from './chunk-JQHGV74L.js';
|
|
3
|
+
import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-ASKJFMSX.js';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promises } from 'node:fs';
|
|
6
6
|
|
|
@@ -334,6 +334,19 @@ var ARTIFACT_SPECS = [
|
|
|
334
334
|
path: "PROJECT_CONTEXT.md",
|
|
335
335
|
kind: "state"
|
|
336
336
|
},
|
|
337
|
+
{
|
|
338
|
+
id: "decisions",
|
|
339
|
+
label: "DECISIONS.md (decision record)",
|
|
340
|
+
path: "DECISIONS.md",
|
|
341
|
+
kind: "decisions"
|
|
342
|
+
},
|
|
343
|
+
{ id: "adr-dir", label: "ADR directory (docs/adr)", path: "docs/adr", kind: "decisions" },
|
|
344
|
+
{
|
|
345
|
+
id: "decisions-dir",
|
|
346
|
+
label: "ADR directory (docs/decisions)",
|
|
347
|
+
path: "docs/decisions",
|
|
348
|
+
kind: "decisions"
|
|
349
|
+
},
|
|
337
350
|
{ id: "claude", label: "CLAUDE.md (Claude Code pointer)", path: "CLAUDE.md", kind: "adapter" },
|
|
338
351
|
{
|
|
339
352
|
id: "copilot",
|
|
@@ -379,12 +392,25 @@ var ARTIFACT_SPECS = [
|
|
|
379
392
|
}
|
|
380
393
|
];
|
|
381
394
|
async function detectArtifacts(root) {
|
|
382
|
-
|
|
395
|
+
const artifacts = await Promise.all(
|
|
383
396
|
ARTIFACT_SPECS.map(async (spec) => ({
|
|
384
397
|
...spec,
|
|
385
398
|
exists: await pathExists(path.join(root, spec.path))
|
|
386
399
|
}))
|
|
387
400
|
);
|
|
401
|
+
let adrFiles = false;
|
|
402
|
+
try {
|
|
403
|
+
adrFiles = (await promises.readdir(path.join(root, "docs"))).some((f) => /^\d{4}-.+\.md$/.test(f));
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
artifacts.push({
|
|
407
|
+
id: "adr-files",
|
|
408
|
+
label: "ADR files (docs/NNNN-*.md)",
|
|
409
|
+
path: "docs",
|
|
410
|
+
kind: "decisions",
|
|
411
|
+
exists: adrFiles
|
|
412
|
+
});
|
|
413
|
+
return artifacts;
|
|
388
414
|
}
|
|
389
415
|
async function walkTree(root) {
|
|
390
416
|
const state = { budget: 2e4, truncated: false };
|
|
@@ -449,7 +475,50 @@ function inferTicketKey(subjects) {
|
|
|
449
475
|
}
|
|
450
476
|
return bestCount >= 3 ? best : void 0;
|
|
451
477
|
}
|
|
452
|
-
async function
|
|
478
|
+
async function collectFreshness(abs, isRepo, artifacts, upstreamRemote) {
|
|
479
|
+
const dated = artifacts.filter((a) => a.exists && (a.kind === "state" || a.kind === "decisions"));
|
|
480
|
+
const allUnverifiable = (reason) => ({
|
|
481
|
+
artifacts: [],
|
|
482
|
+
unverifiable: dated.map((a) => ({ path: a.path, reason }))
|
|
483
|
+
});
|
|
484
|
+
if (!isRepo) return allUnverifiable("not a git repository \u2014 no committer dates to read");
|
|
485
|
+
if (await git(abs, ["rev-parse", "--is-shallow-repository"]) === "true") {
|
|
486
|
+
return allUnverifiable("shallow clone \u2014 history is incomplete, committer dates would lie");
|
|
487
|
+
}
|
|
488
|
+
const repoLastCommit = upstreamRemote ? await git(abs, ["log", "-1", "--format=%cI", "HEAD", "--not", `--remotes=${upstreamRemote}`]) : await git(abs, ["log", "-1", "--format=%cI"]);
|
|
489
|
+
if (!repoLastCommit) {
|
|
490
|
+
if (upstreamRemote) {
|
|
491
|
+
return allUnverifiable(
|
|
492
|
+
`no fork-authored commits \u2014 every commit is reachable from \`${upstreamRemote}\`, the fork has not moved`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
return allUnverifiable("repository has no commits yet");
|
|
496
|
+
}
|
|
497
|
+
const dirtyRaw = await git(abs, ["diff", "--name-only", "HEAD"]);
|
|
498
|
+
const dirtyPaths = dirtyRaw ? dirtyRaw.split("\n").filter(Boolean) : [];
|
|
499
|
+
const isDirty = (p) => dirtyPaths.some((d) => d === p || d.startsWith(`${p}/`));
|
|
500
|
+
const facts = { repoLastCommit, artifacts: [], unverifiable: [] };
|
|
501
|
+
await Promise.all(
|
|
502
|
+
dated.map(async (a) => {
|
|
503
|
+
const lastCommit = await git(abs, ["log", "-1", "--format=%cI", "--", a.path]);
|
|
504
|
+
if (!lastCommit) {
|
|
505
|
+
facts.unverifiable.push({ path: a.path, reason: "untracked \u2014 never committed" });
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
facts.artifacts.push({
|
|
509
|
+
artifactId: a.id,
|
|
510
|
+
path: a.path,
|
|
511
|
+
lastCommit,
|
|
512
|
+
commitsSince: Date.parse(repoLastCommit) > Date.parse(lastCommit),
|
|
513
|
+
...isDirty(a.path) ? { dirty: true } : {}
|
|
514
|
+
});
|
|
515
|
+
})
|
|
516
|
+
);
|
|
517
|
+
facts.artifacts.sort((x, y) => x.path.localeCompare(y.path));
|
|
518
|
+
facts.unverifiable.sort((x, y) => x.path.localeCompare(y.path));
|
|
519
|
+
return facts;
|
|
520
|
+
}
|
|
521
|
+
async function scanProject(root, opts = {}) {
|
|
453
522
|
const abs = path.resolve(root);
|
|
454
523
|
const rootPkg = await readJson(path.join(abs, "package.json"));
|
|
455
524
|
const [packageManager, workspace, ci, artifacts, tree, isRepo] = await Promise.all([
|
|
@@ -469,6 +538,7 @@ async function scanProject(root) {
|
|
|
469
538
|
]) : [null, null, null, null, null];
|
|
470
539
|
const hooksPath = hooksPathRaw ?? void 0;
|
|
471
540
|
const hooks = await detectHooks(abs, hooksPath, rootPkg);
|
|
541
|
+
const freshness = await collectFreshness(abs, isRepo, artifacts, opts.upstreamRemote);
|
|
472
542
|
const packages = workspace.packageGlobs.length ? await listWorkspacePackages(abs, workspace.packageGlobs) : [];
|
|
473
543
|
const recentAuthors = authorsRaw ? new Set(authorsRaw.split("\n").filter(Boolean)).size : void 0;
|
|
474
544
|
return {
|
|
@@ -495,6 +565,7 @@ async function scanProject(root) {
|
|
|
495
565
|
ci,
|
|
496
566
|
hooks,
|
|
497
567
|
artifacts,
|
|
568
|
+
freshness,
|
|
498
569
|
tree
|
|
499
570
|
};
|
|
500
571
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { PACK_VERSION } from './chunk-
|
|
3
|
-
import { pathExists, readText } from './chunk-
|
|
2
|
+
import { PACK_VERSION } from './chunk-WNMA2FYM.js';
|
|
3
|
+
import { pathExists, readText } from './chunk-ASKJFMSX.js';
|
|
4
4
|
import { promises } from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|