etymd 0.4.2 → 0.6.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 +118 -0
- package/README.md +47 -23
- package/dist/{approve-FISVPFL2.js → approve-ZLOW4A7R.js} +6 -6
- package/dist/audit-ICT2JK7A.js +12 -0
- package/dist/{brief-UF6KV7SG.js → brief-PDUGGMLI.js} +6 -6
- package/dist/{chunk-TADKOW6P.js → chunk-5BVKFJWM.js} +7 -3
- package/dist/{chunk-IGHES6AK.js → chunk-6DUDIVIC.js} +1 -1
- package/dist/{chunk-4AEYMVZK.js → chunk-BQGCLPHS.js} +2 -2
- package/dist/{chunk-R74SJ7HS.js → chunk-C7LBUFNU.js} +1 -1
- package/dist/{chunk-WWV5W2MM.js → chunk-HLWCODYX.js} +1 -1
- package/dist/{chunk-YLNQQZ5F.js → chunk-IV3FYVTS.js} +11 -1
- package/dist/{chunk-NMZ3RHWW.js → chunk-K7QBTANO.js} +2 -2
- package/dist/{chunk-OX3LUOZR.js → chunk-KTIEYKFK.js} +46 -22
- package/dist/{chunk-WWBF4Y7K.js → chunk-OAFYLBVG.js} +1 -1
- package/dist/{chunk-6G3JJMZ3.js → chunk-OESQNO2J.js} +77 -9
- package/dist/{chunk-LT67FU2Y.js → chunk-OKU3HXIH.js} +2 -2
- package/dist/{chunk-HX5IYDCU.js → chunk-ULEWJ3ZK.js} +3 -3
- package/dist/{chunk-MMPV67FW.js → chunk-V2SOHPXF.js} +2 -2
- package/dist/cli.js +18 -18
- package/dist/config-UJRSCDMD.js +4 -0
- package/dist/{context-4DGXO7JR.js → context-ZEUI5ESZ.js} +5 -5
- package/dist/doctor-PORXFSYT.js +19 -0
- package/dist/{fleet-UTFZKYNH.js → fleet-JHNQOLSY.js} +71 -22
- package/dist/{gates-YUUV7YI4.js → gates-KHC7HPIF.js} +21 -13
- package/dist/generate-UTKQ4GE6.js +6 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +138 -31
- package/dist/{init-Q5Q7IGZS.js → init-IPUPP4GB.js} +8 -8
- package/dist/ledger-B2XGANTH.js +5 -0
- package/dist/{scan-UVNX52J5.js → scan-6L4L2GXT.js} +6 -6
- package/dist/scan-D4M7JXE3.js +5 -0
- package/dist/{screen-U7JMWZPS.js → screen-6R6NXDSY.js} +2 -2
- package/package.json +3 -4
- package/dist/audit-GUG2QFZM.js +0 -12
- package/dist/config-MX7OYI7F.js +0 -4
- package/dist/doctor-SOLNC37X.js +0 -19
- package/dist/generate-CBMTNN7P.js +0 -6
- package/dist/ledger-TMIV45AA.js +0 -5
- package/dist/scan-NWZ65YWB.js +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,123 @@
|
|
|
1
1
|
# etymd
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b02943b: Local gates: the tool now reads and rewrites what it generated, instead of guessing about it.
|
|
8
|
+
|
|
9
|
+
Two defects in the local-gate machinery, both silent, both affecting every project using
|
|
10
|
+
`etymd gates`. Decision record:
|
|
11
|
+
[`docs/decisions/006-local-gate-provenance.md`](docs/decisions/006-local-gate-provenance.md).
|
|
12
|
+
|
|
13
|
+
**`gate-integrity` follows the `<hook>.local` include.** Every generated hook opens by sourcing a
|
|
14
|
+
sibling companion — the sanctioned place for project-specific guards, which etymd never reads,
|
|
15
|
+
writes or regenerates. The lens stopped at the hook file, so a check placed in the companion ran
|
|
16
|
+
on every push, blocked it, and was still reported as enforced only in CI. The only way to silence
|
|
17
|
+
that was a dismissal, which is meant for a false positive or an accepted trade-off, not for a
|
|
18
|
+
blind spot. The lens now resolves the companion for `pre-commit`, `pre-push` and `commit-msg` and
|
|
19
|
+
counts what it enforces.
|
|
20
|
+
|
|
21
|
+
Two conditions keep it honest, both mirroring what the shell actually does: the hook must
|
|
22
|
+
genuinely reference the companion, and the companion must pass the same `[ -x ]` test the hook
|
|
23
|
+
applies before running it. A present-but-not-executable companion is reported as inert — named,
|
|
24
|
+
with the `chmod +x` that would make it run — rather than credited with checks that never fire.
|
|
25
|
+
Both outcomes are disclosed in the lens report.
|
|
26
|
+
|
|
27
|
+
**A stale generated hook is no longer indistinguishable from a hand-edited one.** `etymd gates`
|
|
28
|
+
refused to overwrite any hook whose content differed from what it would generate, printing
|
|
29
|
+
`kept (hand-edited)`. That protection is right for a customised hook and wrong for a merely stale
|
|
30
|
+
one — generated by an older etymd, or before the repo's package manager or scripts changed. In
|
|
31
|
+
that case the refusal preserved a gate that had stopped matching the repo, while `etymd audit`
|
|
32
|
+
went on reporting the gaps the refused rewrite would have closed; the only escape was deleting the
|
|
33
|
+
file, which nobody would think to try.
|
|
34
|
+
|
|
35
|
+
Generated shell files now carry a stamp as their last line, whose digest covers the file minus
|
|
36
|
+
that line. A file that still hashes to its own stamp is byte-for-byte what etymd wrote and cannot
|
|
37
|
+
contain anyone's work, so it is regenerated freely; any edit breaks the match and the file is
|
|
38
|
+
kept. `[differs]` is no longer one bucket — the plan tags a stale file `[stale]` and says it is
|
|
39
|
+
regenerating, and says why it is keeping the others.
|
|
40
|
+
|
|
41
|
+
A hook generated before the stamp existed carries no stamp and cannot be proven untouched, so it
|
|
42
|
+
is kept — now with the reason and the way out stated, instead of a dead end. One regeneration
|
|
43
|
+
makes it provable from then on.
|
|
44
|
+
|
|
45
|
+
**Provenance applies to everything the pack generates, not just hooks.** The `AGENTS.md` scaffold
|
|
46
|
+
is stamped too, in an HTML comment so it stays invisible in the rendered document; its stamp
|
|
47
|
+
replaces the bare `<!-- etymd pack vN -->` line, carrying the same version plus the provenance.
|
|
48
|
+
`.etymd/config.json` is excluded — it holds the user's recorded decisions, is merged into rather
|
|
49
|
+
than generated, and JSON has nowhere to hide a stamp.
|
|
50
|
+
|
|
51
|
+
**New finding: a companion that cannot run.** `gate-integrity/companion-not-executable` reports a
|
|
52
|
+
`<hook>.local` that a hook calls, that exists, and that provably lacks the execute bit — the check
|
|
53
|
+
inside is skipped silently on every run, and git tracks exactly one permission bit, so the dead
|
|
54
|
+
gate reaches every clone. Emitted at `gap` rather than `risk`: risk is reserved for what makes an
|
|
55
|
+
agent do the wrong thing (`hooks-not-wired` earns it because every gate is dead there), and it
|
|
56
|
+
also means an upgrade cannot fail the `--fail-on risk` gate in a repo whose code did not change.
|
|
57
|
+
The action names both halves of the fix, since `chmod +x` alone does not survive a fresh clone.
|
|
58
|
+
|
|
59
|
+
The accusation is guarded: "not executable" is trusted only where the hook beside it HAS the bit,
|
|
60
|
+
which is what proves the bit means anything on that filesystem. Where it cannot be established the
|
|
61
|
+
checks are counted and the uncertainty is disclosed, so a checkout without POSIX mode bits is
|
|
62
|
+
never told its working gate is dead.
|
|
63
|
+
|
|
64
|
+
**The fleet sweep stops recommending a command that would refuse.** `fleet` reported every
|
|
65
|
+
differing gate under one finding whose action was "re-run `etymd gates`" — advice that does not
|
|
66
|
+
work for a hand-edited hook. A provably-edited gate is now disclosed as a customisation rather
|
|
67
|
+
than reported as drift; stale and unstamped gates stay in the finding, since being unable to prove
|
|
68
|
+
a file was touched is not evidence that it was.
|
|
69
|
+
|
|
70
|
+
`PACK_VERSION` moves to 6: the generated files change meaning.
|
|
71
|
+
|
|
72
|
+
### Patch Changes
|
|
73
|
+
|
|
74
|
+
- `yaml` moves from 2.7.0 to 2.9.0, clearing GHSA-48c2-rrv3-qjmp (stack overflow on deeply nested
|
|
75
|
+
collections) from the dependency tree of everyone who installs etymd. The lens that parses CI
|
|
76
|
+
files already wrapped every `YAML.parse` in a try/catch, so a hostile `.gitlab-ci.yml` was
|
|
77
|
+
caught and disclosed as a parse error rather than crashing anything — verified against a
|
|
78
|
+
200,000-level nested file before and after. The bump is about not shipping a known-vulnerable
|
|
79
|
+
dependency, not about a reachable exploit.
|
|
80
|
+
|
|
81
|
+
## 0.5.0
|
|
82
|
+
|
|
83
|
+
### Minor Changes
|
|
84
|
+
|
|
85
|
+
- e199ccc: `fleet add --profile corp` now records the alias-to-directory mapping too, not just the entry.
|
|
86
|
+
|
|
87
|
+
A corp entry in the tracked manifest is deliberately alias-only — no path, no remote — which is
|
|
88
|
+
what keeps employer names out of a file that gets pushed. It also means the entry resolves to
|
|
89
|
+
nothing on its own. Registering wrote only that half, so every corp registration ended as a
|
|
90
|
+
dangling entry that `fleet check` reported immediately and the user had to fix by hand, in the
|
|
91
|
+
one file the tool otherwise never asks anyone to hand-edit.
|
|
92
|
+
|
|
93
|
+
The mapping is written `~`-relative, so the local manifest stays portable between machines, and
|
|
94
|
+
merged into the existing document so hand-maintained entries survive.
|
|
95
|
+
|
|
96
|
+
Two refusals guard it, because this file is the one place real employer directory names are
|
|
97
|
+
written down. If the local manifest is not gitignored, registration refuses rather than creating
|
|
98
|
+
a file git would track — a leak the tool creates is worse than a registration it declines to
|
|
99
|
+
finish. If the file exists but is not valid JSON, it refuses rather than overwriting mappings
|
|
100
|
+
that cannot be re-derived.
|
|
101
|
+
|
|
102
|
+
The mapping is written before the tracked entry. The failure modes are not symmetric: a mapping
|
|
103
|
+
without an entry is inert, while an entry without a mapping is a broken registration sitting in
|
|
104
|
+
the file that gets committed.
|
|
105
|
+
|
|
106
|
+
- 9546979: `fleet add` no longer records a remote URL in the manifest.
|
|
107
|
+
|
|
108
|
+
The field was write-only: nothing in the tool ever read it back. It was persisted because it
|
|
109
|
+
happened to be derivable at registration time, and it stayed because no one asked what consumed
|
|
110
|
+
it.
|
|
111
|
+
|
|
112
|
+
It is also the field that turned a mis-profiled entry into a real disclosure. A raw remote URL
|
|
113
|
+
carries the host and the internal group path; `path` carries a bare directory name. Removing a
|
|
114
|
+
field no consumer reads retires that class outright, with no host-matching heuristic, and unlike
|
|
115
|
+
the corp-host guard it keeps working on a machine that has no local manifest to read corp hosts
|
|
116
|
+
from. The remote stays derivable from the checkout at any time, which is where it came from.
|
|
117
|
+
|
|
118
|
+
Existing entries that already carry a remote are left alone — nothing reads them, and rewriting
|
|
119
|
+
a hand-maintained manifest to remove inert keys would be a worse trade than leaving them.
|
|
120
|
+
|
|
3
121
|
## 0.4.2
|
|
4
122
|
|
|
5
123
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -96,6 +96,10 @@ _From Greek **étymon** — a word's true, original sense (→ etymology) — cl
|
|
|
96
96
|
no committed baseline, there is nothing to measure drift against. Everything else runs.
|
|
97
97
|
- **`etymd init` never overwrites an existing `AGENTS.md`.** It scaffolds a minimal one only if
|
|
98
98
|
you have none. The feared overwrite path simply does not exist.
|
|
99
|
+
- **"The shellcheck step announced that it skipped."** That is the intended behaviour, not a
|
|
100
|
+
misconfiguration: where the binary is absent the hook says so and names the install command,
|
|
101
|
+
because a check that goes quiet when its tool is missing looks installed everywhere and is
|
|
102
|
+
installed nowhere.
|
|
99
103
|
|
|
100
104
|
---
|
|
101
105
|
|
|
@@ -153,24 +157,24 @@ personal and employer repos. See [the fleet manifest](#the-fleet-manifest-experi
|
|
|
153
157
|
|
|
154
158
|
## Commands
|
|
155
159
|
|
|
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.
|
|
160
|
+
| Command | What it does |
|
|
161
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
162
|
+
| `etymd audit` | Verify every claim; ranked findings (risk → gap → polish) + ledger diff. `--lens`, `--truth`, `--json`, `--no-ledger`, `--fail-on <tier>`. |
|
|
163
|
+
| `etymd init` | Onboard: approve the committed baseline; scaffold a minimal AGENTS.md **only if missing**. Never overwrites. |
|
|
164
|
+
| `etymd doctor` | Alias for `audit --truth`. |
|
|
165
|
+
| `etymd context` | The economy view: per-file always-loaded footprint + extraction candidates. |
|
|
166
|
+
| `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 — and from the repo's shell surface, where it has one. |
|
|
167
|
+
| `etymd screen` | Content screen: find text that must never be published. Four scopes — `--staged`, `--message`, `--tree`, `--dir`. Bring your own patterns; etymd ships none. |
|
|
168
|
+
| `etymd scan` | The deterministic reckoning behind everything. `--json`. |
|
|
169
|
+
| `etymd brief` | A grounded briefing your in-repo agent completes to author the semantic layer. |
|
|
170
|
+
| `etymd approve` | Refresh the committed baseline non-interactively after intentional structural changes. |
|
|
171
|
+
| `etymd ledger` | The findings memory: every tracked finding with status and history. |
|
|
172
|
+
| `etymd dismiss` | `dismiss <id> --reason <text>` — a dismissed finding never resurfaces without regressing. |
|
|
173
|
+
| `etymd accept` | `accept <id>` — record a finding as accepted reality; visible in the ledger, out of the report. |
|
|
174
|
+
| `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`. |
|
|
175
|
+
| `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. |
|
|
176
|
+
| `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`. |
|
|
177
|
+
| `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. |
|
|
174
178
|
|
|
175
179
|
`--cwd <dir>` targets another directory. Read-only probing of any repo leaves **zero trace**
|
|
176
180
|
(`audit --no-ledger` writes nothing).
|
|
@@ -252,8 +256,9 @@ request diff like any other change. Keep `.etymd` out of your formatter's reach
|
|
|
252
256
|
[the files Etymd keeps](#the-files-etymd-keeps)).
|
|
253
257
|
|
|
254
258
|
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
|
|
259
|
+
`etymd gates` installs the local pre-commit / pre-push mirror built from your own check scripts —
|
|
260
|
+
and from the repo's shell scripts, which usually have no check of their own — while the
|
|
261
|
+
`gate-integrity` lens flags whatever still runs in CI alone.
|
|
257
262
|
|
|
258
263
|
### Your own checks, beside the generated ones
|
|
259
264
|
|
|
@@ -282,6 +287,24 @@ was edited or went stale, never that you added a check of your own. Delete the c
|
|
|
282
287
|
checks stop running — that is what deleting a file means, and etymd does not police a file it
|
|
283
288
|
does not own.
|
|
284
289
|
|
|
290
|
+
### The shell surface
|
|
291
|
+
|
|
292
|
+
Package scripts are not the only executable surface a repo has, and in some repos they are not
|
|
293
|
+
the main one — a tools or infra repo can be entirely `bootstrap/*.sh` plus `.githooks/*` with no
|
|
294
|
+
`package.json` at all. Those repos were told "no correctness commands detected", which reads as
|
|
295
|
+
"nothing to check" when it means "nothing was looked at". Where a repo has tracked shell scripts,
|
|
296
|
+
the generated pre-push now carries a `shellcheck` step. Three properties are deliberate:
|
|
297
|
+
|
|
298
|
+
- **Scripts are re-discovered by shebang at push time**, never baked into the hook as a list. A
|
|
299
|
+
generated list is correct the day it is written and silently wrong the first time someone adds
|
|
300
|
+
a script.
|
|
301
|
+
- **A missing `shellcheck` binary is a loud skip that names the install command**, never a quiet
|
|
302
|
+
pass. A check that goes silent when its tool is absent looks installed on every machine and is
|
|
303
|
+
installed on one.
|
|
304
|
+
- **Only `warning` severity and above blocks**; style and info print as advice afterwards. A gate
|
|
305
|
+
with a high false-positive rate does not make a repo careful — it teaches everyone the bypass
|
|
306
|
+
flag, and that flag is shared with the checks that must never be bypassed.
|
|
307
|
+
|
|
285
308
|
### The content screen (`etymd screen`)
|
|
286
309
|
|
|
287
310
|
A separate question from "are the instructions true?": **does this repo carry text that must
|
|
@@ -471,9 +494,10 @@ Without it — on a fresh clone or in CI — those suites skip cleanly and the r
|
|
|
471
494
|
|
|
472
495
|
[`docs/decisions/`](https://github.com/triartleet/etymd/tree/main/docs/decisions) — 001 founding · 002 foundation re-lock · **003 the truth-guard
|
|
473
496
|
pivot** (the current identity; includes the state-of-the-field investigation it rests on) ·
|
|
474
|
-
**004 fleet mode** (the truth guard across your repositories)
|
|
475
|
-
|
|
476
|
-
accepted heuristic
|
|
497
|
+
**004 fleet mode** (the truth guard across your repositories) · 005 declared rules (design only) ·
|
|
498
|
+
**006 local gate provenance** (what the tool may read, and what it may rewrite).
|
|
499
|
+
[`ROADMAP.md`](https://github.com/triartleet/etymd/blob/main/ROADMAP.md) — what's now / next / later, and the accepted heuristic
|
|
500
|
+
trade-offs.
|
|
477
501
|
|
|
478
502
|
## License
|
|
479
503
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { section, theme, print, renderBaselineDrift, glyph } from './chunk-
|
|
3
|
-
import { scanProject } from './chunk-
|
|
4
|
-
import { VERSION } from './chunk-
|
|
5
|
-
import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-
|
|
6
|
-
import { PACK_VERSION } from './chunk-
|
|
7
|
-
import './chunk-
|
|
2
|
+
import { section, theme, print, renderBaselineDrift, glyph } from './chunk-5BVKFJWM.js';
|
|
3
|
+
import { scanProject } from './chunk-ULEWJ3ZK.js';
|
|
4
|
+
import { VERSION } from './chunk-HLWCODYX.js';
|
|
5
|
+
import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-C7LBUFNU.js';
|
|
6
|
+
import { PACK_VERSION } from './chunk-6DUDIVIC.js';
|
|
7
|
+
import './chunk-IV3FYVTS.js';
|
|
8
8
|
|
|
9
9
|
// src/commands/approve.ts
|
|
10
10
|
async function run(opts) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export { run } from './chunk-BQGCLPHS.js';
|
|
3
|
+
import './chunk-OESQNO2J.js';
|
|
4
|
+
import './chunk-OKU3HXIH.js';
|
|
5
|
+
import './chunk-OAFYLBVG.js';
|
|
6
|
+
import './chunk-K7QBTANO.js';
|
|
7
|
+
import './chunk-5BVKFJWM.js';
|
|
8
|
+
import './chunk-ULEWJ3ZK.js';
|
|
9
|
+
import './chunk-HLWCODYX.js';
|
|
10
|
+
import './chunk-C7LBUFNU.js';
|
|
11
|
+
import './chunk-6DUDIVIC.js';
|
|
12
|
+
import './chunk-IV3FYVTS.js';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { print, theme } from './chunk-
|
|
3
|
-
import { scanProject } from './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import { ETYMD_DIR } from './chunk-
|
|
6
|
-
import './chunk-
|
|
7
|
-
import './chunk-
|
|
2
|
+
import { print, theme } from './chunk-5BVKFJWM.js';
|
|
3
|
+
import { scanProject } from './chunk-ULEWJ3ZK.js';
|
|
4
|
+
import './chunk-HLWCODYX.js';
|
|
5
|
+
import { ETYMD_DIR } from './chunk-C7LBUFNU.js';
|
|
6
|
+
import './chunk-6DUDIVIC.js';
|
|
7
|
+
import './chunk-IV3FYVTS.js';
|
|
8
8
|
import { promises } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
|
|
@@ -128,11 +128,15 @@ function renderContext(budget, threshold) {
|
|
|
128
128
|
print(` ${glyph.ok} ${theme.dim("lean \u2014 no single file is heavy enough to extract yet")}`);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
function renderPlan(files) {
|
|
131
|
+
function renderPlan(files, opts = {}) {
|
|
132
132
|
section("Plan");
|
|
133
133
|
for (const f of files) {
|
|
134
|
-
const
|
|
135
|
-
|
|
134
|
+
const stale = f.differs && f.drift === "stale";
|
|
135
|
+
const tag = !f.exists ? theme.ok("create") : stale ? theme.warn("stale") : f.differs ? theme.warn("differs") : theme.dim("same");
|
|
136
|
+
const note = !f.differs ? "" : stale ? opts.regeneratesStale ? " \u2014 unedited since etymd generated it; regenerating" : " \u2014 unedited since etymd generated it" : f.drift === "edited" ? " \u2014 hand-edited since etymd generated it; keeping" : " \u2014 no etymd generation stamp; keeping";
|
|
137
|
+
print(
|
|
138
|
+
` ${pc.dim("[")}${tag}${pc.dim("]")} ${theme.info(f.path)} ${theme.dim(f.label)}${theme.dim(note)}`
|
|
139
|
+
);
|
|
136
140
|
}
|
|
137
141
|
}
|
|
138
142
|
var TIER_BADGE = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-
|
|
3
|
-
import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-
|
|
2
|
+
import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-OESQNO2J.js';
|
|
3
|
+
import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-5BVKFJWM.js';
|
|
4
4
|
|
|
5
5
|
// src/commands/audit.ts
|
|
6
6
|
async function run(opts) {
|
|
@@ -34,6 +34,16 @@ async function isDirectory(p) {
|
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
async function isExecutable(p) {
|
|
38
|
+
try {
|
|
39
|
+
const stat = await promises.stat(p);
|
|
40
|
+
if (!stat.isFile()) return false;
|
|
41
|
+
if (process.platform === "win32") return null;
|
|
42
|
+
return (stat.mode & 73) !== 0;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
37
47
|
async function git(root, args) {
|
|
38
48
|
try {
|
|
39
49
|
const { stdout } = await pExecFile("git", args, { cwd: root, timeout: 4e3 });
|
|
@@ -99,4 +109,4 @@ function matchesAnyGlob(p, globs) {
|
|
|
99
109
|
return globs.some((g) => matchesGlob(p, g));
|
|
100
110
|
}
|
|
101
111
|
|
|
102
|
-
export { approxTokens, git, isCiEnvironment, isDirectory, matchesAnyGlob, normalizeRelPath, pathExists, readJson, readText, wordCount };
|
|
112
|
+
export { approxTokens, git, isCiEnvironment, isDirectory, isExecutable, matchesAnyGlob, normalizeRelPath, pathExists, readJson, readText, wordCount };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { ETYMD_DIR } from './chunk-
|
|
3
|
-
import { pathExists, readText } from './chunk-
|
|
2
|
+
import { ETYMD_DIR } from './chunk-C7LBUFNU.js';
|
|
3
|
+
import { pathExists, readText } from './chunk-IV3FYVTS.js';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
6
|
var CONFIG_FILE = path.join(ETYMD_DIR, "config.json");
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { DEFAULT_CONFIG } from './chunk-
|
|
3
|
-
import { PACK_VERSION } from './chunk-
|
|
4
|
-
import { readText, pathExists } from './chunk-
|
|
2
|
+
import { DEFAULT_CONFIG } from './chunk-K7QBTANO.js';
|
|
3
|
+
import { PACK_VERSION } from './chunk-6DUDIVIC.js';
|
|
4
|
+
import { readText, pathExists } from './chunk-IV3FYVTS.js';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
var GENERATION_MARKER_RE = /^(?:# |<!-- )etymd:generated pack-v\S+ ([0-9a-f]{16})(?: -->)?$/;
|
|
9
|
+
function digestOf(body) {
|
|
10
|
+
return createHash("sha256").update(body).digest("hex").slice(0, 16);
|
|
11
|
+
}
|
|
12
|
+
function stampGenerated(body, comment = "sh") {
|
|
13
|
+
const marker = `etymd:generated pack-v${PACK_VERSION} ${digestOf(body)}`;
|
|
14
|
+
return `${body}${comment === "md" ? `<!-- ${marker} -->` : `# ${marker}`}
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
17
|
+
function fileOrigin(text) {
|
|
18
|
+
const lines = text.split("\n");
|
|
19
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
20
|
+
const match = GENERATION_MARKER_RE.exec(lines[i]);
|
|
21
|
+
if (!match) continue;
|
|
22
|
+
const body = [...lines.slice(0, i), ...lines.slice(i + 1)].join("\n");
|
|
23
|
+
return digestOf(body) === match[1] ? "pack" : "edited";
|
|
24
|
+
}
|
|
25
|
+
return "unstamped";
|
|
26
|
+
}
|
|
8
27
|
function runPrefix(pm) {
|
|
9
28
|
switch (pm) {
|
|
10
29
|
case "pnpm":
|
|
@@ -57,7 +76,8 @@ function generateAgentsMd(facts) {
|
|
|
57
76
|
const frameworks = facts.frameworks.length ? facts.frameworks.join(", ") : "none detected";
|
|
58
77
|
const workspace = facts.workspace.kind === "none" ? "single package" : `${facts.workspace.kind} workspace`;
|
|
59
78
|
const topDirs = facts.tree.dirs.slice(0, 14);
|
|
60
|
-
return
|
|
79
|
+
return stampGenerated(
|
|
80
|
+
`# AGENTS.md
|
|
61
81
|
|
|
62
82
|
Operating contract for AI agents working in **${facts.name}**. One source of truth \u2014 most agents
|
|
63
83
|
(Claude Code, Codex, Cursor, Copilot, Gemini, \u2026) read this file natively. Kept true by
|
|
@@ -100,16 +120,17 @@ ${done.map((d) => `- ${d}`).join("\n")}` : `Define the check commands that gate
|
|
|
100
120
|
|
|
101
121
|
\`\`\`bash
|
|
102
122
|
${[
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
facts.commands.dev && `${run} ${facts.commands.dev}`,
|
|
124
|
+
facts.commands.build && `${run} ${facts.commands.build}`,
|
|
125
|
+
facts.commands.test && `${run} ${facts.commands.test}`,
|
|
126
|
+
facts.commands.lint && `${run} ${facts.commands.lint}`,
|
|
127
|
+
facts.commands.typecheck && `${run} ${facts.commands.typecheck}`
|
|
128
|
+
].filter(Boolean).join("\n") || "# add the project's key commands"}
|
|
109
129
|
\`\`\`
|
|
110
130
|
|
|
111
|
-
|
|
112
|
-
|
|
131
|
+
`,
|
|
132
|
+
"md"
|
|
133
|
+
);
|
|
113
134
|
}
|
|
114
135
|
var CONTENT_GATE_RESOLUTION = `GATE="\${CONTENT_GATE:-$(command -v etymd || true)}"`;
|
|
115
136
|
function localHookCall(hook) {
|
|
@@ -121,7 +142,7 @@ if [ -x "$LOCAL" ]; then
|
|
|
121
142
|
fi`;
|
|
122
143
|
}
|
|
123
144
|
function generatePreCommitHook() {
|
|
124
|
-
return `#!/usr/bin/env sh
|
|
145
|
+
return stampGenerated(`#!/usr/bin/env sh
|
|
125
146
|
# etymd: process gate. Cheap, locally-knowable checks belong here (fast, blocks the commit).
|
|
126
147
|
|
|
127
148
|
${localHookCall("pre-commit")}
|
|
@@ -138,10 +159,10 @@ if [ -x "$GATE" ]; then
|
|
|
138
159
|
fi
|
|
139
160
|
|
|
140
161
|
exit 0
|
|
141
|
-
|
|
162
|
+
`);
|
|
142
163
|
}
|
|
143
164
|
function generateCommitMsgHook() {
|
|
144
|
-
return `#!/usr/bin/env sh
|
|
165
|
+
return stampGenerated(`#!/usr/bin/env sh
|
|
145
166
|
# etymd: content screen \u2014 the commit message itself.
|
|
146
167
|
#
|
|
147
168
|
# The staged-content gate reads file bytes and never sees the message, yet a message is as
|
|
@@ -156,7 +177,7 @@ fi
|
|
|
156
177
|
${localHookCall("commit-msg")}
|
|
157
178
|
|
|
158
179
|
exit 0
|
|
159
|
-
|
|
180
|
+
`);
|
|
160
181
|
}
|
|
161
182
|
function shellcheckStep() {
|
|
162
183
|
return `
|
|
@@ -209,7 +230,7 @@ if command -v etymd >/dev/null 2>&1; then
|
|
|
209
230
|
else
|
|
210
231
|
echo "\u203A etymd audit skipped (not on PATH)"
|
|
211
232
|
fi`;
|
|
212
|
-
return `#!/usr/bin/env sh
|
|
233
|
+
return stampGenerated(`#!/usr/bin/env sh
|
|
213
234
|
# etymd: correctness gate. Mirrors CI cheapest-first; blocks the push on any failure.
|
|
214
235
|
|
|
215
236
|
${localHookCall("pre-push")}
|
|
@@ -225,10 +246,10 @@ if [ -x "$GATE" ] && [ "\${CONTENT_GATE_PREPUSH:-1}" = "1" ]; then
|
|
|
225
246
|
fi
|
|
226
247
|
|
|
227
248
|
exit 0
|
|
228
|
-
|
|
249
|
+
`);
|
|
229
250
|
}
|
|
230
251
|
function generateArtifactCheckScript() {
|
|
231
|
-
return `#!/usr/bin/env sh
|
|
252
|
+
return stampGenerated(`#!/usr/bin/env sh
|
|
232
253
|
# etymd: content screen \u2014 the published ARTIFACT, not the repository.
|
|
233
254
|
#
|
|
234
255
|
# Wire it into the irreversible moment:
|
|
@@ -257,7 +278,7 @@ fi
|
|
|
257
278
|
|
|
258
279
|
"$GATE" screen --dir "$WORK" || exit 1
|
|
259
280
|
exit 0
|
|
260
|
-
|
|
281
|
+
`);
|
|
261
282
|
}
|
|
262
283
|
|
|
263
284
|
// src/core/generate.ts
|
|
@@ -277,11 +298,14 @@ async function planWorkflow(root, facts, opts) {
|
|
|
277
298
|
const abs = path.join(root, rel);
|
|
278
299
|
const exists = await pathExists(abs);
|
|
279
300
|
const existing = exists ? await readText(abs) : null;
|
|
301
|
+
const differs = exists ? existing !== contents : void 0;
|
|
302
|
+
const origin = differs && existing !== null ? fileOrigin(existing) : void 0;
|
|
280
303
|
out.push({
|
|
281
304
|
path: rel,
|
|
282
305
|
contents,
|
|
283
306
|
exists,
|
|
284
|
-
differs
|
|
307
|
+
differs,
|
|
308
|
+
drift: origin === "pack" ? "stale" : origin,
|
|
285
309
|
executable,
|
|
286
310
|
label
|
|
287
311
|
});
|