etymd 0.1.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 ADDED
@@ -0,0 +1,48 @@
1
+ # etymd
2
+
3
+ ## 0.1.0 — 2026-07-31
4
+
5
+ First public release.
6
+
7
+ The truth guard for agent instruction files — **keep your agent instructions true**. (Formerly
8
+ prototyped as "clothaid", a broader workflow installer; the pivot and its state-of-the-field
9
+ rationale are recorded in `docs/design/003-truth-guard-pivot.md`.)
10
+
11
+ - `etymd audit` — verify every instruction claim against the actual repo, through three lenses:
12
+ - **instruction-truth**: command claims vs `package.json` scripts, path claims vs the tree,
13
+ package-manager consistency, cross-reference integrity, and drift vs the committed baseline —
14
+ over AGENTS.md, CLAUDE.md, GEMINI.md, Copilot instructions, Cursor rules, and skills.
15
+ - **gate-integrity**: the CI ↔ local gate inventory (GitLab incl. `!reference`/includes/
16
+ script-less jobs, GitHub, husky modern + v3, lint-staged) with honesty rules — advisory jobs
17
+ are never gates, unseen templates are disclosed, server-side thresholds named as such.
18
+ - **context-economy**: the always-loaded footprint in words/tokens; extraction candidates.
19
+ - Ranked findings (risk → gap → polish) with evidence · a **committed ledger** (resolved /
20
+ regressed / dismissed-never-resurfaces) · lens-coverage reporting · `--fail-on <tier>` for CI.
21
+ - **Excluding a file never resolves its tracked findings.** A finding missing from a scoped run is
22
+ absent because nobody looked, not because it was fixed. The ledger holds those entries open
23
+ (`lastSeen` untouched) and the diff reports them as held rather than folding them into
24
+ "N resolved".
25
+ - **`init` baselines the repo it leaves behind.** It used to approve the scan taken *before* its
26
+ own scaffold, so the first baseline recorded AGENTS.md and the hooks as absent — and deleting
27
+ them later never registered as drift, which is the baseline's whole job. It now re-scans after
28
+ writing.
29
+ - **The committed baseline carries no machine path.** `baseline.json` used to record the scan's
30
+ absolute root — the approver's username and directory layout — in the one file etymd tells people
31
+ to commit and therefore publish. The root is now elided on write (`"."`; it is redundant inside
32
+ the repo it describes). A baseline written by an older etymd is detected and disclosed with the
33
+ fix: `etymd approve`.
34
+ - **`.etymd/config.json`** (optional, committed) — `instructions.include` / `instructions.exclude`
35
+ globs scope which instruction files are audited (a fork keeps its own layer honest without
36
+ auditing inherited upstream skills), and `context.perFileWords` / `context.totalWords` make the
37
+ economy budgets per-repo. Excluded files are counted and named in the disclosures, and malformed
38
+ config is disclosed — narrowing an audit can never quietly buy a clean report.
39
+ - `etymd init` — onboarding: approve the committed baseline, gitignore the cache, scaffold a
40
+ minimal AGENTS.md only where none exists. Never overwrites.
41
+ - `etymd doctor` — alias for `audit --truth`.
42
+ - `etymd context` — the per-file always-loaded footprint view.
43
+ - `etymd gates` — local git-hook gates built from the repo's own check commands (writers can
44
+ never enter the gate); pairs with the gate-integrity findings.
45
+ - `etymd scan` — the deterministic reckoning (corpus-validated detectors incl. husky v3 and
46
+ meta-script-proof command classification). `etymd brief` — the agent briefing for the
47
+ semantic layer.
48
+ - Zero-trace guarantee: read-only probes of foreign repos write nothing.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alkis Yuv
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # etymd
2
+
3
+ **Keep your agent instructions true.**
4
+
5
+ Your `AGENTS.md` is the interface between your team and every coding agent — and it rots silently.
6
+ Scripts get renamed, directories move, rules go stale, and the file keeps instructing agents with
7
+ confidence. etymd continuously **verifies the agent context layer against the actual repo**:
8
+
9
+ ```
10
+ $ etymd audit
11
+
12
+ RISK AGENTS.md tells agents to run `pnpm dev` — no such script exists
13
+ evidence AGENTS.md: `pnpm dev` · package.json scripts
14
+ action Update the instruction to the current script name.
15
+
16
+ GAP AGENTS.md references `src/legacy/` — it does not exist in the repo
17
+ GAP Type checking is enforced only in CI — no local hook runs it
18
+ GAP AGENTS.md loads 13,809 words (~18k tokens) into every session
19
+
20
+ since last audit: 1 new · 3 still open · 1 resolved · 1 REGRESSED
21
+ ```
22
+
23
+ _From Greek **étymon** — a word's true, original sense (→ etymology) — clipped to **etym.** + the
24
+ **.md** family it guards._
25
+
26
+ ## Why this exists
27
+
28
+ - Instruction files are now load-bearing: 20+ agents (Claude Code, Codex, Cursor, Copilot,
29
+ Gemini, …) read `AGENTS.md` natively. A stale claim doesn't error — it silently misleads every
30
+ session.
31
+ - Linters exist for these files, but they check **a point in time**. Truth is a property **over
32
+ time**: etymd measures drift against a **committed baseline**, remembers findings in a
33
+ **ledger** (fixed things stay fixed; a returning problem is named a _regression_, and a finding
34
+ you dismissed with a reason never resurfaces), and gates CI on it.
35
+ - **Honesty is structural.** Every report declares what it could NOT see — CI jobs inherited from
36
+ unreadable org templates, server-side quality-gate thresholds, skipped heuristics. No guess is
37
+ ever dressed as a fact.
38
+
39
+ ## Quick start
40
+
41
+ ```bash
42
+ cd your-project
43
+ npx etymd init # approve the baseline (+ scaffold AGENTS.md only if you have none)
44
+ npx etymd audit # verify every instruction claim against the repo
45
+ npx etymd audit --fail-on risk # the CI gate
46
+ ```
47
+
48
+ Requires Node ≥ 18.17. **Status: pre-publish** — run from a checkout (`npm run build &&
49
+ node dist/cli.js …`) until the first npm release.
50
+
51
+ ## What it checks
52
+
53
+ **`instruction-truth`** — over `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, Copilot instructions,
54
+ `.cursor/rules/*`, `.clinerules`, `.claude/skills/*/SKILL.md`:
55
+
56
+ - **Command claims** — every `pnpm X` / `npm run X` the files tell agents to run must exist in
57
+ `package.json` scripts.
58
+ - **Path claims** — every repo path the files point at must exist (conservative heuristics; what's
59
+ skipped is disclosed). A path the surrounding prose tells the agent to _create_, and an obvious
60
+ naming stand-in like `my-custom-skill`, are forward-looking instructions, not stale references.
61
+ - **Package-manager consistency** — instructions must not command `yarn` in a `pnpm` repo.
62
+ - **Cross-references** — pointer chains (`CLAUDE.md` → `AGENTS.md` → state docs) must resolve.
63
+ - **Drift vs baseline** — documented commands/artifacts/layout that existed at approval and are
64
+ now gone.
65
+
66
+ **`gate-integrity`** — a CI config is a claim too: checks enforced only in CI (failures surface a
67
+ slow pipeline after the agent finished — `etymd gates` generates the local mirror), checks only in
68
+ skippable local hooks, latent gaps (coverage collected but nothing gates on it; commitlint
69
+ installed but unwired). `allow_failure` jobs count as advisory, never as gates.
70
+
71
+ **`context-economy`** — the always-loaded footprint in words/tokens (only genuinely
72
+ `alwaysApply` Cursor rules count), flagging files worth extracting into on-demand skills. Context
73
+ is the dominant cost of the loop; a lean contract is a correctness feature.
74
+
75
+ ## Commands
76
+
77
+ | Command | What it does |
78
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
79
+ | `etymd audit` | Verify every claim; ranked findings (risk → gap → polish) + ledger diff. `--lens`, `--truth`, `--json`, `--no-ledger`, `--fail-on <tier>`. |
80
+ | `etymd init` | Onboard: approve the committed baseline; scaffold a minimal AGENTS.md **only if missing**. Never overwrites. |
81
+ | `etymd doctor` | Alias for `audit --truth`. |
82
+ | `etymd context` | The economy view: per-file always-loaded footprint + extraction candidates. |
83
+ | `etymd gates` | Install local git-hook gates (pre-commit / pre-push) built from your own check scripts. |
84
+ | `etymd scan` | The deterministic reckoning behind everything. `--json`. |
85
+ | `etymd brief` | A grounded briefing your in-repo agent completes to author the semantic layer. |
86
+
87
+ `--cwd <dir>` targets another directory. Read-only probing of any repo leaves **zero trace**
88
+ (`audit --no-ledger` writes nothing).
89
+
90
+ ## The files etymd keeps
91
+
92
+ | Path | Lifecycle | Role |
93
+ | ---------------------- | ------------- | ------------------------------------------------ |
94
+ | `.etymd/baseline.json` | **committed** | the approved reckoning drift is measured against |
95
+ | `.etymd/ledger.json` | **committed** | the findings memory: statuses, diffs, dismissals |
96
+ | `.etymd/config.json` | **committed** | optional: audit scope + context budgets |
97
+ | `.etymd/cache/` | gitignored | transient scan cache |
98
+
99
+ The committed files are written to be publishable: the baseline records `"."` as its scan root, never
100
+ your absolute machine path. Only the gitignored cache keeps the real one.
101
+
102
+ ### `.etymd/config.json` (optional)
103
+
104
+ Every key is optional; omit the file entirely and the defaults below apply.
105
+
106
+ ```jsonc
107
+ {
108
+ "instructions": {
109
+ // Audit these too — files detection would not find on its own.
110
+ "include": ["design/**/*.md"],
111
+ // Leave these out. The classic case: a fork that inherits upstream's skills
112
+ // and will never fix them, but must keep its OWN instruction layer honest.
113
+ "exclude": [".claude/skills/**"],
114
+ },
115
+ "context": {
116
+ "perFileWords": 4000, // extraction candidate above this
117
+ "totalWords": 8000, // always-loaded footprint budget
118
+ },
119
+ }
120
+ ```
121
+
122
+ Globs are repo-relative: `*` within a path segment, `**` across segments, `?` one character. A
123
+ pattern with no wildcard is a **path prefix**, so `.claude/skills` covers everything beneath it.
124
+
125
+ Narrowing an audit can hide findings, so etymd never lets it happen quietly: **every excluded file
126
+ is counted and named in the lens disclosures**, and a config that fails to parse is reported as a
127
+ disclosure rather than silently falling back to defaults.
128
+
129
+ ## Programmatic use
130
+
131
+ ```ts
132
+ import { runAudit } from "etymd"
133
+
134
+ const audit = await runAudit(process.cwd(), { persistLedger: false })
135
+ console.log(audit.findings) // one schema: claim · evidence · why · action · effort · confidence
136
+ ```
137
+
138
+ ## The corpus (how this is validated)
139
+
140
+ etymd is developed against a corpus of real sibling repos rather than fixtures alone —
141
+ [`sources.json`](sources.json) lists them by shape. Every heuristic here exists because a real
142
+ repo proved the previous one wrong, and each skip class in the truth lens is a false positive that
143
+ a corpus run caught.
144
+
145
+ Some corpus entries are private and are named by shape (`nx-monorepo`, `spa-bff`, `cra-legacy`)
146
+ rather than by directory. To run those smokes locally, add an untracked `sources.local.json`
147
+ mapping each name to its sibling directory:
148
+
149
+ ```json
150
+ { "dirs": { "nx-monorepo": "my-monorepo-checkout" } }
151
+ ```
152
+
153
+ Each value is resolved as a sibling of the etymd checkout.
154
+
155
+ Without it — on a fresh clone or in CI — those suites skip cleanly and the rest still run.
156
+
157
+ ## Design record & roadmap
158
+
159
+ [`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).
161
+ [`ROADMAP.md`](ROADMAP.md) — what's now / next / later, the pre-publish checklist, and the
162
+ accepted heuristic trade-offs.
163
+
164
+ ## License
165
+
166
+ MIT
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { scanProject, PACK_VERSION } from './chunk-TCWKIMCU.js';
3
+ import { VERSION } from './chunk-U33XLT5I.js';
4
+ import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-SMM2TW35.js';
5
+ import { section, theme, print, renderBaselineDrift, glyph } from './chunk-WVYKYPKS.js';
6
+
7
+ // src/commands/approve.ts
8
+ async function run(opts) {
9
+ const prior = await readBaseline(opts.cwd);
10
+ if (!prior) {
11
+ throw new Error(
12
+ "no committed baseline to re-approve \u2014 run `etymd init` to create the first one"
13
+ );
14
+ }
15
+ const facts = await scanProject(opts.cwd);
16
+ const drift = summarizeBaselineDrift(prior.facts, facts);
17
+ section(`Re-approving baseline ${theme.dim(`\xB7 ${facts.name} \xB7 ${prior.profile} profile`)}`);
18
+ if (isDriftEmpty(drift)) {
19
+ print(` ${theme.dim("no structural change on the measured axes \u2014 refreshing the stamp only")}`);
20
+ } else {
21
+ renderBaselineDrift(drift);
22
+ }
23
+ await writeBaseline(opts.cwd, {
24
+ packVersion: PACK_VERSION,
25
+ etymdVersion: VERSION,
26
+ approvedAt: (/* @__PURE__ */ new Date()).toISOString(),
27
+ // A refresh re-blesses the current tree; the profile is a human decision from init — keep it.
28
+ profile: prior.profile ?? deriveProfile(facts),
29
+ facts
30
+ });
31
+ print();
32
+ print(
33
+ ` ${glyph.ok} ${theme.dim("baseline re-approved \u2192")} ${theme.info(".etymd/baseline.json")} ${theme.dim("(commit it \u2014 drift is now measured against this state)")}`
34
+ );
35
+ }
36
+
37
+ export { run };
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ export { run } from './chunk-5D7WNJ6N.js';
3
+ import './chunk-N64ZTBCW.js';
4
+ import './chunk-I566LDX7.js';
5
+ import './chunk-TCWKIMCU.js';
6
+ import './chunk-U33XLT5I.js';
7
+ import './chunk-SMM2TW35.js';
8
+ import './chunk-WVYKYPKS.js';
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import { scanProject } from './chunk-TCWKIMCU.js';
3
+ import './chunk-U33XLT5I.js';
4
+ import { ETYMD_DIR } from './chunk-SMM2TW35.js';
5
+ import { print, theme } from './chunk-WVYKYPKS.js';
6
+ import { promises } from 'node:fs';
7
+ import path from 'node:path';
8
+
9
+ async function run(opts) {
10
+ const facts = await scanProject(opts.cwd);
11
+ const contents = opts.human ? humanBrief(facts) : agentBrief(facts);
12
+ const rel = path.join(ETYMD_DIR, opts.human ? "onboarding.md" : "brief.md");
13
+ const target = path.join(opts.cwd, rel);
14
+ await promises.mkdir(path.dirname(target), { recursive: true });
15
+ await promises.writeFile(target, contents, "utf8");
16
+ print();
17
+ print(` ${theme.dim("wrote")} ${theme.info(rel)}`);
18
+ if (opts.human) {
19
+ print(` ${theme.dim("A human-readable onboarding brief drawn from the reckoning.")}`);
20
+ } else {
21
+ print(
22
+ ` ${theme.dim("Hand this to the agent already in your repo (Claude Code / Cursor / Copilot):")}`
23
+ );
24
+ print(
25
+ ` ${theme.dim(' "Read')} ${theme.info(rel)}${theme.dim(' and complete it against the codebase."')}`
26
+ );
27
+ print(
28
+ ` ${theme.dim("Its answers become the semantic half of AGENTS.md \u2014 you approve before it lands.")}`
29
+ );
30
+ }
31
+ }
32
+ function factsSummary(facts) {
33
+ const dirs = facts.tree.dirs.slice(0, 14).map((d) => `- \`${d.name}/\` (${d.files} files)`).join("\n");
34
+ return `- Name: ${facts.name}
35
+ - Shape: ${facts.workspace.kind === "none" ? "single package" : `${facts.workspace.kind} workspace`}, ${facts.packageManager}
36
+ - Frameworks: ${facts.frameworks.join(", ") || "unknown"}
37
+ - CI: ${facts.ci.system}
38
+ - Top-level layout:
39
+ ${dirs || "- (single package)"}`;
40
+ }
41
+ function agentBrief(facts) {
42
+ return `# Reckoning brief \u2014 ${facts.name}
43
+
44
+ You are completing the **semantic half** of this project's operating contract. etymd has already
45
+ gathered the deterministic facts below. Your job is to fill in what only reading the code reveals.
46
+ Ground every answer in real files; cite paths. Do not invent structure. When unsure, say so.
47
+
48
+ ## Facts already known (do not re-derive)
49
+
50
+ ${factsSummary(facts)}
51
+
52
+ ## Produce these sections
53
+
54
+ 1. **What this project is** \u2014 one paragraph: what it does, who uses it, the core domain.
55
+ 2. **Composition points** \u2014 the 4\u20138 architectural seams a change usually flows through (entry
56
+ points, routers, the data-access layer, the shared contract surface). For each: file path +
57
+ one line on the invariant that must not break when editing it.
58
+ 3. **Reuse inventory** \u2014 the existing components/hooks/utils/types a new task most often should
59
+ reuse instead of writing fresh. Point at where they live.
60
+ 4. **Ownership boundaries** \u2014 which directories own which concerns; what must not leak across them.
61
+ 5. **Gotchas** \u2014 non-obvious rules, environment traps, or footguns worth a failure-modes entry.
62
+
63
+ ## Output
64
+
65
+ Write your answer to \`AGENTS.md\` under the matching headings (replace the \`<!-- ... -->\`
66
+ placeholders), or return it for review. The human approves before it becomes the contract.
67
+ `;
68
+ }
69
+ function humanBrief(facts) {
70
+ return `# Onboarding \u2014 ${facts.name}
71
+
72
+ A quick orientation generated from the project reckoning.
73
+
74
+ ## At a glance
75
+
76
+ ${factsSummary(facts)}
77
+
78
+ ## Getting started
79
+
80
+ \`\`\`bash
81
+ ${facts.commands.dev ? `${facts.packageManager} ${facts.commands.dev} # run locally` : "# add the dev command"}
82
+ ${facts.commands.test ? `${facts.packageManager} ${facts.commands.test} # run tests` : ""}
83
+ \`\`\`
84
+
85
+ ## Where to look next
86
+
87
+ - \`AGENTS.md\` \u2014 the operating contract (stack, rules, repo map, definition of done).
88
+ - \`PROJECT_CONTEXT.md\` \u2014 current state and decisions.
89
+
90
+ _This is a starting point. Refine it with the domain knowledge only the team has._
91
+ `;
92
+ }
93
+
94
+ export { run };