etymd 0.10.0 → 0.12.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 +92 -0
  2. package/README.md +23 -2
  3. package/dist/{approve-6TMXJLOI.js → approve-PP2QJQ3Y.js} +5 -5
  4. package/dist/audit-BXHJUTMJ.js +12 -0
  5. package/dist/{brief-X64HK325.js → brief-GDLJEG6X.js} +7 -7
  6. package/dist/{chunk-V2SOHPXF.js → chunk-2VLNI3L2.js} +1 -1
  7. package/dist/{chunk-OAFYLBVG.js → chunk-3E2IPCRY.js} +3 -3
  8. package/dist/{chunk-IV3FYVTS.js → chunk-4VPBP6K6.js} +4 -3
  9. package/dist/{chunk-YVGC3E6U.js → chunk-DWL2IKZH.js} +3 -3
  10. package/dist/{chunk-IPOAD67U.js → chunk-F75Q43BC.js} +4 -4
  11. package/dist/{chunk-EF6BPBG6.js → chunk-GWGKEPRX.js} +2 -2
  12. package/dist/{chunk-R6CVJ5D7.js → chunk-HRJJQCMT.js} +9 -13
  13. package/dist/{chunk-C7LBUFNU.js → chunk-JHZ2BN4U.js} +3 -3
  14. package/dist/{chunk-OIYNP6Y6.js → chunk-LSZGCKIQ.js} +1 -1
  15. package/dist/{chunk-WTD2EIMN.js → chunk-MTVEJ33D.js} +5 -5
  16. package/dist/{chunk-KQMQJJZK.js → chunk-O3PHW4OP.js} +9 -9
  17. package/dist/{chunk-SGW7OPHZ.js → chunk-SWA4NQHB.js} +1 -1
  18. package/dist/{chunk-K34AVLDP.js → chunk-YFNRDXUY.js} +1 -1
  19. package/dist/cli.js +25 -21
  20. package/dist/config-XAH6PA5G.js +4 -0
  21. package/dist/{context-K6DZNWAV.js → context-F63RSIBH.js} +4 -4
  22. package/dist/doctor-7SLSUSVK.js +19 -0
  23. package/dist/{fleet-4PPNUIDJ.js → fleet-5NVOUIZD.js} +18 -18
  24. package/dist/{gates-Q7FMVUQP.js → gates-OS5V3P4C.js} +10 -10
  25. package/dist/generate-PMCP37DC.js +6 -0
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +137 -141
  28. package/dist/{init-YYGRFIHQ.js → init-EKQ33WYS.js} +10 -10
  29. package/dist/ledger-4FOZ5HAB.js +5 -0
  30. package/dist/scan-N4BKVL5F.js +5 -0
  31. package/dist/{scan-VO776GM7.js → scan-XS4JPS7J.js} +5 -5
  32. package/dist/{screen-MOQP6XC7.js → screen-UF7VZ26C.js} +154 -20
  33. package/package.json +4 -4
  34. package/dist/audit-FGDX2AYS.js +0 -12
  35. package/dist/config-TPK5LYDM.js +0 -4
  36. package/dist/doctor-W3UZK7KS.js +0 -19
  37. package/dist/generate-NLHGTRZB.js +0 -6
  38. package/dist/ledger-B2XGANTH.js +0 -5
  39. package/dist/scan-PJSZGUG7.js +0 -5
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { applyFiles } from './chunk-EF6BPBG6.js';
3
- import { planWorkflow } from './chunk-R6CVJ5D7.js';
4
- import './chunk-YVGC3E6U.js';
2
+ import { applyFiles } from './chunk-GWGKEPRX.js';
3
+ import { planWorkflow } from './chunk-HRJJQCMT.js';
4
+ import './chunk-DWL2IKZH.js';
5
5
  import { theme, renderFacts, print, renderPlan, section, glyph } from './chunk-5BVKFJWM.js';
6
- import { scanProject } from './chunk-WTD2EIMN.js';
7
- import { VERSION } from './chunk-K34AVLDP.js';
8
- import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-C7LBUFNU.js';
9
- import { PACK_VERSION } from './chunk-OIYNP6Y6.js';
10
- import { git, readText } from './chunk-IV3FYVTS.js';
11
- import { promises } from 'node:fs';
12
- import path from 'node:path';
6
+ import { scanProject } from './chunk-MTVEJ33D.js';
7
+ import { VERSION } from './chunk-YFNRDXUY.js';
8
+ import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-JHZ2BN4U.js';
9
+ import { PACK_VERSION } from './chunk-LSZGCKIQ.js';
10
+ import { git, readText } from './chunk-4VPBP6K6.js';
11
+ import { promises } from 'fs';
12
+ import path from 'path';
13
13
  import { intro, spinner, select, confirm, outro, isCancel, cancel } from '@clack/prompts';
14
14
 
15
15
  function bail() {
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ export { accept, dismiss, list } from './chunk-2VLNI3L2.js';
3
+ import './chunk-3E2IPCRY.js';
4
+ import './chunk-5BVKFJWM.js';
5
+ import './chunk-4VPBP6K6.js';
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ export { scanProject } from './chunk-MTVEJ33D.js';
3
+ import './chunk-YFNRDXUY.js';
4
+ import './chunk-LSZGCKIQ.js';
5
+ import './chunk-4VPBP6K6.js';
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { print, renderFacts, theme } from './chunk-5BVKFJWM.js';
3
- import { scanProject } from './chunk-WTD2EIMN.js';
4
- import './chunk-K34AVLDP.js';
5
- import { writeCachedFacts } from './chunk-C7LBUFNU.js';
6
- import './chunk-OIYNP6Y6.js';
7
- import './chunk-IV3FYVTS.js';
3
+ import { scanProject } from './chunk-MTVEJ33D.js';
4
+ import './chunk-YFNRDXUY.js';
5
+ import { writeCachedFacts } from './chunk-JHZ2BN4U.js';
6
+ import './chunk-LSZGCKIQ.js';
7
+ import './chunk-4VPBP6K6.js';
8
8
 
9
9
  // src/commands/scan.ts
10
10
  async function run(opts) {
@@ -1,21 +1,76 @@
1
1
  #!/usr/bin/env node
2
2
  import { print, glyph, theme, section } from './chunk-5BVKFJWM.js';
3
- import { git, readText, pathExists } from './chunk-IV3FYVTS.js';
4
- import { promises } from 'node:fs';
5
- import path from 'node:path';
3
+ import { git, readText, pathExists } from './chunk-4VPBP6K6.js';
4
+ import { promises } from 'fs';
5
+ import path from 'path';
6
6
 
7
+ var CLASS_DIRECTIVE = /^#[ \t]*class:[ \t]*(secret|vocabulary)\b/i;
7
8
  var MACHINE_PATH_RE = /\/(?:Users|home)\/[A-Za-z0-9._-]+\//;
8
9
  var ALLOW_MARKER = "allow-published-string";
9
10
  var ALLOW_FILE = ".etymd-screen-allow";
10
11
  var LEGACY_ALLOW_FILE = ".artifact-check-allow";
11
- function compile(raw) {
12
- return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).map((l) => {
13
- try {
14
- return new RegExp(l, "i");
15
- } catch {
16
- return new RegExp(l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
12
+ var LABELED_LINE = /^(pattern|reason|date|author)[ \t]+(.*)$/;
13
+ function compileAllow(raw) {
14
+ const entries = [];
15
+ let current = null;
16
+ const close = () => {
17
+ if (current) entries.push(current);
18
+ current = null;
19
+ };
20
+ for (const rawLine of raw.split("\n")) {
21
+ const l = rawLine.trim();
22
+ if (!l || l.startsWith("#")) continue;
23
+ const labeled = LABELED_LINE.exec(l);
24
+ if (labeled) {
25
+ const [, label = "", value = ""] = labeled;
26
+ if (label === "pattern") {
27
+ close();
28
+ current = makeAllowEntry(value);
29
+ } else if (current) {
30
+ if (label === "reason") current.reason = value.trim();
31
+ else if (label === "date") current.date = value.trim();
32
+ else current.author = value.trim();
33
+ }
34
+ } else {
35
+ close();
36
+ current = makeAllowEntry(l);
17
37
  }
18
- });
38
+ }
39
+ close();
40
+ return entries;
41
+ }
42
+ function makeAllowEntry(patternStr) {
43
+ try {
44
+ return { pattern: new RegExp(patternStr, "i"), isSelfName: false };
45
+ } catch {
46
+ return {
47
+ pattern: new RegExp(patternStr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i"),
48
+ isSelfName: false
49
+ };
50
+ }
51
+ }
52
+ function compileOnePattern(l) {
53
+ try {
54
+ return new RegExp(l, "i");
55
+ } catch {
56
+ return new RegExp(l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
57
+ }
58
+ }
59
+ function compilePatterns(raw) {
60
+ const out = [];
61
+ let cls = "secret";
62
+ for (const rawLine of raw.split("\n")) {
63
+ const l = rawLine.trim();
64
+ if (!l) continue;
65
+ const dir = CLASS_DIRECTIVE.exec(l);
66
+ if (dir) {
67
+ cls = dir[1].toLowerCase();
68
+ continue;
69
+ }
70
+ if (l.startsWith("#")) continue;
71
+ out.push({ re: compileOnePattern(l), cls });
72
+ }
73
+ return out;
19
74
  }
20
75
  var META = ".[]*^$+?(){}|";
21
76
  function patternLiteral(source) {
@@ -61,13 +116,17 @@ async function selfIdentities(cwd) {
61
116
  names.delete("");
62
117
  return [...names];
63
118
  }
64
- function screenText(text, file, patterns, allow = []) {
119
+ function screenText(text, file, patterns, allow = [], skipVocabulary = false) {
120
+ const classed = patterns.map(
121
+ (p) => p instanceof RegExp ? { re: p, cls: "secret" } : p
122
+ );
123
+ const active = skipVocabulary ? classed.filter((p) => p.cls === "secret") : classed;
65
124
  const hits = [];
66
125
  const lines = text.split("\n");
67
126
  for (const [i, line] of lines.entries()) {
68
127
  if (line.includes(ALLOW_MARKER)) continue;
69
- if (allow.some((re) => re.test(line))) continue;
70
- for (const re of patterns) {
128
+ if (allow.some((entry) => entry.pattern.test(line))) continue;
129
+ for (const { re } of active) {
71
130
  if (re.test(line)) {
72
131
  hits.push({ file, line: i + 1, text: line.trim().slice(0, 160), reason: String(re) });
73
132
  break;
@@ -84,6 +143,30 @@ function screenText(text, file, patterns, allow = []) {
84
143
  }
85
144
  return hits;
86
145
  }
146
+ async function collectUpstreamBlobs(cwd, remote) {
147
+ const refsRaw = await git(cwd, ["for-each-ref", "--format=%(refname)", `refs/remotes/${remote}/`]);
148
+ const refs = (refsRaw ?? "").split("\n").filter(Boolean);
149
+ if (!refs.length) return null;
150
+ const blobs = /* @__PURE__ */ new Set();
151
+ for (const ref of refs) {
152
+ const tree = await git(cwd, ["ls-tree", "-r", ref]);
153
+ if (tree === null) continue;
154
+ for (const line of tree.split("\n")) {
155
+ const m = /^\d+ blob ([0-9a-f]+)\t/.exec(line);
156
+ if (m) blobs.add(m[1]);
157
+ }
158
+ }
159
+ return blobs.size ? blobs : null;
160
+ }
161
+ async function stagedBlobShas(cwd) {
162
+ const raw = await git(cwd, ["ls-files", "-s"]);
163
+ const map = /* @__PURE__ */ new Map();
164
+ for (const line of (raw ?? "").split("\n")) {
165
+ const m = /^\d+ ([0-9a-f]+) \d+\t(.+)$/.exec(line);
166
+ if (m) map.set(m[2], m[1]);
167
+ }
168
+ return map;
169
+ }
87
170
  async function walk(dir, out, depth = 0) {
88
171
  if (depth > 12) return;
89
172
  let entries;
@@ -122,15 +205,60 @@ async function run(opts) {
122
205
  );
123
206
  return;
124
207
  }
125
- const patterns = compile(rawPatterns);
208
+ const patterns = compilePatterns(rawPatterns);
126
209
  if (!patterns.length) {
127
210
  print(` ${glyph.partial} ${theme.dim(`${patternPath} has no active patterns \u2014 skipping.`)}`);
128
211
  return;
129
212
  }
130
- const allowRaw = await readText(path.join(opts.cwd, ALLOW_FILE)) ?? await readText(path.join(opts.cwd, LEGACY_ALLOW_FILE));
131
- const allow = allowRaw ? compile(allowRaw) : [];
213
+ const usingLegacy = !await pathExists(path.join(opts.cwd, ALLOW_FILE)) && await pathExists(path.join(opts.cwd, LEGACY_ALLOW_FILE));
214
+ const allowRaw = usingLegacy ? await readText(path.join(opts.cwd, LEGACY_ALLOW_FILE)) : await readText(path.join(opts.cwd, ALLOW_FILE));
215
+ const allowEntries = allowRaw ? compileAllow(allowRaw) : [];
132
216
  const selves = await selfIdentities(opts.cwd);
133
- const active = selves.length ? patterns.filter((re) => !isSelfName(re, selves)) : patterns;
217
+ const validatedEntries = allowEntries.filter((entry) => {
218
+ if (entry.isSelfName) return true;
219
+ if (isSelfName(entry.pattern, selves)) {
220
+ entry.isSelfName = true;
221
+ return true;
222
+ }
223
+ if (!entry.reason || !entry.date || !entry.author) {
224
+ print(
225
+ ` ${glyph.partial} ${theme.warn(`Missing provenance for exemption pattern`)} ${theme.dim(String(entry.pattern))}`
226
+ );
227
+ print(
228
+ ` ${theme.dim("Format: a `pattern` line, then `reason`, `date`, `author` lines \u2014 a bare pattern line is a self-name exemption")}`
229
+ );
230
+ return false;
231
+ }
232
+ return true;
233
+ });
234
+ const allow = validatedEntries;
235
+ const active = selves.length ? patterns.filter((p) => !isSelfName(p.re, selves)) : patterns;
236
+ const hasVocab = active.some((p) => p.cls === "vocabulary");
237
+ const upstreamRemote = hasVocab && (opts.scope === "staged" || opts.scope === "tree") ? opts.upstream ?? await git(opts.cwd, ["config", "--get", "etymd.upstream"]) ?? void 0 : void 0;
238
+ let upstreamBlobs = null;
239
+ let blobByPath = /* @__PURE__ */ new Map();
240
+ let upstreamNote = "";
241
+ if (upstreamRemote) {
242
+ upstreamBlobs = await collectUpstreamBlobs(opts.cwd, upstreamRemote);
243
+ if (upstreamBlobs === null) {
244
+ upstreamNote = `upstream ref '${upstreamRemote}' unavailable \u2014 vocabulary exemption off, screening every pattern`;
245
+ } else {
246
+ blobByPath = await stagedBlobShas(opts.cwd);
247
+ }
248
+ }
249
+ const isUpstreamOwned = (rel) => {
250
+ if (!upstreamBlobs) return false;
251
+ const sha = blobByPath.get(rel);
252
+ return !!sha && upstreamBlobs.has(sha);
253
+ };
254
+ if (usingLegacy) {
255
+ print(
256
+ ` ${glyph.partial} ${theme.warn(`Using legacy ${LEGACY_ALLOW_FILE} file \u2014 rename to ${ALLOW_FILE} and add provenance`)}`
257
+ );
258
+ print(
259
+ ` ${theme.dim("New format: a `pattern` line, then `reason`, `date`, `author` lines per entry")}`
260
+ );
261
+ }
134
262
  const hits = [];
135
263
  let scanned = 0;
136
264
  let skippedBinary = 0;
@@ -171,15 +299,21 @@ async function run(opts) {
171
299
  continue;
172
300
  }
173
301
  scanned++;
174
- hits.push(...screenText(raw, rel, active, allow));
302
+ hits.push(...screenText(raw, rel, active, allow, isUpstreamOwned(rel)));
175
303
  }
176
304
  }
177
- if (!hits.length) return;
305
+ if (upstreamNote) {
306
+ print(` ${glyph.partial} ${theme.warn(upstreamNote)}`);
307
+ }
178
308
  section(
179
309
  `Content screen ${theme.dim(
180
310
  `\xB7 ${opts.scope} \xB7 ${scanned} file(s)${skippedBinary ? ` \xB7 ${skippedBinary} binary skipped` : ""}`
181
311
  )}`
182
312
  );
313
+ if (!hits.length) {
314
+ print(` ${glyph.ok} ${theme.dim("clean")}`);
315
+ return;
316
+ }
183
317
  for (const h of hits) {
184
318
  print(` ${theme.warn(h.file)}${theme.dim(`:${h.line}`)} ${theme.dim(`(${h.reason})`)}`);
185
319
  print(` ${h.text}`);
@@ -194,4 +328,4 @@ async function run(opts) {
194
328
  if (!opts.advisory) process.exitCode = 1;
195
329
  }
196
330
 
197
- export { isSelfName, patternLiteral, readScreenable, run, screenText, selfIdentities };
331
+ export { collectUpstreamBlobs, compileAllow, compilePatterns, isSelfName, patternLiteral, readScreenable, run, screenText, selfIdentities, stagedBlobShas };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "etymd",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",
@@ -69,12 +69,12 @@
69
69
  "yaml": "2.9.0"
70
70
  },
71
71
  "devDependencies": {
72
- "@changesets/cli": "2.27.11",
72
+ "@changesets/cli": "2.31.1",
73
73
  "@types/node": "22.10.2",
74
74
  "prettier": "3.4.2",
75
- "tsup": "8.3.5",
75
+ "tsup": "8.5.1",
76
76
  "typescript": "5.7.2",
77
- "vitest": "2.1.8"
77
+ "vitest": "4.1.10"
78
78
  },
79
79
  "publishConfig": {
80
80
  "access": "public"
@@ -1,12 +0,0 @@
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,4 +0,0 @@
1
- #!/usr/bin/env node
2
- export { CONFIG_FILE, DEFAULT_CONFIG, configPath, readConfig } from './chunk-YVGC3E6U.js';
3
- import './chunk-C7LBUFNU.js';
4
- import './chunk-IV3FYVTS.js';
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env node
2
- import { 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';
13
-
14
- // src/commands/doctor.ts
15
- async function run2(opts) {
16
- await run({ cwd: opts.cwd, json: opts.json, truth: true });
17
- }
18
-
19
- export { run2 as run };
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
- export { deriveFailOn, derivedCommands, planWorkflow, riskReachability } from './chunk-R6CVJ5D7.js';
3
- import './chunk-YVGC3E6U.js';
4
- import './chunk-C7LBUFNU.js';
5
- import './chunk-OIYNP6Y6.js';
6
- import './chunk-IV3FYVTS.js';
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- export { accept, dismiss, list } from './chunk-V2SOHPXF.js';
3
- import './chunk-OAFYLBVG.js';
4
- import './chunk-5BVKFJWM.js';
5
- import './chunk-IV3FYVTS.js';
@@ -1,5 +0,0 @@
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';