etymd 0.9.1 → 0.10.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 (29) hide show
  1. package/CHANGELOG.md +54 -14
  2. package/README.md +27 -0
  3. package/dist/{approve-HED3NCTE.js → approve-6TMXJLOI.js} +3 -3
  4. package/dist/audit-FGDX2AYS.js +12 -0
  5. package/dist/{brief-2CXAUDNN.js → brief-X64HK325.js} +3 -3
  6. package/dist/{chunk-JSUYV26K.js → chunk-IPOAD67U.js} +1 -1
  7. package/dist/{chunk-AQM4GYGN.js → chunk-K34AVLDP.js} +1 -1
  8. package/dist/{chunk-KGVRZ6WC.js → chunk-KQMQJJZK.js} +4 -4
  9. package/dist/{chunk-6DUDIVIC.js → chunk-OIYNP6Y6.js} +1 -1
  10. package/dist/{chunk-WENIJMVV.js → chunk-R6CVJ5D7.js} +47 -7
  11. package/dist/{chunk-ULM25GWG.js → chunk-SGW7OPHZ.js} +1 -1
  12. package/dist/{chunk-UN7IAYM6.js → chunk-WTD2EIMN.js} +2 -2
  13. package/dist/{chunk-D2RI3CCT.js → chunk-YVGC3E6U.js} +9 -0
  14. package/dist/cli.js +15 -15
  15. package/dist/{config-7WCVZCM6.js → config-TPK5LYDM.js} +1 -1
  16. package/dist/{context-5ANOP2IJ.js → context-K6DZNWAV.js} +2 -2
  17. package/dist/{doctor-DXXQJ2L3.js → doctor-W3UZK7KS.js} +7 -7
  18. package/dist/{fleet-DPJSZDEY.js → fleet-4PPNUIDJ.js} +9 -9
  19. package/dist/{gates-RR3V4PII.js → gates-Q7FMVUQP.js} +5 -5
  20. package/dist/{generate-IPY5QIR5.js → generate-NLHGTRZB.js} +3 -3
  21. package/dist/index.d.ts +9 -1
  22. package/dist/index.js +57 -8
  23. package/dist/{init-RC35RS5J.js → init-YYGRFIHQ.js} +5 -5
  24. package/dist/scan-PJSZGUG7.js +5 -0
  25. package/dist/{scan-MBXSD5RO.js → scan-VO776GM7.js} +3 -3
  26. package/dist/{screen-EHNK6ANK.js → screen-MOQP6XC7.js} +47 -8
  27. package/package.json +1 -1
  28. package/dist/audit-Y35XVD6Z.js +0 -12
  29. package/dist/scan-KX522VKV.js +0 -5
package/CHANGELOG.md CHANGED
@@ -1,10 +1,50 @@
1
1
  # etymd
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2efbd34: `etymd gates` can now check the commit subject against Conventional Commits, at the commit-msg
8
+ door the content screen already used. Pack v7 — run `etymd gates` to pick it up.
9
+
10
+ **Off unless you ask for it.** Set `gates.commitFormat: true` in `.etymd/config.json`; anything
11
+ else, including leaving the key out, writes exactly the hook you got before this release. It is
12
+ the one generated check that needs nothing installed and would therefore run for everyone who
13
+ clones your repo — every other check the pack writes is either derived from what your repo
14
+ already does, or inert without a checker you installed yourself. A commit convention is neither.
15
+ It is an opinion, and this tool does not hold opinions on your behalf.
16
+
17
+ Where you do turn it on, it is a format check and not a taste check: it asks whether a machine
18
+ can classify the subject and stops there. An over-long subject is advice and never blocks, and
19
+ the subjects git writes for you (merge, revert, fixup, squash, amend) are exempt.
20
+
21
+ ### Patch Changes
22
+
23
+ - 5104394: Content screen: recognise a repo's own name however the pattern spells it
24
+
25
+ The self-name exemption compared a pattern's raw source to the repo's directory name. Once a
26
+ pattern file started listing names word-anchored — `\bwidget\b` rather than `widget` — no source
27
+ matched any name, and the exemption silently stopped applying: a repo began reporting its own
28
+ `package.json`, its own contract titles and its own storage keys as leaks, enough of them to bury
29
+ anything else in the report. Nothing failed; the code, its comment and its test all still read as
30
+ correct.
31
+
32
+ The comparison is now of meaning rather than spelling. A pattern is exempt when the single
33
+ string it matches is one of the names the repo can prove is its own, with word anchors accepted
34
+ because they change where a match may begin, never which string matches. Any pattern that can
35
+ match more than one string — a class, a quantifier, an alternation — is never exempt, so an
36
+ exemption stays exactly as wide as a name.
37
+
38
+ "Its own name" also widens from the worktree directory to the union of the directory, the
39
+ `name` in `package.json` (npm scope stripped) and the basename of the `origin` remote, since
40
+ those three routinely disagree. Each is read from the repo being screened, so a repo can only
41
+ ever exempt itself; where none can be read the union is empty and every pattern stays active.
42
+
3
43
  ## 0.9.1
4
44
 
5
45
  ### Patch Changes
6
46
 
7
- - 8967d26: Content screen: skip binary files, and say how many were skipped
47
+ - 91ddb97: Content screen: skip binary files, and say how many were skipped
8
48
 
9
49
  Compressed bytes contain any short sequence eventually, so a short pattern
10
50
  matches inside binary assets by accident and is reported as a "line" of
@@ -24,7 +64,7 @@
24
64
 
25
65
  ### Minor Changes
26
66
 
27
- - 3cf2e53: State documents are now checked for truth, not only for age: the instruction-truth lens runs its
67
+ - 35dbeab: State documents are now checked for truth, not only for age: the instruction-truth lens runs its
28
68
  command-claim and path-claim checks over detected state docs, and resolves their `D-NNN` decision
29
69
  references against the repo's decisions file — a citation of an entry that was never written is a
30
70
  gap finding. References naming another record ("peer D-050") and citations against directory
@@ -35,12 +75,12 @@
35
75
 
36
76
  ### Minor Changes
37
77
 
38
- - f2c1bfd: `etymd gates` no longer writes a gate that cannot fail. It derives whether any risk-tier finding is
78
+ - bad1380: `etymd gates` no longer writes a gate that cannot fail. It derives whether any risk-tier finding is
39
79
  reachable in the repo (a package manifest to contradict, a state document to fall behind); where
40
80
  none is, the generated hook drops to `--fail-on gap` and the output says so and why. A `failOn`
41
81
  recorded in `.etymd/config.json` is never adjusted, and `gates` now states which tier it wrote and
42
82
  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
83
+ - e873664: Declared-field checks are forward-only from the marker's position, not from the file. A decisions
44
84
  file that declares required entry fields mid-life no longer demands them from the entries appended
45
85
  above the marker — an append-only record cannot backfill them. The built-in `Scope:` check shares
46
86
  the same gate; `Revisit:` keeps whole-file reach, since it fires only where the entry already wrote
@@ -50,7 +90,7 @@
50
90
 
51
91
  ### Minor Changes
52
92
 
53
- - 7371c8b: Decisions records can require fields of their own, and etymd holds no opinion about which.
93
+ - d27d635: Decisions records can require fields of their own, and etymd holds no opinion about which.
54
94
 
55
95
  A decisions file already opts into per-entry format checks with a marker. It can now append field
56
96
  names to it — `<!-- decisions-format: 1 fields=Owner,Rollback -->` — and every entry after the
@@ -86,7 +126,7 @@
86
126
 
87
127
  ### Minor Changes
88
128
 
89
- - b02943b: Local gates: the tool now reads and rewrites what it generated, instead of guessing about it.
129
+ - 47c5032: Local gates: the tool now reads and rewrites what it generated, instead of guessing about it.
90
130
 
91
131
  Two defects in the local-gate machinery, both silent, both affecting every project using
92
132
  `etymd gates`. Decision record:
@@ -164,7 +204,7 @@
164
204
 
165
205
  ### Minor Changes
166
206
 
167
- - e199ccc: `fleet add --profile corp` now records the alias-to-directory mapping too, not just the entry.
207
+ - e265713: `fleet add --profile corp` now records the alias-to-directory mapping too, not just the entry.
168
208
 
169
209
  A corp entry in the tracked manifest is deliberately alias-only — no path, no remote — which is
170
210
  what keeps employer names out of a file that gets pushed. It also means the entry resolves to
@@ -185,7 +225,7 @@
185
225
  without an entry is inert, while an entry without a mapping is a broken registration sitting in
186
226
  the file that gets committed.
187
227
 
188
- - 9546979: `fleet add` no longer records a remote URL in the manifest.
228
+ - e8ed2ba: `fleet add` no longer records a remote URL in the manifest.
189
229
 
190
230
  The field was write-only: nothing in the tool ever read it back. It was persisted because it
191
231
  happened to be derivable at registration time, and it stayed because no one asked what consumed
@@ -204,7 +244,7 @@
204
244
 
205
245
  ### Patch Changes
206
246
 
207
- - 52ebca8: Fix: `fleet add --profile corp` was silently ignored, registering employer repos as personal.
247
+ - 88492b5: Fix: `fleet add --profile corp` was silently ignored, registering employer repos as personal.
208
248
 
209
249
  `fleet` declares its own `--profile` (the sweep filter), and commander hands a parent-declared
210
250
  option the value even when it is typed after the subcommand. So `fleet add <dir> --profile corp`
@@ -225,7 +265,7 @@
225
265
 
226
266
  ### Patch Changes
227
267
 
228
- - 915ae78: Fix: the shell gate now actually prints the sub-warning findings it documented.
268
+ - 821f144: Fix: the shell gate now actually prints the sub-warning findings it documented.
229
269
 
230
270
  0.4.0 said "style and info print as advice" and did not do it — the generated hook ran only
231
271
  `shellcheck -S warning` and discarded everything below that bar. The generated file made no such
@@ -246,7 +286,7 @@
246
286
 
247
287
  ### Minor Changes
248
288
 
249
- - 69d1aad: Gate the shell surface: `etymd gates` now installs a shellcheck step in repos that have one.
289
+ - 59c506d: Gate the shell surface: `etymd gates` now installs a shellcheck step in repos that have one.
250
290
 
251
291
  Package scripts are not the only executable surface a repo has, and in some repos they are not
252
292
  the main one. A tools or infra repo can be entirely `bootstrap/*.sh` plus `.githooks/*` with no
@@ -363,7 +403,7 @@
363
403
 
364
404
  ### Patch Changes
365
405
 
366
- - 80db483: Gate integrity: detect scripts regardless of how the package manager is invoked.
406
+ - 7bc6cf5: Gate integrity: detect scripts regardless of how the package manager is invoked.
367
407
 
368
408
  Script expansion guessed the script name positionally — the token straight after the
369
409
  manager. That only holds for `npm run x`, `yarn x` and `pnpm x`. Every other live shape
@@ -392,7 +432,7 @@
392
432
 
393
433
  ### Patch Changes
394
434
 
395
- - fdf6b6e: Fleet sweep: recurring classes + `placement: "none"` honored.
435
+ - 983c0e5: Fleet sweep: recurring classes + `placement: "none"` honored.
396
436
 
397
437
  - **Recurring classes** — the sweep report groups open findings by their engine-minted
398
438
  class prefix and lists every class present in ≥2 projects (worst tier first). A class
@@ -410,7 +450,7 @@
410
450
 
411
451
  ### Minor Changes
412
452
 
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).
453
+ - 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
454
 
415
455
  - 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
456
  - 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
@@ -340,6 +341,32 @@ same hook file is safe to commit to a public repo: it carries no patterns and im
340
341
  on anyone who clones it. A deliberate exception is marked inline with `allow-published-string`,
341
342
  visible in the diff rather than hidden in an allowlist.
342
343
 
344
+ ### The commit subject, if you ask for it
345
+
346
+ Off unless you turn it on:
347
+
348
+ ```json
349
+ { "gates": { "commitFormat": true } }
350
+ ```
351
+
352
+ in `.etymd/config.json`. Then the `commit-msg` hook also checks the subject against
353
+ [Conventional Commits](https://www.conventionalcommits.org) — `<type>[(scope)][!]: <summary>`,
354
+ with `feat fix docs style refactor perf test build ci chore revert` as the types. It is a format
355
+ check and not a taste check: it asks whether a machine can classify the line, and stops there.
356
+ An over-long subject is reported as advice and never blocks, and the subjects git writes for you
357
+ — merge, revert, fixup, squash, amend — are exempt, since gating those would ask you to rewrite
358
+ text you did not write.
359
+
360
+ It is the one generated check that needs nothing installed, and therefore the one that would run
361
+ for everyone who clones your repo. That is exactly why it is off by default. Every other check
362
+ the pack writes is either derived from what your repo already does or inert without a checker you
363
+ installed yourself; a commit convention is neither — it is an opinion, and this tool does not
364
+ hold opinions on your behalf.
365
+
366
+ Turn it on where the convention is already yours. A convention nobody gates does not get
367
+ abandoned in a decision you could point at: it erodes one hurried commit at a time, and by the
368
+ time the log reads as a mixture, every commit in it is already published.
369
+
343
370
  Modeled on this repo's own workflow (Etymd guards its own instructions with Etymd — its CI runs
344
371
  the same gate against its own freshly built CLI):
345
372
 
@@ -1,9 +1,9 @@
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';
3
+ import { scanProject } from './chunk-WTD2EIMN.js';
4
+ import { VERSION } from './chunk-K34AVLDP.js';
5
5
  import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-C7LBUFNU.js';
6
- import { PACK_VERSION } from './chunk-6DUDIVIC.js';
6
+ import { PACK_VERSION } from './chunk-OIYNP6Y6.js';
7
7
  import './chunk-IV3FYVTS.js';
8
8
 
9
9
  // src/commands/approve.ts
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ export { run } from './chunk-SGW7OPHZ.js';
3
+ import './chunk-KQMQJJZK.js';
4
+ import './chunk-IPOAD67U.js';
5
+ import './chunk-OAFYLBVG.js';
6
+ import './chunk-YVGC3E6U.js';
7
+ import './chunk-5BVKFJWM.js';
8
+ import './chunk-WTD2EIMN.js';
9
+ import './chunk-K34AVLDP.js';
10
+ import './chunk-C7LBUFNU.js';
11
+ import './chunk-OIYNP6Y6.js';
12
+ import './chunk-IV3FYVTS.js';
@@ -1,9 +1,9 @@
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';
3
+ import { scanProject } from './chunk-WTD2EIMN.js';
4
+ import './chunk-K34AVLDP.js';
5
5
  import { ETYMD_DIR } from './chunk-C7LBUFNU.js';
6
- import './chunk-6DUDIVIC.js';
6
+ import './chunk-OIYNP6Y6.js';
7
7
  import './chunk-IV3FYVTS.js';
8
8
  import { promises } from 'node:fs';
9
9
  import path from 'node:path';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_CONFIG } from './chunk-D2RI3CCT.js';
2
+ import { DEFAULT_CONFIG } from './chunk-YVGC3E6U.js';
3
3
  import { readText, wordCount, approxTokens, pathExists } from './chunk-IV3FYVTS.js';
4
4
  import { promises } from 'node:fs';
5
5
  import path from 'node:path';
@@ -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.10.0"};
5
5
 
6
6
  // src/version.ts
7
7
  var VERSION = package_default.version;
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { measureContext } from './chunk-JSUYV26K.js';
2
+ import { measureContext } from './chunk-IPOAD67U.js';
3
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';
4
+ import { DEFAULT_CONFIG, readConfig, CONFIG_FILE } from './chunk-YVGC3E6U.js';
5
+ import { scanProject, expandFileGlobs } from './chunk-WTD2EIMN.js';
6
6
  import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-C7LBUFNU.js';
7
- import { PACK_VERSION } from './chunk-6DUDIVIC.js';
7
+ import { PACK_VERSION } from './chunk-OIYNP6Y6.js';
8
8
  import { pathExists, readJson, readText, isDirectory, matchesAnyGlob, git, normalizeRelPath, isCiEnvironment, isExecutable } from './chunk-IV3FYVTS.js';
9
9
  import path from 'node:path';
10
10
  import YAML from 'yaml';
@@ -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 = "7";
4
4
 
5
5
  export { PACK_VERSION };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_CONFIG } from './chunk-D2RI3CCT.js';
3
- import { PACK_VERSION } from './chunk-6DUDIVIC.js';
2
+ import { DEFAULT_CONFIG } from './chunk-YVGC3E6U.js';
3
+ import { PACK_VERSION } from './chunk-OIYNP6Y6.js';
4
4
  import { readText, pathExists } from './chunk-IV3FYVTS.js';
5
5
  import path from 'node:path';
6
6
  import { createHash } from 'node:crypto';
@@ -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
@@ -336,14 +375,15 @@ async function planWorkflow(root, facts, opts) {
336
375
  await add(".githooks/pre-commit", generatePreCommitHook(), "Process gate (pre-commit)", true);
337
376
  await add(
338
377
  ".githooks/commit-msg",
339
- generateCommitMsgHook(),
340
- "Content screen (commit message)",
378
+ generateCommitMsgHook(opts.gateConfig),
379
+ "Message gate (content screen, format)",
341
380
  true
342
381
  );
343
382
  const gateConfig = opts.gateConfig?.commands?.length ? opts.gateConfig : {
344
383
  commands: derivedCommands(facts, existingPrePush ?? void 0),
345
384
  failOn: opts.gateConfig?.failOn ?? DEFAULT_CONFIG.gates.failOn,
346
385
  publishGate: opts.gateConfig?.publishGate,
386
+ commitFormat: opts.gateConfig?.commitFormat,
347
387
  allowWriting: opts.gateConfig?.allowWriting ?? []
348
388
  };
349
389
  await add(
@@ -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-KQMQJJZK.js';
3
3
  import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-5BVKFJWM.js';
4
4
 
5
5
  // src/commands/audit.ts
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-AQM4GYGN.js';
3
- import { PACK_VERSION } from './chunk-6DUDIVIC.js';
2
+ import { VERSION } from './chunk-K34AVLDP.js';
3
+ import { PACK_VERSION } from './chunk-OIYNP6Y6.js';
4
4
  import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-IV3FYVTS.js';
5
5
  import path from 'node:path';
6
6
  import { promises } from 'node:fs';
@@ -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
  }
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-AQM4GYGN.js';
2
+ import { VERSION } from './chunk-K34AVLDP.js';
3
3
  import path from 'node:path';
4
4
  import { Command } from 'commander';
5
5
  import pc from 'picocolors';
@@ -31,7 +31,7 @@ program.command("audit").description("Verify every instruction claim against the
31
31
  "exit non-zero when findings at/above this tier exist (risk|gap|polish)"
32
32
  ).action(
33
33
  (opts, cmd) => action(async () => {
34
- const { run } = await import('./audit-Y35XVD6Z.js');
34
+ const { run } = await import('./audit-FGDX2AYS.js');
35
35
  await run({
36
36
  cwd: resolveCwd(cmd),
37
37
  json: opts.json,
@@ -44,7 +44,7 @@ program.command("audit").description("Verify every instruction claim against the
44
44
  );
45
45
  program.command("init").description("Onboard the truth guard: approve the baseline; scaffold AGENTS.md only if missing").option("-y, --yes", "accept defaults without prompting (never overwrites)").action(
46
46
  (opts, cmd) => action(async () => {
47
- const { run } = await import('./init-RC35RS5J.js');
47
+ const { run } = await import('./init-YYGRFIHQ.js');
48
48
  await run({ cwd: resolveCwd(cmd), yes: opts.yes });
49
49
  })
50
50
  );
@@ -52,31 +52,31 @@ program.command("approve").description(
52
52
  "Re-approve the committed baseline after intentional structural changes (non-interactive)"
53
53
  ).action(
54
54
  (_opts, cmd) => action(async () => {
55
- const { run } = await import('./approve-HED3NCTE.js');
55
+ const { run } = await import('./approve-6TMXJLOI.js');
56
56
  await run({ cwd: resolveCwd(cmd) });
57
57
  })
58
58
  );
59
59
  program.command("scan").description("Deterministically reckon the project into a facts index").option("--json", "print the raw facts as JSON").option("--no-save", "do not write the .etymd cache").action(
60
60
  (opts, cmd) => action(async () => {
61
- const { run } = await import('./scan-MBXSD5RO.js');
61
+ const { run } = await import('./scan-VO776GM7.js');
62
62
  await run({ cwd: resolveCwd(cmd), json: opts.json, save: opts.save });
63
63
  })
64
64
  );
65
65
  program.command("doctor").description('Alias for `audit --truth` \u2014 "are the recorded instructions still true?"').option("--json", "print findings as JSON").action(
66
66
  (opts, cmd) => action(async () => {
67
- const { run } = await import('./doctor-DXXQJ2L3.js');
67
+ const { run } = await import('./doctor-W3UZK7KS.js');
68
68
  await run({ cwd: resolveCwd(cmd), json: opts.json });
69
69
  })
70
70
  );
71
71
  program.command("context").description("Measure the always-loaded context footprint and flag extraction candidates").option("--json", "print the budget as JSON").action(
72
72
  (opts, cmd) => action(async () => {
73
- const { run } = await import('./context-5ANOP2IJ.js');
73
+ const { run } = await import('./context-K6DZNWAV.js');
74
74
  await run({ cwd: resolveCwd(cmd), json: opts.json });
75
75
  })
76
76
  );
77
77
  program.command("brief").description("Emit a grounded briefing for the in-repo agent to complete the semantic layer").option("--human", "write a human onboarding brief instead of the agent briefing").action(
78
78
  (opts, cmd) => action(async () => {
79
- const { run } = await import('./brief-2CXAUDNN.js');
79
+ const { run } = await import('./brief-X64HK325.js');
80
80
  await run({ cwd: resolveCwd(cmd), human: opts.human });
81
81
  })
82
82
  );
@@ -111,7 +111,7 @@ var fleet = program.command("fleet").description(
111
111
  "exit non-zero when findings at/above this tier exist (risk|gap|polish)"
112
112
  ).action(
113
113
  (opts, cmd) => action(async () => {
114
- const { sweep } = await import('./fleet-DPJSZDEY.js');
114
+ const { sweep } = await import('./fleet-4PPNUIDJ.js');
115
115
  await sweep({
116
116
  cwd: resolveCwd(cmd),
117
117
  manifest: opts.manifest,
@@ -126,7 +126,7 @@ var fleet = program.command("fleet").description(
126
126
  );
127
127
  fleet.command("check").description("Validate the manifest pair only \u2014 no lenses: dangling mappings, duplicates, leaks").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").option("--json", "print the findings as JSON (EXPERIMENTAL through 0.2.x)").action(
128
128
  (_opts, cmd) => action(async () => {
129
- const { check } = await import('./fleet-DPJSZDEY.js');
129
+ const { check } = await import('./fleet-4PPNUIDJ.js');
130
130
  const opts = cmd.optsWithGlobals();
131
131
  await check({ cwd: resolveCwd(cmd), manifest: opts.manifest, json: opts.json });
132
132
  })
@@ -136,7 +136,7 @@ fleet.command("add").argument("<dir>", "directory of the project to register").d
136
136
  "public-repo | public-bound | private \u2014 mandatory for personal entries"
137
137
  ).option("-y, --yes", "skip prompts; every mandatory value must be passed as a flag").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
138
138
  (dir, opts, cmd) => action(async () => {
139
- const { add } = await import('./fleet-DPJSZDEY.js');
139
+ const { add } = await import('./fleet-4PPNUIDJ.js');
140
140
  const shared = cmd.optsWithGlobals();
141
141
  await add({
142
142
  cwd: resolveCwd(cmd),
@@ -159,7 +159,7 @@ fleet.command("dismiss").argument("<name>", "the registered project name").argum
159
159
  "Dismiss a project's finding from any cwd \u2014 corp ledgers persist beside the manifest"
160
160
  ).requiredOption("--reason <text>", "why it is dismissed \u2014 recorded so the decision survives").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
161
161
  (name, id, _opts, cmd) => action(async () => {
162
- const { dismiss } = await import('./fleet-DPJSZDEY.js');
162
+ const { dismiss } = await import('./fleet-4PPNUIDJ.js');
163
163
  const opts = cmd.optsWithGlobals();
164
164
  await dismiss({
165
165
  cwd: resolveCwd(cmd),
@@ -174,7 +174,7 @@ fleet.command("accept").argument("<name>", "the registered project name").argume
174
174
  "Accept a project's finding as a known trade-off \u2014 corp ledgers persist beside the manifest"
175
175
  ).option("--reason <text>", "optional note on why the trade-off is accepted").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
176
176
  (name, id, _opts, cmd) => action(async () => {
177
- const { accept } = await import('./fleet-DPJSZDEY.js');
177
+ const { accept } = await import('./fleet-4PPNUIDJ.js');
178
178
  const opts = cmd.optsWithGlobals();
179
179
  await accept({ cwd: resolveCwd(cmd), manifest: opts.manifest, name, id, reason: opts.reason });
180
180
  })
@@ -183,7 +183,7 @@ program.command("screen").description(
183
183
  "Content screen: check for text that must never be published. Bring your own patterns \u2014 etymd ships none."
184
184
  ).option("--staged", "screen what a commit is about to add (default)").option("--tree", "screen every tracked file \u2014 what is about to leave the machine").option("--message <file>", "screen a commit message (the commit-msg hook's $1)").option("--dir <dir>", "screen an unpacked build artifact \u2014 what actually ships").option("--patterns <file>", "pattern file (default: ~/.config/etymd/screen-patterns)").option("--advisory", "report without failing").action(
185
185
  (opts, cmd) => action(async () => {
186
- const { run } = await import('./screen-EHNK6ANK.js');
186
+ const { run } = await import('./screen-MOQP6XC7.js');
187
187
  const scope = opts.message ? "message" : opts.dir ? "dir" : opts.tree ? "tree" : "staged";
188
188
  await run({
189
189
  cwd: resolveCwd(cmd),
@@ -196,7 +196,7 @@ program.command("screen").description(
196
196
  );
197
197
  program.command("gates").description("Install the local git-hook gates (process \u2192 pre-commit, correctness \u2192 pre-push)").option("--ci", "note about the CI review gate (ships later; local gates install now)").option("-y, --yes", "skip prompts; never overwrites a hand-edited hook").action(
198
198
  (opts, cmd) => action(async () => {
199
- const { run } = await import('./gates-RR3V4PII.js');
199
+ const { run } = await import('./gates-Q7FMVUQP.js');
200
200
  await run({ cwd: resolveCwd(cmd), ci: opts.ci, yes: opts.yes });
201
201
  })
202
202
  );
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- export { CONFIG_FILE, DEFAULT_CONFIG, configPath, readConfig } from './chunk-D2RI3CCT.js';
2
+ export { CONFIG_FILE, DEFAULT_CONFIG, configPath, readConfig } from './chunk-YVGC3E6U.js';
3
3
  import './chunk-C7LBUFNU.js';
4
4
  import './chunk-IV3FYVTS.js';
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { measureContext } from './chunk-JSUYV26K.js';
3
- import { readConfig } from './chunk-D2RI3CCT.js';
2
+ import { measureContext } from './chunk-IPOAD67U.js';
3
+ import { readConfig } from './chunk-YVGC3E6U.js';
4
4
  import { print, renderContext } from './chunk-5BVKFJWM.js';
5
5
  import './chunk-C7LBUFNU.js';
6
6
  import './chunk-IV3FYVTS.js';
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { run } from './chunk-ULM25GWG.js';
3
- import './chunk-KGVRZ6WC.js';
4
- import './chunk-JSUYV26K.js';
2
+ import { run } from './chunk-SGW7OPHZ.js';
3
+ import './chunk-KQMQJJZK.js';
4
+ import './chunk-IPOAD67U.js';
5
5
  import './chunk-OAFYLBVG.js';
6
- import './chunk-D2RI3CCT.js';
6
+ import './chunk-YVGC3E6U.js';
7
7
  import './chunk-5BVKFJWM.js';
8
- import './chunk-UN7IAYM6.js';
9
- import './chunk-AQM4GYGN.js';
8
+ import './chunk-WTD2EIMN.js';
9
+ import './chunk-K34AVLDP.js';
10
10
  import './chunk-C7LBUFNU.js';
11
- import './chunk-6DUDIVIC.js';
11
+ import './chunk-OIYNP6Y6.js';
12
12
  import './chunk-IV3FYVTS.js';
13
13
 
14
14
  // src/commands/doctor.ts
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { dismiss, accept } from './chunk-V2SOHPXF.js';
3
- import { parseFailOnTier, meetsFailOn, runAudit } from './chunk-KGVRZ6WC.js';
4
- import './chunk-JSUYV26K.js';
3
+ import { parseFailOnTier, meetsFailOn, runAudit } from './chunk-KQMQJJZK.js';
4
+ import './chunk-IPOAD67U.js';
5
5
  import { readLedger } from './chunk-OAFYLBVG.js';
6
- import { DEFAULT_CONFIG } from './chunk-D2RI3CCT.js';
6
+ import { DEFAULT_CONFIG } from './chunk-YVGC3E6U.js';
7
7
  import { print, theme, section, renderFleetRows, renderFleetNotes, renderFindings, TIER_BADGE, glyph } from './chunk-5BVKFJWM.js';
8
- import { scanProject } from './chunk-UN7IAYM6.js';
9
- import './chunk-AQM4GYGN.js';
8
+ import { scanProject } from './chunk-WTD2EIMN.js';
9
+ import './chunk-K34AVLDP.js';
10
10
  import { ETYMD_DIR } from './chunk-C7LBUFNU.js';
11
- import './chunk-6DUDIVIC.js';
11
+ import './chunk-OIYNP6Y6.js';
12
12
  import { readText, pathExists, git, isDirectory } from './chunk-IV3FYVTS.js';
13
13
  import { promises } from 'node:fs';
14
14
  import os from 'node:os';
@@ -687,9 +687,9 @@ async function checkCorpEmails(manifest, findings, disclosures) {
687
687
  }
688
688
  }
689
689
  async function checkGateDrift(manifest, findings, disclosures) {
690
- const { planWorkflow } = await import('./generate-IPY5QIR5.js');
691
- const { scanProject: scanProject2 } = await import('./scan-KX522VKV.js');
692
- const { readConfig } = await import('./config-7WCVZCM6.js');
690
+ const { planWorkflow } = await import('./generate-NLHGTRZB.js');
691
+ const { scanProject: scanProject2 } = await import('./scan-PJSZGUG7.js');
692
+ const { readConfig } = await import('./config-TPK5LYDM.js');
693
693
  for (const entry of manifest.entries) {
694
694
  const root = entry.resolvedRoot;
695
695
  if (!root || !await isDirectory(root)) continue;
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { applyFiles } from './chunk-EF6BPBG6.js';
3
- import { deriveFailOn, derivedCommands, planWorkflow, runPrefix, isSafeGateCommand } from './chunk-WENIJMVV.js';
4
- import { readConfig, CONFIG_FILE, configPath } from './chunk-D2RI3CCT.js';
3
+ import { deriveFailOn, derivedCommands, planWorkflow, runPrefix, isSafeGateCommand } from './chunk-R6CVJ5D7.js';
4
+ import { readConfig, CONFIG_FILE, configPath } from './chunk-YVGC3E6U.js';
5
5
  import { section, print, glyph, theme, renderPlan } from './chunk-5BVKFJWM.js';
6
- import { scanProject } from './chunk-UN7IAYM6.js';
7
- import './chunk-AQM4GYGN.js';
6
+ import { scanProject } from './chunk-WTD2EIMN.js';
7
+ import './chunk-K34AVLDP.js';
8
8
  import './chunk-C7LBUFNU.js';
9
- import './chunk-6DUDIVIC.js';
9
+ import './chunk-OIYNP6Y6.js';
10
10
  import { readText, git } from './chunk-IV3FYVTS.js';
11
11
  import { promises } from 'node:fs';
12
12
  import path from 'node:path';
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- export { deriveFailOn, derivedCommands, planWorkflow, riskReachability } from './chunk-WENIJMVV.js';
3
- import './chunk-D2RI3CCT.js';
2
+ export { deriveFailOn, derivedCommands, planWorkflow, riskReachability } from './chunk-R6CVJ5D7.js';
3
+ import './chunk-YVGC3E6U.js';
4
4
  import './chunk-C7LBUFNU.js';
5
- import './chunk-6DUDIVIC.js';
5
+ import './chunk-OIYNP6Y6.js';
6
6
  import './chunk-IV3FYVTS.js';
package/dist/index.d.ts CHANGED
@@ -226,6 +226,14 @@ interface GateConfig {
226
226
  failOn: string;
227
227
  /** Emit the publish-time screen. Unset = derive from whether the repo publishes. */
228
228
  publishGate?: boolean;
229
+ /**
230
+ * Check the commit subject against Conventional Commits. **Unset = off.** Every other check
231
+ * the pack generates is either derived from what the repo already does or inert without a
232
+ * checker the user installed themselves; this one is a convention, and a convention is an
233
+ * opinion. Etymd ships the door and never decides that a repo wants it: `true` here is the
234
+ * repo asking, and nothing else turns it on.
235
+ */
236
+ commitFormat?: boolean;
229
237
  /** Commands allowed into a gate despite looking like they write (an explicit override). */
230
238
  allowWriting: string[];
231
239
  }
@@ -804,7 +812,7 @@ declare const gateIntegrityLens: Lens;
804
812
  * change meaning. Stamped into facts, baselines, and generated artifacts so drift against the
805
813
  * pack is computable and `harvest` has something to diff.
806
814
  */
807
- declare const PACK_VERSION = "6";
815
+ declare const PACK_VERSION = "7";
808
816
 
809
817
  declare const VERSION: string;
810
818
  declare const NAME: string;
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ var __export = (target, all) => {
20
20
  var PACK_VERSION;
21
21
  var init_version = __esm({
22
22
  "src/pack/version.ts"() {
23
- PACK_VERSION = "6";
23
+ PACK_VERSION = "7";
24
24
  }
25
25
  });
26
26
 
@@ -30,7 +30,7 @@ var init_package = __esm({
30
30
  "package.json"() {
31
31
  package_default = {
32
32
  name: "etymd",
33
- version: "0.9.1",
33
+ version: "0.10.0",
34
34
  description: "Keep your agent instructions true \u2014 verify AGENTS.md, CLAUDE.md, rules & skills against the actual repo, with drift caught over time and a regression ledger.",
35
35
  keywords: [
36
36
  "cli",
@@ -961,6 +961,14 @@ async function readConfig(root) {
961
961
  }
962
962
  return gates.publishGate;
963
963
  })();
964
+ const commitFormat = (() => {
965
+ if (gates.commitFormat === void 0) return void 0;
966
+ if (typeof gates.commitFormat !== "boolean") {
967
+ problems.push(`${CONFIG_FILE}: \`gates.commitFormat\` must be true or false \u2014 ignored.`);
968
+ return void 0;
969
+ }
970
+ return gates.commitFormat;
971
+ })();
964
972
  return {
965
973
  present: true,
966
974
  problems,
@@ -982,6 +990,7 @@ async function readConfig(root) {
982
990
  commands: readGlobList(gates.commands, "gates.commands", problems) ?? DEFAULT_CONFIG.gates.commands,
983
991
  failOn: failOn ?? DEFAULT_CONFIG.gates.failOn,
984
992
  publishGate,
993
+ commitFormat,
985
994
  allowWriting: readGlobList(gates.allowWriting, "gates.allowWriting", problems) ?? DEFAULT_CONFIG.gates.allowWriting
986
995
  }
987
996
  }
@@ -1155,9 +1164,34 @@ fi
1155
1164
  exit 0
1156
1165
  `);
1157
1166
  }
1158
- function generateCommitMsgHook() {
1167
+ function commitFormatStep() {
1168
+ const types = COMMIT_TYPES.join("|");
1169
+ return `
1170
+ # Message format \u2014 <type>[(scope)][!]: <summary>. Needs nothing installed, so it always runs.
1171
+ subject=$(sed -e '/^#/d' -e '/^[[:space:]]*$/d' "$1" | head -1)
1172
+ case "$subject" in
1173
+ "Merge "*|"Revert "*|fixup!*|squash!*|amend!*) ;;
1174
+ *)
1175
+ if ! printf '%s' "$subject" | grep -qE '^(${types})(\\([a-z0-9._/-]+\\))?!?: .+'; then
1176
+ echo "\u2717 commit message: expected '<type>[(scope)][!]: <summary>'"
1177
+ echo " got: $subject"
1178
+ echo " types: ${COMMIT_TYPES.join(" ")}"
1179
+ echo " a '!' after the type or scope marks a breaking change"
1180
+ exit 1
1181
+ fi
1182
+ # Length is advice, not a block: the format is what tooling reads, the length is what a
1183
+ # person reads, and only one of the two can break anything.
1184
+ if [ "\${#subject}" -gt ${SUBJECT_ADVISORY_LENGTH} ]; then
1185
+ echo "\u203A note: subject is \${#subject} characters; ${SUBJECT_ADVISORY_LENGTH} or fewer reads better in git log"
1186
+ fi
1187
+ ;;
1188
+ esac`;
1189
+ }
1190
+ function generateCommitMsgHook(gates) {
1191
+ const format = gates?.commitFormat === true ? `${commitFormatStep()}
1192
+ ` : "";
1159
1193
  return stampGenerated(`#!/usr/bin/env sh
1160
- # etymd: content screen \u2014 the commit message itself.
1194
+ # etymd: the commit message itself \u2014 content screen, then format.
1161
1195
  #
1162
1196
  # The staged-content gate reads file bytes and never sees the message, yet a message is as
1163
1197
  # permanently published as any file. No-op where no checker is installed.
@@ -1167,7 +1201,7 @@ GATE="\${COMMIT_MSG_GATE:-$(command -v etymd || true)}"
1167
1201
  if [ -x "$GATE" ]; then
1168
1202
  "$GATE" screen --message "$1" || exit 1
1169
1203
  fi
1170
-
1204
+ ${format}
1171
1205
  ${localHookCall("commit-msg")}
1172
1206
 
1173
1207
  exit 0
@@ -1274,12 +1308,26 @@ fi
1274
1308
  exit 0
1275
1309
  `);
1276
1310
  }
1277
- var GENERATION_MARKER_RE, CONTENT_GATE_RESOLUTION;
1311
+ var GENERATION_MARKER_RE, CONTENT_GATE_RESOLUTION, COMMIT_TYPES, SUBJECT_ADVISORY_LENGTH;
1278
1312
  var init_templates = __esm({
1279
1313
  "src/pack/templates.ts"() {
1280
1314
  init_version();
1281
1315
  GENERATION_MARKER_RE = /^(?:# |<!-- )etymd:generated pack-v\S+ ([0-9a-f]{16})(?: -->)?$/;
1282
1316
  CONTENT_GATE_RESOLUTION = `GATE="\${CONTENT_GATE:-$(command -v etymd || true)}"`;
1317
+ COMMIT_TYPES = [
1318
+ "feat",
1319
+ "fix",
1320
+ "docs",
1321
+ "style",
1322
+ "refactor",
1323
+ "perf",
1324
+ "test",
1325
+ "build",
1326
+ "ci",
1327
+ "chore",
1328
+ "revert"
1329
+ ];
1330
+ SUBJECT_ADVISORY_LENGTH = 72;
1283
1331
  }
1284
1332
  });
1285
1333
 
@@ -1345,14 +1393,15 @@ async function planWorkflow(root, facts, opts) {
1345
1393
  await add(".githooks/pre-commit", generatePreCommitHook(), "Process gate (pre-commit)", true);
1346
1394
  await add(
1347
1395
  ".githooks/commit-msg",
1348
- generateCommitMsgHook(),
1349
- "Content screen (commit message)",
1396
+ generateCommitMsgHook(opts.gateConfig),
1397
+ "Message gate (content screen, format)",
1350
1398
  true
1351
1399
  );
1352
1400
  const gateConfig = opts.gateConfig?.commands?.length ? opts.gateConfig : {
1353
1401
  commands: derivedCommands(facts, existingPrePush ?? void 0),
1354
1402
  failOn: opts.gateConfig?.failOn ?? DEFAULT_CONFIG.gates.failOn,
1355
1403
  publishGate: opts.gateConfig?.publishGate,
1404
+ commitFormat: opts.gateConfig?.commitFormat,
1356
1405
  allowWriting: opts.gateConfig?.allowWriting ?? []
1357
1406
  };
1358
1407
  await add(
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { applyFiles } from './chunk-EF6BPBG6.js';
3
- import { planWorkflow } from './chunk-WENIJMVV.js';
4
- import './chunk-D2RI3CCT.js';
3
+ import { planWorkflow } from './chunk-R6CVJ5D7.js';
4
+ import './chunk-YVGC3E6U.js';
5
5
  import { theme, renderFacts, print, renderPlan, section, glyph } from './chunk-5BVKFJWM.js';
6
- import { scanProject } from './chunk-UN7IAYM6.js';
7
- import { VERSION } from './chunk-AQM4GYGN.js';
6
+ import { scanProject } from './chunk-WTD2EIMN.js';
7
+ import { VERSION } from './chunk-K34AVLDP.js';
8
8
  import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-C7LBUFNU.js';
9
- import { PACK_VERSION } from './chunk-6DUDIVIC.js';
9
+ import { PACK_VERSION } from './chunk-OIYNP6Y6.js';
10
10
  import { git, readText } from './chunk-IV3FYVTS.js';
11
11
  import { promises } from 'node:fs';
12
12
  import path from 'node:path';
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ export { scanProject } from './chunk-WTD2EIMN.js';
3
+ import './chunk-K34AVLDP.js';
4
+ import './chunk-OIYNP6Y6.js';
5
+ import './chunk-IV3FYVTS.js';
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { print, renderFacts, theme } from './chunk-5BVKFJWM.js';
3
- import { scanProject } from './chunk-UN7IAYM6.js';
4
- import './chunk-AQM4GYGN.js';
3
+ import { scanProject } from './chunk-WTD2EIMN.js';
4
+ import './chunk-K34AVLDP.js';
5
5
  import { writeCachedFacts } from './chunk-C7LBUFNU.js';
6
- import './chunk-6DUDIVIC.js';
6
+ import './chunk-OIYNP6Y6.js';
7
7
  import './chunk-IV3FYVTS.js';
8
8
 
9
9
  // src/commands/scan.ts
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { print, glyph, theme, section } from './chunk-5BVKFJWM.js';
3
- import { readText, git, pathExists } from './chunk-IV3FYVTS.js';
3
+ import { git, readText, pathExists } from './chunk-IV3FYVTS.js';
4
4
  import { promises } from 'node:fs';
5
5
  import path from 'node:path';
6
6
 
@@ -17,8 +17,49 @@ function compile(raw) {
17
17
  }
18
18
  });
19
19
  }
20
- function isSelfName(pattern, self) {
21
- return pattern.source.toLowerCase() === self.toLowerCase();
20
+ var META = ".[]*^$+?(){}|";
21
+ function patternLiteral(source) {
22
+ let out = "";
23
+ let i = source.startsWith("\\b") ? 2 : 0;
24
+ for (; i < source.length; i++) {
25
+ const c = source.charAt(i);
26
+ if (c === "\\") {
27
+ const next = source[i + 1];
28
+ if (next === void 0) return null;
29
+ if (/[A-Za-z0-9]/.test(next)) return next === "b" && i + 2 === source.length ? out : null;
30
+ out += next;
31
+ i++;
32
+ continue;
33
+ }
34
+ if (META.includes(c)) return null;
35
+ out += c;
36
+ }
37
+ return out;
38
+ }
39
+ function isSelfName(pattern, selves) {
40
+ const literal = patternLiteral(pattern.source);
41
+ if (literal === null || literal === "") return false;
42
+ return selves.some((s) => s.toLowerCase() === literal.toLowerCase());
43
+ }
44
+ function bareName(raw) {
45
+ return raw.replace(/^@[^/]+\//, "").replace(/\.git$/, "");
46
+ }
47
+ async function selfIdentities(cwd) {
48
+ const names = /* @__PURE__ */ new Set();
49
+ const top = await git(cwd, ["rev-parse", "--show-toplevel"]);
50
+ names.add(path.basename(top || path.resolve(cwd)));
51
+ const pkg = await readText(path.join(top || cwd, "package.json"));
52
+ if (pkg) {
53
+ try {
54
+ const name = JSON.parse(pkg).name;
55
+ if (typeof name === "string" && name) names.add(bareName(name));
56
+ } catch {
57
+ }
58
+ }
59
+ const remote = await git(cwd, ["remote", "get-url", "origin"]);
60
+ if (remote) names.add(bareName(path.basename(remote.trim())));
61
+ names.delete("");
62
+ return [...names];
22
63
  }
23
64
  function screenText(text, file, patterns, allow = []) {
24
65
  const hits = [];
@@ -88,10 +129,8 @@ async function run(opts) {
88
129
  }
89
130
  const allowRaw = await readText(path.join(opts.cwd, ALLOW_FILE)) ?? await readText(path.join(opts.cwd, LEGACY_ALLOW_FILE));
90
131
  const allow = allowRaw ? compile(allowRaw) : [];
91
- const self = path.basename(
92
- await git(opts.cwd, ["rev-parse", "--show-toplevel"]) || path.resolve(opts.cwd)
93
- );
94
- const active = self ? patterns.filter((re) => !isSelfName(re, self)) : patterns;
132
+ const selves = await selfIdentities(opts.cwd);
133
+ const active = selves.length ? patterns.filter((re) => !isSelfName(re, selves)) : patterns;
95
134
  const hits = [];
96
135
  let scanned = 0;
97
136
  let skippedBinary = 0;
@@ -155,4 +194,4 @@ async function run(opts) {
155
194
  if (!opts.advisory) process.exitCode = 1;
156
195
  }
157
196
 
158
- export { isSelfName, readScreenable, run, screenText };
197
+ export { isSelfName, patternLiteral, readScreenable, run, screenText, selfIdentities };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "etymd",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Keep your agent instructions true — verify AGENTS.md, CLAUDE.md, rules & skills against the actual repo, with drift caught over time and a regression ledger.",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,12 +0,0 @@
1
- #!/usr/bin/env node
2
- export { run } from './chunk-ULM25GWG.js';
3
- import './chunk-KGVRZ6WC.js';
4
- import './chunk-JSUYV26K.js';
5
- import './chunk-OAFYLBVG.js';
6
- import './chunk-D2RI3CCT.js';
7
- import './chunk-5BVKFJWM.js';
8
- import './chunk-UN7IAYM6.js';
9
- import './chunk-AQM4GYGN.js';
10
- import './chunk-C7LBUFNU.js';
11
- import './chunk-6DUDIVIC.js';
12
- import './chunk-IV3FYVTS.js';
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- export { scanProject } from './chunk-UN7IAYM6.js';
3
- import './chunk-AQM4GYGN.js';
4
- import './chunk-6DUDIVIC.js';
5
- import './chunk-IV3FYVTS.js';