etymd 0.9.1 → 0.11.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 (39) hide show
  1. package/CHANGELOG.md +120 -14
  2. package/README.md +50 -2
  3. package/dist/{approve-HED3NCTE.js → approve-OUJ4UPGK.js} +5 -5
  4. package/dist/audit-A75FVXHD.js +12 -0
  5. package/dist/{brief-2CXAUDNN.js → brief-MYUESUMM.js} +7 -7
  6. package/dist/{chunk-ULM25GWG.js → chunk-2KVQBL4E.js} +1 -1
  7. package/dist/{chunk-V2SOHPXF.js → chunk-2VLNI3L2.js} +1 -1
  8. package/dist/{chunk-OAFYLBVG.js → chunk-3E2IPCRY.js} +3 -3
  9. package/dist/{chunk-IV3FYVTS.js → chunk-4VPBP6K6.js} +4 -3
  10. package/dist/{chunk-UN7IAYM6.js → chunk-5GQ52DK6.js} +5 -5
  11. package/dist/{chunk-D2RI3CCT.js → chunk-DWL2IKZH.js} +12 -3
  12. package/dist/{chunk-JSUYV26K.js → chunk-F75Q43BC.js} +4 -4
  13. package/dist/{chunk-EF6BPBG6.js → chunk-GWGKEPRX.js} +2 -2
  14. package/dist/{chunk-WENIJMVV.js → chunk-HRJJQCMT.js} +54 -18
  15. package/dist/{chunk-AQM4GYGN.js → chunk-J7XVOTZL.js} +1 -1
  16. package/dist/{chunk-C7LBUFNU.js → chunk-JHZ2BN4U.js} +3 -3
  17. package/dist/{chunk-KGVRZ6WC.js → chunk-JVLWM27I.js} +9 -9
  18. package/dist/{chunk-6DUDIVIC.js → chunk-LSZGCKIQ.js} +1 -1
  19. package/dist/cli.js +19 -19
  20. package/dist/config-XAH6PA5G.js +4 -0
  21. package/dist/{context-5ANOP2IJ.js → context-F63RSIBH.js} +4 -4
  22. package/dist/doctor-OZQTOUFR.js +19 -0
  23. package/dist/{fleet-DPJSZDEY.js → fleet-Z7W3QBEV.js} +18 -18
  24. package/dist/{gates-RR3V4PII.js → gates-47YTMMLZ.js} +10 -10
  25. package/dist/generate-PMCP37DC.js +6 -0
  26. package/dist/index.d.ts +9 -1
  27. package/dist/index.js +192 -147
  28. package/dist/{init-RC35RS5J.js → init-QLGIYFCX.js} +10 -10
  29. package/dist/ledger-4FOZ5HAB.js +5 -0
  30. package/dist/{scan-MBXSD5RO.js → scan-VWEXWM6G.js} +5 -5
  31. package/dist/scan-W7RHZVSF.js +5 -0
  32. package/dist/{screen-EHNK6ANK.js → screen-NSWNC7VQ.js} +125 -16
  33. package/package.json +4 -4
  34. package/dist/audit-Y35XVD6Z.js +0 -12
  35. package/dist/config-7WCVZCM6.js +0 -4
  36. package/dist/doctor-DXXQJ2L3.js +0 -19
  37. package/dist/generate-IPY5QIR5.js +0 -6
  38. package/dist/ledger-B2XGANTH.js +0 -5
  39. package/dist/scan-KX522VKV.js +0 -5
package/CHANGELOG.md CHANGED
@@ -1,10 +1,116 @@
1
1
  # etymd
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1193d71: Content screen: allow-file entries carry provenance, and the env bypasses are gone
8
+
9
+ Every exemption is a hole in the gate, and a hole with no name on it lives forever — nobody can
10
+ ask "is this still needed?" about an entry nobody signed. Repo-level exceptions now live in
11
+ `.etymd-screen-allow` (the previous `.artifact-check-allow` is still honoured) as records of
12
+ labeled lines:
13
+
14
+ ```
15
+ pattern ^/Users/someone
16
+ reason test fixture for machine-path detector
17
+ date 2026-08-15
18
+ author owner
19
+ ```
20
+
21
+ The pattern is the rest of its line, verbatim — it may contain any character, `|` included,
22
+ without escaping. Delimiting a free-form field in-band would be an ambiguity in the format
23
+ itself, and every parser-side guard against it only makes the misparses you imagined loud while
24
+ leaving the rest silent; the line break is the one boundary the field cannot contain.
25
+
26
+ An entry naming the repo itself needs no provenance — a bare `^widget$` line is a complete
27
+ record, the exemption being exactly as wide as the name. Anything else missing a field is
28
+ reported and does not apply, so an unprovenanced file reads as noise to fix, not as silence to
29
+ trust.
30
+
31
+ The two environment bypasses the generated hooks carried — `CONTENT_GATE_PREPUSH` and
32
+ `ARTIFACT_CHECK_SKIP` — are removed. They were off-switches invisible in the tree: nothing in
33
+ the repo records that a gate was skipped, or why. The allow file is the one bypass path left,
34
+ and every entry in it says who exempted what, when, and for what reason.
35
+
36
+ Generated hooks now resolve their screener in a defined order: an explicit `CONTENT_GATE`, then
37
+ the repo's own `./dist/cli.js` when it builds one, then the `etymd` on PATH. The middle step is
38
+ the dogfood case — a repo developing the screener must gate on its own unreleased build, or its
39
+ hooks enforce the last published behaviour against a tree that has moved past it. Repos without
40
+ a `dist/` resolve exactly as before. Pack v8 — run `etymd gates` to pick it up.
41
+
42
+ ### Patch Changes
43
+
44
+ - 652a353: Dependency advisories, 2026-08 batch: vitest 2.1.8 → 4.1.10, @changesets/cli 2.27.11 → 2.31.1,
45
+ tsup 8.3.5 → 8.5.1, plus in-range transitive fixes (js-yaml, nanoid, postcss, tmp)
46
+
47
+ Clears the whole advisory list reachable in range, then the vitest 2 → 4 major the rest of it
48
+ needed — the deliberate migration, not an audit-chased one: all 260 tests pass on 4.1.10
49
+ unchanged, config needed nothing, and the esbuild/vite/vitest development-server exposure class
50
+ (file read and execution via a listening dev server) is closed rather than reasoned around.
51
+
52
+ What remains is one low: esbuild 0.27.3–0.28.0, arbitrary file read when running a development
53
+ server on Windows, reached only through tsup (which pins ^0.27.0). Forcing an override past
54
+ tsup's declared range is audit-chasing a code path this repo never enters — nothing in it
55
+ starts a listening dev server — so it waits for a tsup release that carries esbuild 0.28.
56
+
57
+ - 9521732: Content screen: a clean run reports itself, with the file count
58
+
59
+ A clean `etymd screen` printed nothing at all — the summary header, file count and binary-skip
60
+ count only appeared when there were findings. From inside a hook, where the exit code is the
61
+ only other signal, "silent" and "never looked" were indistinguishable, which is the exact
62
+ failure the binary-skip disclosure exists to prevent (0.9.1 claimed the count is always
63
+ reported; on a clean run it never was).
64
+
65
+ Every run now prints its summary line — scope, files scanned, binary skipped — and a clean run
66
+ says so. Hooks gain one line per successful commit; a silent screen remains possible only
67
+ where the screener is absent, which the tool already reports as inert rather than clean.
68
+
69
+ ## 0.10.0
70
+
71
+ ### Minor Changes
72
+
73
+ - 2efbd34: `etymd gates` can now check the commit subject against Conventional Commits, at the commit-msg
74
+ door the content screen already used. Pack v7 — run `etymd gates` to pick it up.
75
+
76
+ **Off unless you ask for it.** Set `gates.commitFormat: true` in `.etymd/config.json`; anything
77
+ else, including leaving the key out, writes exactly the hook you got before this release. It is
78
+ the one generated check that needs nothing installed and would therefore run for everyone who
79
+ clones your repo — every other check the pack writes is either derived from what your repo
80
+ already does, or inert without a checker you installed yourself. A commit convention is neither.
81
+ It is an opinion, and this tool does not hold opinions on your behalf.
82
+
83
+ Where you do turn it on, it is a format check and not a taste check: it asks whether a machine
84
+ can classify the subject and stops there. An over-long subject is advice and never blocks, and
85
+ the subjects git writes for you (merge, revert, fixup, squash, amend) are exempt.
86
+
87
+ ### Patch Changes
88
+
89
+ - 5104394: Content screen: recognise a repo's own name however the pattern spells it
90
+
91
+ The self-name exemption compared a pattern's raw source to the repo's directory name. Once a
92
+ pattern file started listing names word-anchored — `\bwidget\b` rather than `widget` — no source
93
+ matched any name, and the exemption silently stopped applying: a repo began reporting its own
94
+ `package.json`, its own contract titles and its own storage keys as leaks, enough of them to bury
95
+ anything else in the report. Nothing failed; the code, its comment and its test all still read as
96
+ correct.
97
+
98
+ The comparison is now of meaning rather than spelling. A pattern is exempt when the single
99
+ string it matches is one of the names the repo can prove is its own, with word anchors accepted
100
+ because they change where a match may begin, never which string matches. Any pattern that can
101
+ match more than one string — a class, a quantifier, an alternation — is never exempt, so an
102
+ exemption stays exactly as wide as a name.
103
+
104
+ "Its own name" also widens from the worktree directory to the union of the directory, the
105
+ `name` in `package.json` (npm scope stripped) and the basename of the `origin` remote, since
106
+ those three routinely disagree. Each is read from the repo being screened, so a repo can only
107
+ ever exempt itself; where none can be read the union is empty and every pattern stays active.
108
+
3
109
  ## 0.9.1
4
110
 
5
111
  ### Patch Changes
6
112
 
7
- - 8967d26: Content screen: skip binary files, and say how many were skipped
113
+ - 91ddb97: Content screen: skip binary files, and say how many were skipped
8
114
 
9
115
  Compressed bytes contain any short sequence eventually, so a short pattern
10
116
  matches inside binary assets by accident and is reported as a "line" of
@@ -24,7 +130,7 @@
24
130
 
25
131
  ### Minor Changes
26
132
 
27
- - 3cf2e53: State documents are now checked for truth, not only for age: the instruction-truth lens runs its
133
+ - 35dbeab: State documents are now checked for truth, not only for age: the instruction-truth lens runs its
28
134
  command-claim and path-claim checks over detected state docs, and resolves their `D-NNN` decision
29
135
  references against the repo's decisions file — a citation of an entry that was never written is a
30
136
  gap finding. References naming another record ("peer D-050") and citations against directory
@@ -35,12 +141,12 @@
35
141
 
36
142
  ### Minor Changes
37
143
 
38
- - f2c1bfd: `etymd gates` no longer writes a gate that cannot fail. It derives whether any risk-tier finding is
144
+ - bad1380: `etymd gates` no longer writes a gate that cannot fail. It derives whether any risk-tier finding is
39
145
  reachable in the repo (a package manifest to contradict, a state document to fall behind); where
40
146
  none is, the generated hook drops to `--fail-on gap` and the output says so and why. A `failOn`
41
147
  recorded in `.etymd/config.json` is never adjusted, and `gates` now states which tier it wrote and
42
148
  where that tier came from — the config key was previously unmentioned anywhere in its output.
43
- - 743ad44: Declared-field checks are forward-only from the marker's position, not from the file. A decisions
149
+ - e873664: Declared-field checks are forward-only from the marker's position, not from the file. A decisions
44
150
  file that declares required entry fields mid-life no longer demands them from the entries appended
45
151
  above the marker — an append-only record cannot backfill them. The built-in `Scope:` check shares
46
152
  the same gate; `Revisit:` keeps whole-file reach, since it fires only where the entry already wrote
@@ -50,7 +156,7 @@
50
156
 
51
157
  ### Minor Changes
52
158
 
53
- - 7371c8b: Decisions records can require fields of their own, and etymd holds no opinion about which.
159
+ - d27d635: Decisions records can require fields of their own, and etymd holds no opinion about which.
54
160
 
55
161
  A decisions file already opts into per-entry format checks with a marker. It can now append field
56
162
  names to it — `<!-- decisions-format: 1 fields=Owner,Rollback -->` — and every entry after the
@@ -86,7 +192,7 @@
86
192
 
87
193
  ### Minor Changes
88
194
 
89
- - b02943b: Local gates: the tool now reads and rewrites what it generated, instead of guessing about it.
195
+ - 47c5032: Local gates: the tool now reads and rewrites what it generated, instead of guessing about it.
90
196
 
91
197
  Two defects in the local-gate machinery, both silent, both affecting every project using
92
198
  `etymd gates`. Decision record:
@@ -164,7 +270,7 @@
164
270
 
165
271
  ### Minor Changes
166
272
 
167
- - e199ccc: `fleet add --profile corp` now records the alias-to-directory mapping too, not just the entry.
273
+ - e265713: `fleet add --profile corp` now records the alias-to-directory mapping too, not just the entry.
168
274
 
169
275
  A corp entry in the tracked manifest is deliberately alias-only — no path, no remote — which is
170
276
  what keeps employer names out of a file that gets pushed. It also means the entry resolves to
@@ -185,7 +291,7 @@
185
291
  without an entry is inert, while an entry without a mapping is a broken registration sitting in
186
292
  the file that gets committed.
187
293
 
188
- - 9546979: `fleet add` no longer records a remote URL in the manifest.
294
+ - e8ed2ba: `fleet add` no longer records a remote URL in the manifest.
189
295
 
190
296
  The field was write-only: nothing in the tool ever read it back. It was persisted because it
191
297
  happened to be derivable at registration time, and it stayed because no one asked what consumed
@@ -204,7 +310,7 @@
204
310
 
205
311
  ### Patch Changes
206
312
 
207
- - 52ebca8: Fix: `fleet add --profile corp` was silently ignored, registering employer repos as personal.
313
+ - 88492b5: Fix: `fleet add --profile corp` was silently ignored, registering employer repos as personal.
208
314
 
209
315
  `fleet` declares its own `--profile` (the sweep filter), and commander hands a parent-declared
210
316
  option the value even when it is typed after the subcommand. So `fleet add <dir> --profile corp`
@@ -225,7 +331,7 @@
225
331
 
226
332
  ### Patch Changes
227
333
 
228
- - 915ae78: Fix: the shell gate now actually prints the sub-warning findings it documented.
334
+ - 821f144: Fix: the shell gate now actually prints the sub-warning findings it documented.
229
335
 
230
336
  0.4.0 said "style and info print as advice" and did not do it — the generated hook ran only
231
337
  `shellcheck -S warning` and discarded everything below that bar. The generated file made no such
@@ -246,7 +352,7 @@
246
352
 
247
353
  ### Minor Changes
248
354
 
249
- - 69d1aad: Gate the shell surface: `etymd gates` now installs a shellcheck step in repos that have one.
355
+ - 59c506d: Gate the shell surface: `etymd gates` now installs a shellcheck step in repos that have one.
250
356
 
251
357
  Package scripts are not the only executable surface a repo has, and in some repos they are not
252
358
  the main one. A tools or infra repo can be entirely `bootstrap/*.sh` plus `.githooks/*` with no
@@ -363,7 +469,7 @@
363
469
 
364
470
  ### Patch Changes
365
471
 
366
- - 80db483: Gate integrity: detect scripts regardless of how the package manager is invoked.
472
+ - 7bc6cf5: Gate integrity: detect scripts regardless of how the package manager is invoked.
367
473
 
368
474
  Script expansion guessed the script name positionally — the token straight after the
369
475
  manager. That only holds for `npm run x`, `yarn x` and `pnpm x`. Every other live shape
@@ -392,7 +498,7 @@
392
498
 
393
499
  ### Patch Changes
394
500
 
395
- - fdf6b6e: Fleet sweep: recurring classes + `placement: "none"` honored.
501
+ - 983c0e5: Fleet sweep: recurring classes + `placement: "none"` honored.
396
502
 
397
503
  - **Recurring classes** — the sweep report groups open findings by their engine-minted
398
504
  class prefix and lists every class present in ≥2 projects (worst tier first). A class
@@ -410,7 +516,7 @@
410
516
 
411
517
  ### Minor Changes
412
518
 
413
- - 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).
519
+ - a336902: 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).
414
520
 
415
521
  - 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.
416
522
  - 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.
package/README.md CHANGED
@@ -223,6 +223,7 @@ Every key is optional; omit the file entirely and the defaults below apply.
223
223
  "commands": ["typecheck", "lint"], // pre-push steps, in order
224
224
  "failOn": "risk", // audit tier that fails the push: risk | gap | polish
225
225
  "publishGate": true, // screen the published artifact
226
+ "commitFormat": true, // check the commit subject — Conventional Commits, off unless set
226
227
  "allowWriting": [], // commands allowed into a gate despite writing
227
228
  // Why a value here is what it is. Each key mirrors the field it explains, so the
228
229
  // note says what it refers to instead of sitting near it and hoping. Etymd keeps
@@ -337,8 +338,55 @@ packaged into a published release while every git-scoped check passes forever.
337
338
 
338
339
  Every generated hook resolves the screener at run time and **no-ops when it is absent**, so the
339
340
  same hook file is safe to commit to a public repo: it carries no patterns and imposes no policy
340
- on anyone who clones it. A deliberate exception is marked inline with `allow-published-string`,
341
- visible in the diff rather than hidden in an allowlist.
341
+ on anyone who clones it. A deliberate exception on a line you can edit is marked inline with
342
+ `allow-published-string`, visible in the diff.
343
+
344
+ Some exemptions cannot live on the line itself: a scanner's own source contains the strings it
345
+ screens for, its tests contain fixtures that must match, and a bundler strips comments so an
346
+ inline marker would not survive into the artifact. Those live in `.etymd-screen-allow` at the
347
+ repo root — one labeled line per field, so the pattern is never delimited:
348
+
349
+ ```
350
+ pattern ^AcmeCorp|BetaCorp$
351
+ reason fixture proving the detector fires on either name
352
+ date 2026-08-15
353
+ author someone
354
+ ```
355
+
356
+ A pattern may contain any character, including the `|` shown above, without escaping: a
357
+ free-form field delimited in-band is an ambiguity in the format itself, and no amount of
358
+ parser-side guarding fixes that class — labels move the boundary to the line break, which the
359
+ field cannot contain. An entry naming the repo itself needs no provenance — a bare `^widget$`
360
+ line is a complete record, the exemption being exactly as wide as the name. Anything else
361
+ missing a field is reported and does not apply: an exemption is a hole in the gate, and a hole
362
+ nobody signed cannot be audited later. The file is read from the repo being screened, never a
363
+ shared location, and it screens itself out (it necessarily contains every string it exempts).
364
+
365
+ ### The commit subject, if you ask for it
366
+
367
+ Off unless you turn it on:
368
+
369
+ ```json
370
+ { "gates": { "commitFormat": true } }
371
+ ```
372
+
373
+ in `.etymd/config.json`. Then the `commit-msg` hook also checks the subject against
374
+ [Conventional Commits](https://www.conventionalcommits.org) — `<type>[(scope)][!]: <summary>`,
375
+ with `feat fix docs style refactor perf test build ci chore revert` as the types. It is a format
376
+ check and not a taste check: it asks whether a machine can classify the line, and stops there.
377
+ An over-long subject is reported as advice and never blocks, and the subjects git writes for you
378
+ — merge, revert, fixup, squash, amend — are exempt, since gating those would ask you to rewrite
379
+ text you did not write.
380
+
381
+ It is the one generated check that needs nothing installed, and therefore the one that would run
382
+ for everyone who clones your repo. That is exactly why it is off by default. Every other check
383
+ the pack writes is either derived from what your repo already does or inert without a checker you
384
+ installed yourself; a commit convention is neither — it is an opinion, and this tool does not
385
+ hold opinions on your behalf.
386
+
387
+ Turn it on where the convention is already yours. A convention nobody gates does not get
388
+ abandoned in a decision you could point at: it erodes one hurried commit at a time, and by the
389
+ time the log reads as a mixture, every commit in it is already published.
342
390
 
343
391
  Modeled on this repo's own workflow (Etymd guards its own instructions with Etymd — its CI runs
344
392
  the same gate against its own freshly built CLI):
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { section, theme, print, renderBaselineDrift, glyph } from './chunk-5BVKFJWM.js';
3
- import { scanProject } from './chunk-UN7IAYM6.js';
4
- import { VERSION } from './chunk-AQM4GYGN.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';
3
+ import { scanProject } from './chunk-5GQ52DK6.js';
4
+ import { VERSION } from './chunk-J7XVOTZL.js';
5
+ import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-JHZ2BN4U.js';
6
+ import { PACK_VERSION } from './chunk-LSZGCKIQ.js';
7
+ import './chunk-4VPBP6K6.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-2KVQBL4E.js';
3
+ import './chunk-JVLWM27I.js';
4
+ import './chunk-F75Q43BC.js';
5
+ import './chunk-3E2IPCRY.js';
6
+ import './chunk-DWL2IKZH.js';
7
+ import './chunk-5BVKFJWM.js';
8
+ import './chunk-5GQ52DK6.js';
9
+ import './chunk-J7XVOTZL.js';
10
+ import './chunk-JHZ2BN4U.js';
11
+ import './chunk-LSZGCKIQ.js';
12
+ import './chunk-4VPBP6K6.js';
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { print, theme } from './chunk-5BVKFJWM.js';
3
- import { scanProject } from './chunk-UN7IAYM6.js';
4
- import './chunk-AQM4GYGN.js';
5
- import { ETYMD_DIR } from './chunk-C7LBUFNU.js';
6
- import './chunk-6DUDIVIC.js';
7
- import './chunk-IV3FYVTS.js';
8
- import { promises } from 'node:fs';
9
- import path from 'node:path';
3
+ import { scanProject } from './chunk-5GQ52DK6.js';
4
+ import './chunk-J7XVOTZL.js';
5
+ import { ETYMD_DIR } from './chunk-JHZ2BN4U.js';
6
+ import './chunk-LSZGCKIQ.js';
7
+ import './chunk-4VPBP6K6.js';
8
+ import { promises } from 'fs';
9
+ import path from 'path';
10
10
 
11
11
  async function run(opts) {
12
12
  const facts = await scanProject(opts.cwd);
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-KGVRZ6WC.js';
2
+ import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-JVLWM27I.js';
3
3
  import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-5BVKFJWM.js';
4
4
 
5
5
  // src/commands/audit.ts
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readLedger, resolveEntry, writeLedger } from './chunk-OAFYLBVG.js';
2
+ import { readLedger, resolveEntry, writeLedger } from './chunk-3E2IPCRY.js';
3
3
  import { print, renderLedger, glyph, theme } from './chunk-5BVKFJWM.js';
4
4
 
5
5
  // src/commands/ledger.ts
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { readJson } from './chunk-IV3FYVTS.js';
3
- import { promises } from 'node:fs';
4
- import path from 'node:path';
2
+ import { readJson } from './chunk-4VPBP6K6.js';
3
+ import { promises } from 'fs';
4
+ import path from 'path';
5
5
 
6
6
  function ledgerPath(root) {
7
7
  return path.join(root, ".etymd", "ledger.json");
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { execFile } from 'node:child_process';
3
- import { promises } from 'node:fs';
4
- import { promisify } from 'node:util';
2
+ import { execFile } from 'child_process';
3
+ import { promises } from 'fs';
4
+ import 'path';
5
+ import { promisify } from 'util';
5
6
 
6
7
  var pExecFile = promisify(execFile);
7
8
  async function pathExists(p) {
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-AQM4GYGN.js';
3
- import { PACK_VERSION } from './chunk-6DUDIVIC.js';
4
- import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-IV3FYVTS.js';
5
- import path from 'node:path';
6
- import { promises } from 'node:fs';
2
+ import { VERSION } from './chunk-J7XVOTZL.js';
3
+ import { PACK_VERSION } from './chunk-LSZGCKIQ.js';
4
+ import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-4VPBP6K6.js';
5
+ import path from 'path';
6
+ import { promises } from 'fs';
7
7
 
8
8
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
9
9
  "node_modules",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { ETYMD_DIR } from './chunk-C7LBUFNU.js';
3
- import { pathExists, readText } from './chunk-IV3FYVTS.js';
4
- import path from 'node:path';
2
+ import { ETYMD_DIR } from './chunk-JHZ2BN4U.js';
3
+ import { pathExists, readText } from './chunk-4VPBP6K6.js';
4
+ import path from 'path';
5
5
 
6
6
  var CONFIG_FILE = path.join(ETYMD_DIR, "config.json");
7
7
  var DEFAULT_CONFIG = {
@@ -70,6 +70,14 @@ async function readConfig(root) {
70
70
  }
71
71
  return gates.publishGate;
72
72
  })();
73
+ const commitFormat = (() => {
74
+ if (gates.commitFormat === void 0) return void 0;
75
+ if (typeof gates.commitFormat !== "boolean") {
76
+ problems.push(`${CONFIG_FILE}: \`gates.commitFormat\` must be true or false \u2014 ignored.`);
77
+ return void 0;
78
+ }
79
+ return gates.commitFormat;
80
+ })();
73
81
  return {
74
82
  present: true,
75
83
  problems,
@@ -91,6 +99,7 @@ async function readConfig(root) {
91
99
  commands: readGlobList(gates.commands, "gates.commands", problems) ?? DEFAULT_CONFIG.gates.commands,
92
100
  failOn: failOn ?? DEFAULT_CONFIG.gates.failOn,
93
101
  publishGate,
102
+ commitFormat,
94
103
  allowWriting: readGlobList(gates.allowWriting, "gates.allowWriting", problems) ?? DEFAULT_CONFIG.gates.allowWriting
95
104
  }
96
105
  }
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_CONFIG } from './chunk-D2RI3CCT.js';
3
- import { readText, wordCount, approxTokens, pathExists } from './chunk-IV3FYVTS.js';
4
- import { promises } from 'node:fs';
5
- import path from 'node:path';
2
+ import { DEFAULT_CONFIG } from './chunk-DWL2IKZH.js';
3
+ import { readText, wordCount, approxTokens, pathExists } from './chunk-4VPBP6K6.js';
4
+ import { promises } from 'fs';
5
+ import path from 'path';
6
6
 
7
7
  var ALWAYS_LOADED = [
8
8
  { path: "AGENTS.md", role: "operating contract" },
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { promises } from 'node:fs';
3
- import path from 'node:path';
2
+ import { promises } from 'fs';
3
+ import path from 'path';
4
4
 
5
5
  async function applyFiles(root, files, overwrite = /* @__PURE__ */ new Set()) {
6
6
  const written = [];
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_CONFIG } from './chunk-D2RI3CCT.js';
3
- import { PACK_VERSION } from './chunk-6DUDIVIC.js';
4
- import { readText, pathExists } from './chunk-IV3FYVTS.js';
5
- import path from 'node:path';
6
- import { createHash } from 'node:crypto';
2
+ import { DEFAULT_CONFIG } from './chunk-DWL2IKZH.js';
3
+ import { PACK_VERSION } from './chunk-LSZGCKIQ.js';
4
+ import { readText, pathExists } from './chunk-4VPBP6K6.js';
5
+ import path from 'path';
6
+ import { createHash } from 'crypto';
7
7
 
8
8
  var GENERATION_MARKER_RE = /^(?:# |<!-- )etymd:generated pack-v\S+ ([0-9a-f]{16})(?: -->)?$/;
9
9
  function digestOf(body) {
@@ -132,7 +132,7 @@ ${[
132
132
  "md"
133
133
  );
134
134
  }
135
- var CONTENT_GATE_RESOLUTION = `GATE="\${CONTENT_GATE:-$(command -v etymd || true)}"`;
135
+ var CONTENT_GATE_RESOLUTION = `GATE="\${CONTENT_GATE:-$(if [ -x ./dist/cli.js ]; then echo ./dist/cli.js; else command -v etymd || true; fi)}"`;
136
136
  function localHookCall(hook) {
137
137
  return `# Repo-owned checks. This file is generated and will be overwritten; \`.githooks/${hook}.local\`
138
138
  # is yours \u2014 etymd never reads, writes, or regenerates it. Put project-specific guards there.
@@ -161,9 +161,48 @@ fi
161
161
  exit 0
162
162
  `);
163
163
  }
164
- function generateCommitMsgHook() {
164
+ var COMMIT_TYPES = [
165
+ "feat",
166
+ "fix",
167
+ "docs",
168
+ "style",
169
+ "refactor",
170
+ "perf",
171
+ "test",
172
+ "build",
173
+ "ci",
174
+ "chore",
175
+ "revert"
176
+ ];
177
+ var SUBJECT_ADVISORY_LENGTH = 72;
178
+ function commitFormatStep() {
179
+ const types = COMMIT_TYPES.join("|");
180
+ return `
181
+ # Message format \u2014 <type>[(scope)][!]: <summary>. Needs nothing installed, so it always runs.
182
+ subject=$(sed -e '/^#/d' -e '/^[[:space:]]*$/d' "$1" | head -1)
183
+ case "$subject" in
184
+ "Merge "*|"Revert "*|fixup!*|squash!*|amend!*) ;;
185
+ *)
186
+ if ! printf '%s' "$subject" | grep -qE '^(${types})(\\([a-z0-9._/-]+\\))?!?: .+'; then
187
+ echo "\u2717 commit message: expected '<type>[(scope)][!]: <summary>'"
188
+ echo " got: $subject"
189
+ echo " types: ${COMMIT_TYPES.join(" ")}"
190
+ echo " a '!' after the type or scope marks a breaking change"
191
+ exit 1
192
+ fi
193
+ # Length is advice, not a block: the format is what tooling reads, the length is what a
194
+ # person reads, and only one of the two can break anything.
195
+ if [ "\${#subject}" -gt ${SUBJECT_ADVISORY_LENGTH} ]; then
196
+ echo "\u203A note: subject is \${#subject} characters; ${SUBJECT_ADVISORY_LENGTH} or fewer reads better in git log"
197
+ fi
198
+ ;;
199
+ esac`;
200
+ }
201
+ function generateCommitMsgHook(gates) {
202
+ const format = gates?.commitFormat === true ? `${commitFormatStep()}
203
+ ` : "";
165
204
  return stampGenerated(`#!/usr/bin/env sh
166
- # etymd: content screen \u2014 the commit message itself.
205
+ # etymd: the commit message itself \u2014 content screen, then format.
167
206
  #
168
207
  # The staged-content gate reads file bytes and never sees the message, yet a message is as
169
208
  # permanently published as any file. No-op where no checker is installed.
@@ -173,7 +212,7 @@ GATE="\${COMMIT_MSG_GATE:-$(command -v etymd || true)}"
173
212
  if [ -x "$GATE" ]; then
174
213
  "$GATE" screen --message "$1" || exit 1
175
214
  fi
176
-
215
+ ${format}
177
216
  ${localHookCall("commit-msg")}
178
217
 
179
218
  exit 0
@@ -241,7 +280,7 @@ ${auditStep}
241
280
  # with --no-verify and anything a rebase or merge brought in from elsewhere. Advisory here (it
242
281
  # never blocks the push): the blocking decision belongs at commit time, where the fix is cheap.
243
282
  ${CONTENT_GATE_RESOLUTION}
244
- if [ -x "$GATE" ] && [ "\${CONTENT_GATE_PREPUSH:-1}" = "1" ]; then
283
+ if [ -x "$GATE" ]; then
245
284
  "$GATE" screen --tree --advisory || true
246
285
  fi
247
286
 
@@ -255,14 +294,10 @@ function generateArtifactCheckScript() {
255
294
  # Wire it into the irreversible moment:
256
295
  # package.json \u2192 "prepublishOnly": "./scripts/artifact-check.sh"
257
296
  #
258
- # No-op where no checker is installed. Bypass is deliberate and loud: ARTIFACT_CHECK_SKIP=1.
297
+ # The artifact gate is the one check that sees what actually SHIPS \u2014 bypass with
298
+ # .etymd-screen-allow entries (with provenance) if you must exempt a string.
259
299
  set -eu
260
300
 
261
- if [ "\${ARTIFACT_CHECK_SKIP:-0}" = "1" ]; then
262
- echo "\u203A artifact-check: SKIPPED by ARTIFACT_CHECK_SKIP=1"
263
- exit 0
264
- fi
265
-
266
301
  ${CONTENT_GATE_RESOLUTION}
267
302
  [ -x "$GATE" ] || { echo "\u203A artifact-check: no checker installed \u2014 skipping."; exit 0; }
268
303
 
@@ -336,14 +371,15 @@ async function planWorkflow(root, facts, opts) {
336
371
  await add(".githooks/pre-commit", generatePreCommitHook(), "Process gate (pre-commit)", true);
337
372
  await add(
338
373
  ".githooks/commit-msg",
339
- generateCommitMsgHook(),
340
- "Content screen (commit message)",
374
+ generateCommitMsgHook(opts.gateConfig),
375
+ "Message gate (content screen, format)",
341
376
  true
342
377
  );
343
378
  const gateConfig = opts.gateConfig?.commands?.length ? opts.gateConfig : {
344
379
  commands: derivedCommands(facts, existingPrePush ?? void 0),
345
380
  failOn: opts.gateConfig?.failOn ?? DEFAULT_CONFIG.gates.failOn,
346
381
  publishGate: opts.gateConfig?.publishGate,
382
+ commitFormat: opts.gateConfig?.commitFormat,
347
383
  allowWriting: opts.gateConfig?.allowWriting ?? []
348
384
  };
349
385
  await add(
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // package.json
3
3
  var package_default = {
4
- version: "0.9.1"};
4
+ version: "0.11.0"};
5
5
 
6
6
  // src/version.ts
7
7
  var VERSION = package_default.version;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { readJson } from './chunk-IV3FYVTS.js';
3
- import { promises } from 'node:fs';
4
- import path from 'node:path';
2
+ import { readJson } from './chunk-4VPBP6K6.js';
3
+ import { promises } from 'fs';
4
+ import path from 'path';
5
5
 
6
6
  var ETYMD_DIR = ".etymd";
7
7
  var CACHE_DIR = path.join(ETYMD_DIR, "cache");
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { measureContext } from './chunk-JSUYV26K.js';
3
- import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-OAFYLBVG.js';
4
- import { DEFAULT_CONFIG, readConfig, CONFIG_FILE } from './chunk-D2RI3CCT.js';
5
- import { scanProject, expandFileGlobs } from './chunk-UN7IAYM6.js';
6
- import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-C7LBUFNU.js';
7
- import { PACK_VERSION } from './chunk-6DUDIVIC.js';
8
- import { pathExists, readJson, readText, isDirectory, matchesAnyGlob, git, normalizeRelPath, isCiEnvironment, isExecutable } from './chunk-IV3FYVTS.js';
9
- import path from 'node:path';
2
+ import { measureContext } from './chunk-F75Q43BC.js';
3
+ import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-3E2IPCRY.js';
4
+ import { DEFAULT_CONFIG, readConfig, CONFIG_FILE } from './chunk-DWL2IKZH.js';
5
+ import { scanProject, expandFileGlobs } from './chunk-5GQ52DK6.js';
6
+ import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-JHZ2BN4U.js';
7
+ import { PACK_VERSION } from './chunk-LSZGCKIQ.js';
8
+ import { pathExists, readJson, readText, isDirectory, matchesAnyGlob, git, normalizeRelPath, isCiEnvironment, isExecutable } from './chunk-4VPBP6K6.js';
9
+ import path from 'path';
10
10
  import YAML from 'yaml';
11
- import { promises } from 'node:fs';
11
+ import { promises } from 'fs';
12
12
 
13
13
  // src/engine/finding.ts
14
14
  var TIER_ORDER = { risk: 0, gap: 1, polish: 2 };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  // src/pack/version.ts
3
- var PACK_VERSION = "6";
3
+ var PACK_VERSION = "8";
4
4
 
5
5
  export { PACK_VERSION };