etymd 0.2.2 → 0.3.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 (38) hide show
  1. package/CHANGELOG.md +42 -2
  2. package/README.md +145 -37
  3. package/dist/{approve-VC4V3OPW.js → approve-JEOEC2MV.js} +6 -4
  4. package/dist/audit-GYZ5YZ7U.js +12 -0
  5. package/dist/{brief-TZ45MNHD.js → brief-ZK7TXAC4.js} +6 -4
  6. package/dist/{chunk-MWIK6ML3.js → chunk-3NYDVB4Z.js} +123 -25
  7. package/dist/chunk-CRJHIXJH.js +5 -0
  8. package/dist/chunk-EF6BPBG6.js +22 -0
  9. package/dist/chunk-LT67FU2Y.js +59 -0
  10. package/dist/{chunk-BH37LELV.js → chunk-MKYPLNLH.js} +8 -5
  11. package/dist/{chunk-EUIHYD6F.js → chunk-MMPV67FW.js} +2 -2
  12. package/dist/{chunk-LVLZLCND.js → chunk-NFENQ3IK.js} +1 -1
  13. package/dist/{chunk-ANCCGYSH.js → chunk-NMZ3RHWW.js} +31 -59
  14. package/dist/{chunk-FZSIOCOZ.js → chunk-R74SJ7HS.js} +1 -1
  15. package/dist/{chunk-LBRNZZZF.js → chunk-TADKOW6P.js} +1 -99
  16. package/dist/{chunk-KMAJIQHC.js → chunk-V3DPLOOM.js} +7 -5
  17. package/dist/{chunk-KBF3SW3N.js → chunk-WWBF4Y7K.js} +1 -1
  18. package/dist/{chunk-IW6K5HLV.js → chunk-XTAX4YNT.js} +2 -2
  19. package/dist/chunk-YLNQQZ5F.js +102 -0
  20. package/dist/cli.js +50 -16
  21. package/dist/config-MX7OYI7F.js +4 -0
  22. package/dist/{context-GYCLR3TG.js → context-4DGXO7JR.js} +5 -3
  23. package/dist/doctor-C2RTL2QV.js +19 -0
  24. package/dist/{fleet-N3G5ZIGR.js → fleet-YZGR4UC2.js} +243 -10
  25. package/dist/gates-ILE4PXDA.js +272 -0
  26. package/dist/generate-IYCQETQB.js +4 -0
  27. package/dist/index.d.ts +113 -33
  28. package/dist/index.js +713 -257
  29. package/dist/{init-4CQOLFQG.js → init-OVLO4OBZ.js} +8 -5
  30. package/dist/ledger-TMIV45AA.js +5 -0
  31. package/dist/{scan-CYOOGJFG.js → scan-RAJUFTQR.js} +6 -4
  32. package/dist/scan-ZYZ7XDSJ.js +5 -0
  33. package/dist/screen-U7JMWZPS.js +134 -0
  34. package/package.json +2 -2
  35. package/dist/audit-LZJRJS5V.js +0 -9
  36. package/dist/doctor-6JDAHGDT.js +0 -16
  37. package/dist/gates-G5BCMRRV.js +0 -71
  38. package/dist/ledger-RHQJH5EA.js +0 -4
package/dist/index.js CHANGED
@@ -1,24 +1,127 @@
1
- import path from 'node:path';
2
- import { promises } from 'node:fs';
3
1
  import { execFile } from 'node:child_process';
2
+ import { promises } from 'node:fs';
4
3
  import { promisify } from 'node:util';
4
+ import path from 'node:path';
5
5
  import YAML from 'yaml';
6
6
  import os from 'node:os';
7
7
 
8
- // src/core/scan.ts
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __esm = (fn, res) => function __init() {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ };
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
9
17
 
10
18
  // src/pack/version.ts
11
- var PACK_VERSION = "2";
19
+ var PACK_VERSION;
20
+ var init_version = __esm({
21
+ "src/pack/version.ts"() {
22
+ PACK_VERSION = "3";
23
+ }
24
+ });
12
25
 
13
26
  // package.json
14
- var package_default = {
15
- name: "etymd",
16
- version: "0.2.2"};
27
+ var package_default;
28
+ var init_package = __esm({
29
+ "package.json"() {
30
+ package_default = {
31
+ name: "etymd",
32
+ version: "0.3.0",
33
+ 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.",
34
+ keywords: [
35
+ "cli",
36
+ "ai",
37
+ "agents",
38
+ "agents-md",
39
+ "claude-md",
40
+ "instructions",
41
+ "lint",
42
+ "drift",
43
+ "context-engineering",
44
+ "truth",
45
+ "developer-tools"
46
+ ],
47
+ license: "MIT",
48
+ author: "Alkis Yuv",
49
+ repository: {
50
+ type: "git",
51
+ url: "git+https://github.com/triartleet/etymd.git"
52
+ },
53
+ homepage: "https://github.com/triartleet/etymd#readme",
54
+ bugs: {
55
+ url: "https://github.com/triartleet/etymd/issues"
56
+ },
57
+ type: "module",
58
+ bin: {
59
+ etymd: "./dist/cli.js"
60
+ },
61
+ main: "./dist/index.js",
62
+ module: "./dist/index.js",
63
+ types: "./dist/index.d.ts",
64
+ exports: {
65
+ ".": {
66
+ types: "./dist/index.d.ts",
67
+ import: "./dist/index.js"
68
+ }
69
+ },
70
+ files: [
71
+ "dist",
72
+ "README.md",
73
+ "LICENSE",
74
+ "CHANGELOG.md"
75
+ ],
76
+ engines: {
77
+ node: ">=18.17"
78
+ },
79
+ scripts: {
80
+ build: "tsup",
81
+ dev: "tsup --watch",
82
+ start: "node dist/cli.js",
83
+ typecheck: "tsc --noEmit",
84
+ test: "vitest run",
85
+ "test:watch": "vitest",
86
+ format: "prettier --write . && npm run guard:md",
87
+ "format:check": "prettier --check . && npm run guard:md",
88
+ "guard:md": "! grep -rnF --include=*.md --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.git '\\*\\*' .",
89
+ prepare: "npm run build",
90
+ prepublishOnly: "npm run build && artifact-check",
91
+ changeset: "changeset",
92
+ "version:packages": "changeset version",
93
+ release: "npm run build && changeset publish"
94
+ },
95
+ dependencies: {
96
+ "@clack/prompts": "0.7.0",
97
+ commander: "12.1.0",
98
+ picocolors: "1.1.1",
99
+ yaml: "2.7.0"
100
+ },
101
+ devDependencies: {
102
+ "@changesets/cli": "2.27.11",
103
+ "@types/node": "22.10.2",
104
+ prettier: "3.4.2",
105
+ tsup: "8.3.5",
106
+ typescript: "5.7.2",
107
+ vitest: "2.1.8"
108
+ },
109
+ publishConfig: {
110
+ access: "public"
111
+ }
112
+ };
113
+ }
114
+ });
17
115
 
18
116
  // src/version.ts
19
- var VERSION = package_default.version;
20
- var NAME = package_default.name;
21
- var pExecFile = promisify(execFile);
117
+ var VERSION, NAME;
118
+ var init_version2 = __esm({
119
+ "src/version.ts"() {
120
+ init_package();
121
+ VERSION = package_default.version;
122
+ NAME = package_default.name;
123
+ }
124
+ });
22
125
  async function pathExists(p) {
23
126
  try {
24
127
  await promises.access(p);
@@ -64,15 +167,6 @@ function wordCount(text) {
64
167
  function approxTokens(words) {
65
168
  return Math.round(words / 0.75);
66
169
  }
67
- var CI_ENV_VARS = [
68
- "CI",
69
- "GITHUB_ACTIONS",
70
- "GITLAB_CI",
71
- "BUILDKITE",
72
- "CIRCLECI",
73
- "JENKINS_URL",
74
- "TF_BUILD"
75
- ];
76
170
  function isCiEnvironment() {
77
171
  return CI_ENV_VARS.some((v) => Boolean(process.env[v]));
78
172
  }
@@ -113,31 +207,21 @@ function matchesGlob(p, glob) {
113
207
  function matchesAnyGlob(p, globs) {
114
208
  return globs.some((g) => matchesGlob(p, g));
115
209
  }
116
-
117
- // src/core/detect.ts
118
- var IGNORED_DIRS = /* @__PURE__ */ new Set([
119
- "node_modules",
120
- ".git",
121
- "dist",
122
- "build",
123
- "coverage",
124
- ".next",
125
- ".turbo",
126
- ".cache",
127
- ".pnpm",
128
- "out"
129
- ]);
130
- var MEANINGFUL_DOT_DIRS = /* @__PURE__ */ new Set([
131
- ".github",
132
- ".gitlab",
133
- ".claude",
134
- ".cursor",
135
- ".githooks",
136
- ".husky",
137
- ".changeset",
138
- ".vscode",
139
- ".storybook"
140
- ]);
210
+ var pExecFile, CI_ENV_VARS;
211
+ var init_util = __esm({
212
+ "src/core/util.ts"() {
213
+ pExecFile = promisify(execFile);
214
+ CI_ENV_VARS = [
215
+ "CI",
216
+ "GITHUB_ACTIONS",
217
+ "GITLAB_CI",
218
+ "BUILDKITE",
219
+ "CIRCLECI",
220
+ "JENKINS_URL",
221
+ "TF_BUILD"
222
+ ];
223
+ }
224
+ });
141
225
  async function detectPackageManager(root, pkg) {
142
226
  if (pkg?.packageManager) {
143
227
  const name = pkg.packageManager.split("@")[0];
@@ -245,66 +329,6 @@ async function expandFileGlobs(root, globs, cap = 500) {
245
329
  }
246
330
  return [...out].sort();
247
331
  }
248
- var isMeta = (value) => /npm-run-all|run-s\b|run-p\b|concurrently|&&/.test(value);
249
- var ROLE_LADDERS = [
250
- {
251
- role: "formatCheck",
252
- ladder: [
253
- (k) => /^format:check$/.test(k),
254
- (k) => /^test:format(:local)?$/.test(k),
255
- (k) => /^(prettier:check|check:format|format:ci)$/.test(k),
256
- (_k, v) => /prettier(\s|.*\s)(-l\b|--check)/.test(v)
257
- ]
258
- },
259
- {
260
- role: "typecheck",
261
- exclude: /generate|codegen|build|emit|watch/,
262
- ladder: [
263
- (k) => /^(typecheck|type-check)$/.test(k),
264
- (k) => /^test:types(:local)?$/.test(k),
265
- (k) => /^(types|check:types|types:check)$/.test(k),
266
- (_k, v) => /tsc\b.*--noemit/i.test(v)
267
- ]
268
- },
269
- {
270
- role: "test",
271
- exclude: /watch|coverage|snapshot|e2e|integration|debug|notsilent|mdx/,
272
- ladder: [
273
- (k) => /^test:unit(:local|:ci)?$/.test(k),
274
- (k) => /^test:jest$/.test(k),
275
- (k, v) => /^test$/.test(k) && !isMeta(v),
276
- (k) => /^test$/.test(k)
277
- ]
278
- },
279
- {
280
- role: "lint",
281
- exclude: /fix|format|staged|watch|style/,
282
- ladder: [
283
- (k) => /^lint$/.test(k),
284
- (k) => /^test:lint(:local)?$/.test(k),
285
- (k) => /^lint:/.test(k),
286
- (_k, v) => /eslint\b/.test(v) && !/--fix/.test(v)
287
- ]
288
- },
289
- {
290
- role: "format",
291
- exclude: /check|:ci$/,
292
- ladder: [
293
- (k) => /^format$/.test(k),
294
- (k) => /^(format:write|format:prettier|format:fix|prettier)$/.test(k),
295
- (_k, v) => /prettier\b.*--write/.test(v)
296
- ]
297
- },
298
- {
299
- role: "build",
300
- exclude: /watch|storybook/,
301
- ladder: [(k) => /^build$/.test(k), (k) => /^build:/.test(k)]
302
- },
303
- {
304
- role: "dev",
305
- ladder: [(k) => /^dev$/.test(k), (k) => /^start$/.test(k), (k) => /^dev:/.test(k)]
306
- }
307
- ];
308
332
  function classifyCommands(scripts = {}) {
309
333
  const out = { raw: scripts };
310
334
  const entries = Object.entries(scripts);
@@ -319,27 +343,6 @@ function classifyCommands(scripts = {}) {
319
343
  }
320
344
  return out;
321
345
  }
322
- var FRAMEWORK_MARKERS = [
323
- ["next", "Next.js"],
324
- ["@tanstack/react-start", "TanStack Start"],
325
- ["@tanstack/start", "TanStack Start"],
326
- ["@tanstack/react-router", "TanStack Router"],
327
- ["react", "React"],
328
- ["vue", "Vue"],
329
- ["svelte", "Svelte"],
330
- ["@angular/core", "Angular"],
331
- ["express", "Express"],
332
- ["fastify", "Fastify"],
333
- ["@nestjs/core", "NestJS"],
334
- ["@trpc/server", "tRPC"],
335
- ["vite", "Vite"],
336
- ["react-scripts", "CRA"],
337
- ["@apollo/client", "Apollo"],
338
- ["drizzle-orm", "Drizzle"],
339
- ["prisma", "Prisma"],
340
- ["@sanity/client", "Sanity"],
341
- ["nx", "Nx"]
342
- ];
343
346
  function detectFrameworks(pkg) {
344
347
  if (!pkg) return [];
345
348
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
@@ -435,71 +438,6 @@ async function detectHooks(root, hooksPath, pkg) {
435
438
  }
436
439
  return { source: "none", preCommit: false, prePush: false, commitMsg: false, lintStaged };
437
440
  }
438
- var ARTIFACT_SPECS = [
439
- { id: "agents", label: "AGENTS.md (operating contract)", path: "AGENTS.md", kind: "contract" },
440
- {
441
- id: "project-context",
442
- label: "PROJECT_CONTEXT.md (ground-truth state)",
443
- path: "PROJECT_CONTEXT.md",
444
- kind: "state"
445
- },
446
- {
447
- id: "decisions",
448
- label: "DECISIONS.md (decision record)",
449
- path: "DECISIONS.md",
450
- kind: "decisions"
451
- },
452
- { id: "adr-dir", label: "ADR directory (docs/adr)", path: "docs/adr", kind: "decisions" },
453
- {
454
- id: "decisions-dir",
455
- label: "ADR directory (docs/decisions)",
456
- path: "docs/decisions",
457
- kind: "decisions"
458
- },
459
- { id: "claude", label: "CLAUDE.md (Claude Code pointer)", path: "CLAUDE.md", kind: "adapter" },
460
- {
461
- id: "copilot",
462
- label: "Copilot instructions",
463
- path: ".github/copilot-instructions.md",
464
- kind: "adapter"
465
- },
466
- {
467
- id: "cursorrules",
468
- label: ".cursorrules (legacy Cursor)",
469
- path: ".cursorrules",
470
- kind: "adapter"
471
- },
472
- { id: "cursor-rules", label: ".cursor/rules", path: ".cursor/rules", kind: "adapter" },
473
- { id: "gemini", label: "GEMINI.md", path: "GEMINI.md", kind: "adapter" },
474
- { id: "cline", label: ".clinerules", path: ".clinerules", kind: "adapter" },
475
- { id: "windsurf", label: ".windsurfrules", path: ".windsurfrules", kind: "adapter" },
476
- { id: "skills", label: "Agent skills (.claude/skills)", path: ".claude/skills", kind: "skill" },
477
- {
478
- id: "failure-modes-skill",
479
- label: "Failure-modes register (skill)",
480
- path: ".claude/skills/failure-modes",
481
- kind: "skill"
482
- },
483
- {
484
- id: "failure-modes-doc",
485
- label: "Failure-modes register (doc)",
486
- path: "docs/failure-modes.md",
487
- kind: "other"
488
- },
489
- {
490
- id: "sessions",
491
- label: "Session archive (docs/sessions)",
492
- path: "docs/sessions",
493
- kind: "sessions"
494
- },
495
- { id: "githooks", label: "Tracked git hooks (.githooks)", path: ".githooks", kind: "gate" },
496
- {
497
- id: "inventory",
498
- label: "Reuse inventory (docs/INVENTORY.md)",
499
- path: "docs/INVENTORY.md",
500
- kind: "map"
501
- }
502
- ];
503
441
  async function detectArtifacts(root) {
504
442
  const artifacts = await Promise.all(
505
443
  ARTIFACT_SPECS.map(async (spec) => ({
@@ -566,8 +504,187 @@ async function walkTree(root) {
566
504
  dirs.sort((a, b) => b.files - a.files);
567
505
  return { dirs, truncated: state.truncated };
568
506
  }
507
+ var IGNORED_DIRS, MEANINGFUL_DOT_DIRS, isMeta, ROLE_LADDERS, FRAMEWORK_MARKERS, ARTIFACT_SPECS;
508
+ var init_detect = __esm({
509
+ "src/core/detect.ts"() {
510
+ init_util();
511
+ IGNORED_DIRS = /* @__PURE__ */ new Set([
512
+ "node_modules",
513
+ ".git",
514
+ "dist",
515
+ "build",
516
+ "coverage",
517
+ ".next",
518
+ ".turbo",
519
+ ".cache",
520
+ ".pnpm",
521
+ "out"
522
+ ]);
523
+ MEANINGFUL_DOT_DIRS = /* @__PURE__ */ new Set([
524
+ ".github",
525
+ ".gitlab",
526
+ ".claude",
527
+ ".cursor",
528
+ ".githooks",
529
+ ".husky",
530
+ ".changeset",
531
+ ".vscode",
532
+ ".storybook"
533
+ ]);
534
+ isMeta = (value) => /npm-run-all|run-s\b|run-p\b|concurrently|&&/.test(value);
535
+ ROLE_LADDERS = [
536
+ {
537
+ role: "formatCheck",
538
+ ladder: [
539
+ (k) => /^format:check$/.test(k),
540
+ (k) => /^test:format(:local)?$/.test(k),
541
+ (k) => /^(prettier:check|check:format|format:ci)$/.test(k),
542
+ (_k, v) => /prettier(\s|.*\s)(-l\b|--check)/.test(v)
543
+ ]
544
+ },
545
+ {
546
+ role: "typecheck",
547
+ exclude: /generate|codegen|build|emit|watch/,
548
+ ladder: [
549
+ (k) => /^(typecheck|type-check)$/.test(k),
550
+ (k) => /^test:types(:local)?$/.test(k),
551
+ (k) => /^(types|check:types|types:check)$/.test(k),
552
+ (_k, v) => /tsc\b.*--noemit/i.test(v)
553
+ ]
554
+ },
555
+ {
556
+ role: "test",
557
+ exclude: /watch|coverage|snapshot|e2e|integration|debug|notsilent|mdx/,
558
+ ladder: [
559
+ (k) => /^test:unit(:local|:ci)?$/.test(k),
560
+ (k) => /^test:jest$/.test(k),
561
+ (k, v) => /^test$/.test(k) && !isMeta(v),
562
+ (k) => /^test$/.test(k)
563
+ ]
564
+ },
565
+ {
566
+ role: "lint",
567
+ exclude: /fix|format|staged|watch|style/,
568
+ ladder: [
569
+ (k) => /^lint$/.test(k),
570
+ (k) => /^test:lint(:local)?$/.test(k),
571
+ (k) => /^lint:/.test(k),
572
+ (_k, v) => /eslint\b/.test(v) && !/--fix/.test(v)
573
+ ]
574
+ },
575
+ {
576
+ role: "format",
577
+ exclude: /check|:ci$/,
578
+ ladder: [
579
+ (k) => /^format$/.test(k),
580
+ (k) => /^(format:write|format:prettier|format:fix|prettier)$/.test(k),
581
+ (_k, v) => /prettier\b.*--write/.test(v)
582
+ ]
583
+ },
584
+ {
585
+ role: "build",
586
+ exclude: /watch|storybook/,
587
+ ladder: [(k) => /^build$/.test(k), (k) => /^build:/.test(k)]
588
+ },
589
+ {
590
+ role: "dev",
591
+ ladder: [(k) => /^dev$/.test(k), (k) => /^start$/.test(k), (k) => /^dev:/.test(k)]
592
+ }
593
+ ];
594
+ FRAMEWORK_MARKERS = [
595
+ ["next", "Next.js"],
596
+ ["@tanstack/react-start", "TanStack Start"],
597
+ ["@tanstack/start", "TanStack Start"],
598
+ ["@tanstack/react-router", "TanStack Router"],
599
+ ["react", "React"],
600
+ ["vue", "Vue"],
601
+ ["svelte", "Svelte"],
602
+ ["@angular/core", "Angular"],
603
+ ["express", "Express"],
604
+ ["fastify", "Fastify"],
605
+ ["@nestjs/core", "NestJS"],
606
+ ["@trpc/server", "tRPC"],
607
+ ["vite", "Vite"],
608
+ ["react-scripts", "CRA"],
609
+ ["@apollo/client", "Apollo"],
610
+ ["drizzle-orm", "Drizzle"],
611
+ ["prisma", "Prisma"],
612
+ ["@sanity/client", "Sanity"],
613
+ ["nx", "Nx"]
614
+ ];
615
+ ARTIFACT_SPECS = [
616
+ { id: "agents", label: "AGENTS.md (operating contract)", path: "AGENTS.md", kind: "contract" },
617
+ {
618
+ id: "project-context",
619
+ label: "PROJECT_CONTEXT.md (ground-truth state)",
620
+ path: "PROJECT_CONTEXT.md",
621
+ kind: "state"
622
+ },
623
+ {
624
+ id: "decisions",
625
+ label: "DECISIONS.md (decision record)",
626
+ path: "DECISIONS.md",
627
+ kind: "decisions"
628
+ },
629
+ { id: "adr-dir", label: "ADR directory (docs/adr)", path: "docs/adr", kind: "decisions" },
630
+ {
631
+ id: "decisions-dir",
632
+ label: "ADR directory (docs/decisions)",
633
+ path: "docs/decisions",
634
+ kind: "decisions"
635
+ },
636
+ { id: "claude", label: "CLAUDE.md (Claude Code pointer)", path: "CLAUDE.md", kind: "adapter" },
637
+ {
638
+ id: "copilot",
639
+ label: "Copilot instructions",
640
+ path: ".github/copilot-instructions.md",
641
+ kind: "adapter"
642
+ },
643
+ {
644
+ id: "cursorrules",
645
+ label: ".cursorrules (legacy Cursor)",
646
+ path: ".cursorrules",
647
+ kind: "adapter"
648
+ },
649
+ { id: "cursor-rules", label: ".cursor/rules", path: ".cursor/rules", kind: "adapter" },
650
+ { id: "gemini", label: "GEMINI.md", path: "GEMINI.md", kind: "adapter" },
651
+ { id: "cline", label: ".clinerules", path: ".clinerules", kind: "adapter" },
652
+ { id: "windsurf", label: ".windsurfrules", path: ".windsurfrules", kind: "adapter" },
653
+ { id: "skills", label: "Agent skills (.claude/skills)", path: ".claude/skills", kind: "skill" },
654
+ {
655
+ id: "failure-modes-skill",
656
+ label: "Failure-modes register (skill)",
657
+ path: ".claude/skills/failure-modes",
658
+ kind: "skill"
659
+ },
660
+ {
661
+ id: "failure-modes-doc",
662
+ label: "Failure-modes register (doc)",
663
+ path: "docs/failure-modes.md",
664
+ kind: "other"
665
+ },
666
+ {
667
+ id: "sessions",
668
+ label: "Session archive (docs/sessions)",
669
+ path: "docs/sessions",
670
+ kind: "sessions"
671
+ },
672
+ { id: "githooks", label: "Tracked git hooks (.githooks)", path: ".githooks", kind: "gate" },
673
+ {
674
+ id: "inventory",
675
+ label: "Reuse inventory (docs/INVENTORY.md)",
676
+ path: "docs/INVENTORY.md",
677
+ kind: "map"
678
+ }
679
+ ];
680
+ }
681
+ });
569
682
 
570
683
  // src/core/scan.ts
684
+ var scan_exports = {};
685
+ __export(scan_exports, {
686
+ scanProject: () => scanProject
687
+ });
571
688
  function inferTicketKey(subjects) {
572
689
  const counts = /* @__PURE__ */ new Map();
573
690
  for (const m of subjects.matchAll(/\b([A-Z][A-Z0-9]{1,9})-\d+\b/g)) {
@@ -673,15 +790,23 @@ async function scanProject(root, opts = {}) {
673
790
  commands: classifyCommands(rootPkg?.scripts),
674
791
  ci,
675
792
  hooks,
793
+ publishable: Boolean(rootPkg) && rootPkg?.private !== true,
794
+ // `engines.vscode` is what makes a package an extension, and extensions publish through
795
+ // vsce — which never runs prepublishOnly.
796
+ publishRoute: !rootPkg ? "none" : rootPkg.engines?.vscode ? "vscode" : "npm",
676
797
  artifacts,
677
798
  freshness,
678
799
  tree
679
800
  };
680
801
  }
681
- var ETYMD_DIR = ".etymd";
682
- var CACHE_DIR = path.join(ETYMD_DIR, "cache");
683
- var CACHE_FACTS_FILE = path.join(CACHE_DIR, "facts.json");
684
- var BASELINE_FILE = path.join(ETYMD_DIR, "baseline.json");
802
+ var init_scan = __esm({
803
+ "src/core/scan.ts"() {
804
+ init_version();
805
+ init_version2();
806
+ init_detect();
807
+ init_util();
808
+ }
809
+ });
685
810
  function cacheFactsPath(root) {
686
811
  return path.join(root, CACHE_FACTS_FILE);
687
812
  }
@@ -718,14 +843,25 @@ async function readBaseline(root) {
718
843
  function deriveProfile(facts) {
719
844
  return (facts.git.recentAuthors ?? 1) > 2 ? "team" : "solo";
720
845
  }
846
+ var ETYMD_DIR, CACHE_DIR, CACHE_FACTS_FILE, BASELINE_FILE;
847
+ var init_facts = __esm({
848
+ "src/core/facts.ts"() {
849
+ init_util();
850
+ ETYMD_DIR = ".etymd";
851
+ CACHE_DIR = path.join(ETYMD_DIR, "cache");
852
+ CACHE_FACTS_FILE = path.join(CACHE_DIR, "facts.json");
853
+ BASELINE_FILE = path.join(ETYMD_DIR, "baseline.json");
854
+ }
855
+ });
721
856
 
722
857
  // src/core/config.ts
723
- var CONFIG_FILE = path.join(ETYMD_DIR, "config.json");
724
- var DEFAULT_CONFIG = {
725
- instructions: { include: [], exclude: [] },
726
- context: { perFileWords: 4e3, totalWords: 8e3 },
727
- state: { staleAfterDays: 30, maxChars: 9500 }
728
- };
858
+ var config_exports = {};
859
+ __export(config_exports, {
860
+ CONFIG_FILE: () => CONFIG_FILE,
861
+ DEFAULT_CONFIG: () => DEFAULT_CONFIG,
862
+ configPath: () => configPath,
863
+ readConfig: () => readConfig
864
+ });
729
865
  function configPath(root) {
730
866
  return path.join(root, CONFIG_FILE);
731
867
  }
@@ -769,6 +905,23 @@ async function readConfig(root) {
769
905
  const instructions = obj.instructions ?? {};
770
906
  const context = obj.context ?? {};
771
907
  const state = obj.state ?? {};
908
+ const gates = obj.gates ?? {};
909
+ const failOn = (() => {
910
+ if (gates.failOn === void 0) return void 0;
911
+ if (typeof gates.failOn !== "string" || !["risk", "gap", "polish"].includes(gates.failOn.toLowerCase())) {
912
+ problems.push(`${CONFIG_FILE}: \`gates.failOn\` must be risk | gap | polish \u2014 ignored.`);
913
+ return void 0;
914
+ }
915
+ return gates.failOn.toLowerCase();
916
+ })();
917
+ const publishGate = (() => {
918
+ if (gates.publishGate === void 0) return void 0;
919
+ if (typeof gates.publishGate !== "boolean") {
920
+ problems.push(`${CONFIG_FILE}: \`gates.publishGate\` must be true or false \u2014 ignored.`);
921
+ return void 0;
922
+ }
923
+ return gates.publishGate;
924
+ })();
772
925
  return {
773
926
  present: true,
774
927
  problems,
@@ -784,63 +937,30 @@ async function readConfig(root) {
784
937
  state: {
785
938
  staleAfterDays: readBudget(state.staleAfterDays, "state.staleAfterDays", problems) ?? DEFAULT_CONFIG.state.staleAfterDays,
786
939
  maxChars: readBudget(state.maxChars, "state.maxChars", problems) ?? DEFAULT_CONFIG.state.maxChars
940
+ },
941
+ gates: {
942
+ commands: readGlobList(gates.commands, "gates.commands", problems) ?? DEFAULT_CONFIG.gates.commands,
943
+ failOn: failOn ?? DEFAULT_CONFIG.gates.failOn,
944
+ publishGate,
945
+ allowWriting: readGlobList(gates.allowWriting, "gates.allowWriting", problems) ?? DEFAULT_CONFIG.gates.allowWriting
787
946
  }
788
947
  }
789
948
  };
790
949
  }
791
-
792
- // src/core/context.ts
793
- var ALWAYS_LOADED = [
794
- { path: "AGENTS.md", role: "operating contract" },
795
- { path: "PROJECT_CONTEXT.md", role: "ground-truth state" },
796
- { path: "CLAUDE.md", role: "Claude Code pointer" },
797
- { path: "GEMINI.md", role: "Gemini pointer" },
798
- { path: ".github/copilot-instructions.md", role: "Copilot instructions" },
799
- { path: ".cursorrules", role: "Cursor rules (legacy)" }
800
- ];
801
- var EXTRACTION_THRESHOLD = DEFAULT_CONFIG.context.perFileWords;
802
- function isAlwaysAppliedCursorRule(text) {
803
- const fm = text.match(/^---\n([\s\S]*?)\n---/);
804
- if (!fm) return true;
805
- const frontmatter = fm[1] ?? "";
806
- return /^\s*alwaysApply\s*:\s*true\s*$/m.test(frontmatter);
807
- }
808
- async function measureContext(root, perFileWords = EXTRACTION_THRESHOLD) {
809
- const files = [];
810
- for (const spec of ALWAYS_LOADED) {
811
- const abs = path.join(root, spec.path);
812
- const text = await readText(abs);
813
- if (text === null) continue;
814
- const words = wordCount(text);
815
- files.push({ path: spec.path, role: spec.role, words, approxTokens: approxTokens(words) });
816
- }
817
- const rulesDir = path.join(root, ".cursor", "rules");
818
- if (await pathExists(rulesDir)) {
819
- try {
820
- for (const entry of await promises.readdir(rulesDir)) {
821
- if (!entry.endsWith(".mdc") && !entry.endsWith(".md")) continue;
822
- const text = await readText(path.join(rulesDir, entry));
823
- if (text === null || !isAlwaysAppliedCursorRule(text)) continue;
824
- const words = wordCount(text);
825
- files.push({
826
- path: `.cursor/rules/${entry}`,
827
- role: "Cursor rule (always applied)",
828
- words,
829
- approxTokens: approxTokens(words)
830
- });
831
- }
832
- } catch {
833
- }
950
+ var CONFIG_FILE, DEFAULT_CONFIG;
951
+ var init_config = __esm({
952
+ "src/core/config.ts"() {
953
+ init_facts();
954
+ init_util();
955
+ CONFIG_FILE = path.join(ETYMD_DIR, "config.json");
956
+ DEFAULT_CONFIG = {
957
+ instructions: { include: [], exclude: [] },
958
+ context: { perFileWords: 4e3, totalWords: 8e3 },
959
+ state: { staleAfterDays: 30, maxChars: 9500 },
960
+ gates: { commands: [], failOn: "risk", allowWriting: [] }
961
+ };
834
962
  }
835
- const totalWords = files.reduce((s, f) => s + f.words, 0);
836
- return {
837
- files: files.sort((a, b) => b.words - a.words),
838
- totalWords,
839
- totalApproxTokens: approxTokens(totalWords),
840
- perFileWords,
841
- extractionCandidates: files.filter((f) => f.words >= perFileWords)
842
- };
843
- }
963
+ });
844
964
 
845
965
  // src/pack/templates.ts
846
966
  function runPrefix(pm) {
@@ -949,26 +1069,133 @@ ${[
949
1069
  <!-- etymd pack v${PACK_VERSION} -->
950
1070
  `;
951
1071
  }
1072
+ function localHookCall(hook) {
1073
+ return `# Repo-owned checks. This file is generated and will be overwritten; \`.githooks/${hook}.local\`
1074
+ # is yours \u2014 etymd never reads, writes, or regenerates it. Put project-specific guards there.
1075
+ LOCAL="$(dirname "$0")/${hook}.local"
1076
+ if [ -x "$LOCAL" ]; then
1077
+ "$LOCAL" "$@" || exit 1
1078
+ fi`;
1079
+ }
952
1080
  function generatePreCommitHook() {
953
1081
  return `#!/usr/bin/env sh
954
1082
  # etymd: process gate. Cheap, locally-knowable checks belong here (fast, blocks the commit).
1083
+
1084
+ ${localHookCall("pre-commit")}
1085
+
1086
+ # Content screen \u2014 staged file bytes. Refuses to commit environment, employer or identity
1087
+ # detail into a repo whose history is (or could become) public. The checker and its patterns
1088
+ # are machine-local by design, so this is a NO-OP wherever no checker is installed: safe to
1089
+ # commit anywhere, active only where you opted in.
1090
+ #
1091
+ # Bypass, with a reason: git commit --no-verify
1092
+ ${CONTENT_GATE_RESOLUTION}
1093
+ if [ -x "$GATE" ]; then
1094
+ "$GATE" screen --staged || exit 1
1095
+ fi
1096
+
955
1097
  exit 0
956
1098
  `;
957
1099
  }
958
- function generatePrePushHook(facts) {
1100
+ function generateCommitMsgHook() {
1101
+ return `#!/usr/bin/env sh
1102
+ # etymd: content screen \u2014 the commit message itself.
1103
+ #
1104
+ # The staged-content gate reads file bytes and never sees the message, yet a message is as
1105
+ # permanently published as any file. No-op where no checker is installed.
1106
+ #
1107
+ # Bypass, with a reason: git commit --no-verify
1108
+ GATE="\${COMMIT_MSG_GATE:-$(command -v etymd || true)}"
1109
+ if [ -x "$GATE" ]; then
1110
+ "$GATE" screen --message "$1" || exit 1
1111
+ fi
1112
+
1113
+ ${localHookCall("commit-msg")}
1114
+
1115
+ exit 0
1116
+ `;
1117
+ }
1118
+ function generatePrePushHook(facts, gates) {
959
1119
  const run = runPrefix(facts.packageManager);
960
1120
  const c = facts.commands;
961
- const candidates = [c.formatCheck, c.typecheck, c.lint];
962
- const steps = candidates.filter((key) => Boolean(key) && isSafeGateCommand(c.raw[key])).map((key) => `${run} ${key}`);
1121
+ const candidates = gates?.commands.length ? gates.commands : [c.formatCheck, c.typecheck, c.lint];
1122
+ const allowed = new Set(gates?.allowWriting ?? []);
1123
+ const steps = candidates.filter(
1124
+ (key) => Boolean(key) && (allowed.has(key) || isSafeGateCommand(c.raw[key]))
1125
+ ).map((key) => `${run} ${key}`);
963
1126
  const body = steps.length ? steps.map((s) => `echo "\u203A ${s}"
964
1127
  ${s} || exit 1`).join("\n") : 'echo "etymd: no correctness commands detected \u2014 add format:check / typecheck / lint"';
1128
+ const failOn = gates?.failOn ?? "risk";
1129
+ const auditStep = `
1130
+ if command -v etymd >/dev/null 2>&1; then
1131
+ echo "\u203A etymd audit --fail-on ${failOn}"
1132
+ etymd audit --no-ledger --fail-on ${failOn} || exit 1
1133
+ else
1134
+ echo "\u203A etymd audit skipped (not on PATH)"
1135
+ fi`;
965
1136
  return `#!/usr/bin/env sh
966
1137
  # etymd: correctness gate. Mirrors CI cheapest-first; blocks the push on any failure.
1138
+
1139
+ ${localHookCall("pre-push")}
967
1140
  ${body}
1141
+ ${auditStep}
1142
+
1143
+ # Content screen, second pass \u2014 the WHOLE TREE rather than one diff. Catches anything committed
1144
+ # with --no-verify and anything a rebase or merge brought in from elsewhere. Advisory here (it
1145
+ # never blocks the push): the blocking decision belongs at commit time, where the fix is cheap.
1146
+ ${CONTENT_GATE_RESOLUTION}
1147
+ if [ -x "$GATE" ] && [ "\${CONTENT_GATE_PREPUSH:-1}" = "1" ]; then
1148
+ "$GATE" screen --tree --advisory || true
1149
+ fi
1150
+
1151
+ exit 0
968
1152
  `;
969
1153
  }
1154
+ function generateArtifactCheckScript() {
1155
+ return `#!/usr/bin/env sh
1156
+ # etymd: content screen \u2014 the published ARTIFACT, not the repository.
1157
+ #
1158
+ # Wire it into the irreversible moment:
1159
+ # package.json \u2192 "prepublishOnly": "./scripts/artifact-check.sh"
1160
+ #
1161
+ # No-op where no checker is installed. Bypass is deliberate and loud: ARTIFACT_CHECK_SKIP=1.
1162
+ set -eu
1163
+
1164
+ if [ "\${ARTIFACT_CHECK_SKIP:-0}" = "1" ]; then
1165
+ echo "\u203A artifact-check: SKIPPED by ARTIFACT_CHECK_SKIP=1"
1166
+ exit 0
1167
+ fi
1168
+
1169
+ ${CONTENT_GATE_RESOLUTION}
1170
+ [ -x "$GATE" ] || { echo "\u203A artifact-check: no checker installed \u2014 skipping."; exit 0; }
1171
+
1172
+ WORK=$(mktemp -d)
1173
+ trap 'rm -rf "$WORK"' EXIT INT TERM
1174
+
1175
+ # Pack exactly what would ship, then screen the unpacked bytes.
1176
+ if [ -f package.json ]; then
1177
+ npm pack --pack-destination "$WORK" >/dev/null 2>&1 || {
1178
+ echo "\u203A artifact-check: npm pack failed \u2014 cannot verify what would ship" >&2; exit 1; }
1179
+ tar -xzf "$WORK"/*.tgz -C "$WORK" 2>/dev/null || true
1180
+ fi
1181
+
1182
+ "$GATE" screen --dir "$WORK" || exit 1
1183
+ exit 0
1184
+ `;
1185
+ }
1186
+ var CONTENT_GATE_RESOLUTION;
1187
+ var init_templates = __esm({
1188
+ "src/pack/templates.ts"() {
1189
+ init_version();
1190
+ CONTENT_GATE_RESOLUTION = `GATE="\${CONTENT_GATE:-$(command -v etymd || true)}"`;
1191
+ }
1192
+ });
970
1193
 
971
1194
  // src/core/generate.ts
1195
+ var generate_exports = {};
1196
+ __export(generate_exports, {
1197
+ planWorkflow: () => planWorkflow
1198
+ });
972
1199
  async function planWorkflow(root, facts, opts) {
973
1200
  const out = [];
974
1201
  const add = async (rel, contents, label, executable = false) => {
@@ -989,10 +1216,96 @@ async function planWorkflow(root, facts, opts) {
989
1216
  }
990
1217
  if (opts.gates) {
991
1218
  await add(".githooks/pre-commit", generatePreCommitHook(), "Process gate (pre-commit)", true);
992
- await add(".githooks/pre-push", generatePrePushHook(facts), "Correctness gate (pre-push)", true);
1219
+ await add(
1220
+ ".githooks/commit-msg",
1221
+ generateCommitMsgHook(),
1222
+ "Content screen (commit message)",
1223
+ true
1224
+ );
1225
+ await add(
1226
+ ".githooks/pre-push",
1227
+ generatePrePushHook(facts, opts.gateConfig),
1228
+ "Correctness gate (pre-push)",
1229
+ true
1230
+ );
1231
+ if (opts.gateConfig?.publishGate ?? opts.publishGate ?? facts.publishable) {
1232
+ await add(
1233
+ "scripts/artifact-check.sh",
1234
+ generateArtifactCheckScript(),
1235
+ "Content screen (published artifact)",
1236
+ true
1237
+ );
1238
+ }
993
1239
  }
994
1240
  return out;
995
1241
  }
1242
+ var init_generate = __esm({
1243
+ "src/core/generate.ts"() {
1244
+ init_templates();
1245
+ init_util();
1246
+ }
1247
+ });
1248
+
1249
+ // src/index.ts
1250
+ init_scan();
1251
+
1252
+ // src/core/context.ts
1253
+ init_config();
1254
+ init_util();
1255
+ var ALWAYS_LOADED = [
1256
+ { path: "AGENTS.md", role: "operating contract" },
1257
+ { path: "PROJECT_CONTEXT.md", role: "ground-truth state" },
1258
+ { path: "CLAUDE.md", role: "Claude Code pointer" },
1259
+ { path: "GEMINI.md", role: "Gemini pointer" },
1260
+ { path: ".github/copilot-instructions.md", role: "Copilot instructions" },
1261
+ { path: ".cursorrules", role: "Cursor rules (legacy)" }
1262
+ ];
1263
+ var EXTRACTION_THRESHOLD = DEFAULT_CONFIG.context.perFileWords;
1264
+ function isAlwaysAppliedCursorRule(text) {
1265
+ const fm = text.match(/^---\n([\s\S]*?)\n---/);
1266
+ if (!fm) return true;
1267
+ const frontmatter = fm[1] ?? "";
1268
+ return /^\s*alwaysApply\s*:\s*true\s*$/m.test(frontmatter);
1269
+ }
1270
+ async function measureContext(root, perFileWords = EXTRACTION_THRESHOLD) {
1271
+ const files = [];
1272
+ for (const spec of ALWAYS_LOADED) {
1273
+ const abs = path.join(root, spec.path);
1274
+ const text = await readText(abs);
1275
+ if (text === null) continue;
1276
+ const words = wordCount(text);
1277
+ files.push({ path: spec.path, role: spec.role, words, approxTokens: approxTokens(words) });
1278
+ }
1279
+ const rulesDir = path.join(root, ".cursor", "rules");
1280
+ if (await pathExists(rulesDir)) {
1281
+ try {
1282
+ for (const entry of await promises.readdir(rulesDir)) {
1283
+ if (!entry.endsWith(".mdc") && !entry.endsWith(".md")) continue;
1284
+ const text = await readText(path.join(rulesDir, entry));
1285
+ if (text === null || !isAlwaysAppliedCursorRule(text)) continue;
1286
+ const words = wordCount(text);
1287
+ files.push({
1288
+ path: `.cursor/rules/${entry}`,
1289
+ role: "Cursor rule (always applied)",
1290
+ words,
1291
+ approxTokens: approxTokens(words)
1292
+ });
1293
+ }
1294
+ } catch {
1295
+ }
1296
+ }
1297
+ const totalWords = files.reduce((s, f) => s + f.words, 0);
1298
+ return {
1299
+ files: files.sort((a, b) => b.words - a.words),
1300
+ totalWords,
1301
+ totalApproxTokens: approxTokens(totalWords),
1302
+ perFileWords,
1303
+ extractionCandidates: files.filter((f) => f.words >= perFileWords)
1304
+ };
1305
+ }
1306
+
1307
+ // src/index.ts
1308
+ init_generate();
996
1309
  async function applyFiles(root, files, overwrite = /* @__PURE__ */ new Set()) {
997
1310
  const written = [];
998
1311
  const skipped = [];
@@ -1010,7 +1323,18 @@ async function applyFiles(root, files, overwrite = /* @__PURE__ */ new Set()) {
1010
1323
  return { written, skipped };
1011
1324
  }
1012
1325
 
1326
+ // src/index.ts
1327
+ init_facts();
1328
+ init_config();
1329
+
1330
+ // src/engine/run.ts
1331
+ init_facts();
1332
+ init_config();
1333
+ init_scan();
1334
+ init_util();
1335
+
1013
1336
  // src/lenses/context-economy.ts
1337
+ init_config();
1014
1338
  var LENS_ID = "context-economy";
1015
1339
  var TOTAL_BUDGET_WORDS = DEFAULT_CONFIG.context.totalWords;
1016
1340
  var contextEconomyLens = {
@@ -1062,6 +1386,12 @@ var contextEconomyLens = {
1062
1386
  };
1063
1387
  }
1064
1388
  };
1389
+
1390
+ // src/lenses/gate-integrity/lens.ts
1391
+ init_util();
1392
+
1393
+ // src/lenses/gate-integrity/inventory.ts
1394
+ init_util();
1065
1395
  var TOOL_MATCHERS = [
1066
1396
  ["sonar", /sonar-scanner|sonarqube|\bsonar\b/i],
1067
1397
  ["codecov", /codecov/i],
@@ -1581,6 +1911,16 @@ var gateIntegrityLens = {
1581
1911
  };
1582
1912
  }
1583
1913
  };
1914
+
1915
+ // src/lenses/instruction-truth/lens.ts
1916
+ init_version();
1917
+ init_config();
1918
+ init_facts();
1919
+ init_util();
1920
+
1921
+ // src/lenses/instruction-truth/claims.ts
1922
+ init_detect();
1923
+ init_util();
1584
1924
  async function listInstructionFiles(root, facts, scope) {
1585
1925
  const files = [];
1586
1926
  const add = async (rel) => {
@@ -2089,6 +2429,10 @@ var instructionTruthLens = {
2089
2429
  };
2090
2430
  }
2091
2431
  };
2432
+
2433
+ // src/lenses/state-freshness.ts
2434
+ init_config();
2435
+ init_util();
2092
2436
  var LENS_ID4 = "state-freshness";
2093
2437
  var DECISIONS_FORMAT_MARKER = "<!-- decisions-format: 1 -->";
2094
2438
  var MS_PER_DAY = 864e5;
@@ -2298,6 +2642,9 @@ function rankFindings(findings) {
2298
2642
  (a, b) => TIER_ORDER[a.tier] - TIER_ORDER[b.tier] || EFFORT_ORDER[a.effort] - EFFORT_ORDER[b.effort]
2299
2643
  );
2300
2644
  }
2645
+
2646
+ // src/engine/ledger.ts
2647
+ init_util();
2301
2648
  function ledgerPath(root) {
2302
2649
  return path.join(root, ".etymd", "ledger.json");
2303
2650
  }
@@ -2443,6 +2790,13 @@ async function runAudit(root, opts = {}) {
2443
2790
  diff
2444
2791
  };
2445
2792
  }
2793
+
2794
+ // src/core/fleet.ts
2795
+ init_util();
2796
+ var FLEET_TRUST_VALUES = ["public-repo", "public-bound", "private"];
2797
+ function isFleetTrust(value) {
2798
+ return typeof value === "string" && FLEET_TRUST_VALUES.includes(value);
2799
+ }
2446
2800
  function expandTilde(p) {
2447
2801
  if (p === "~") return os.homedir();
2448
2802
  if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
@@ -2538,7 +2892,10 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
2538
2892
  private: rec.private === true,
2539
2893
  path: asString(rec.path),
2540
2894
  upstream: asString(rec.upstream),
2541
- trust: asString(rec.trust),
2895
+ // An unknown value is NOT coerced to a default: trust gates content screening, so a typo
2896
+ // must surface as "undeclared" (a finding) rather than quietly picking an answer.
2897
+ trust: isFleetTrust(rec.trust) ? rec.trust : void 0,
2898
+ trustRaw: asString(rec.trust),
2542
2899
  staleAfterDays: typeof rec.staleAfterDays === "number" ? rec.staleAfterDays : void 0,
2543
2900
  stateBudget: typeof rec.stateBudget === "number" ? rec.stateBudget : void 0,
2544
2901
  contract,
@@ -2655,6 +3012,24 @@ async function loadFleetManifest(manifestPath) {
2655
3012
  manifest.machineProfile = local.data.machineProfile === "corp" || local.data.machineProfile === "personal" ? local.data.machineProfile : void 0;
2656
3013
  const rootRaw = asString(local.data.root) ?? asString(parsed.root);
2657
3014
  if (rootRaw) manifest.root = path.resolve(dir, expandTilde(rootRaw));
3015
+ const orientationRec = asRecord(parsed.orientation);
3016
+ if (orientationRec) {
3017
+ const orientationRoot = asString(orientationRec.root);
3018
+ if (orientationRoot) manifest.orientation = { root: orientationRoot };
3019
+ else {
3020
+ manifest.problems.push({
3021
+ kind: "bad-shape",
3022
+ file: manifestFile,
3023
+ detail: "`orientation` declares no `root` name \u2014 the fleet's orientation cannot be resolved"
3024
+ });
3025
+ }
3026
+ } else if (parsed.orientation !== void 0) {
3027
+ manifest.problems.push({
3028
+ kind: "bad-shape",
3029
+ file: manifestFile,
3030
+ detail: '`orientation` must be an object (`{ "root": "<name>" }`) \u2014 ignored'
3031
+ });
3032
+ }
2658
3033
  loadRegistryEntries(parsed.projects, manifest, manifestFile);
2659
3034
  } else if (Array.isArray(parsed.sources)) {
2660
3035
  manifest.shape = "corpus";
@@ -2672,6 +3047,11 @@ async function loadFleetManifest(manifestPath) {
2672
3047
  }
2673
3048
  return manifest;
2674
3049
  }
3050
+
3051
+ // src/engine/fleet.ts
3052
+ init_config();
3053
+ init_facts();
3054
+ init_util();
2675
3055
  var pExecFile2 = promisify(execFile);
2676
3056
  var FLEET_LENS = "fleet-manifest";
2677
3057
  var FLEET_JSON_SCHEMA = "fleet-experimental-0.2";
@@ -2797,6 +3177,19 @@ async function checkManifest(manifest) {
2797
3177
  );
2798
3178
  }
2799
3179
  seen.add(entry.name);
3180
+ if (entry.profile !== "corp" && !entry.private && !entry.trust) {
3181
+ const bad = entry.trustRaw;
3182
+ findings.push(
3183
+ finding3(
3184
+ `${FLEET_LENS}/${bad ? "unknown-trust" : "undeclared-trust"}:${entry.name}`,
3185
+ "risk",
3186
+ bad ? `\`${entry.name}\` declares unknown trust \`${bad}\`` : `\`${entry.name}\` declares no \`trust\``,
3187
+ [`${manifestFile}: ${entry.name}${bad ? `.trust = ${bad}` : " has no trust field"}`],
3188
+ "Trust decides whether content screening applies. An unanswered entry is not a private one \u2014 publishing exposes ALL history, so a repo that went public without ever answering is exactly what a default would hide.",
3189
+ `Declare one of ${FLEET_TRUST_VALUES.map((t) => `\`${t}\``).join(", ")}.`
3190
+ )
3191
+ );
3192
+ }
2800
3193
  if (entry.private && entry.path) {
2801
3194
  findings.push(
2802
3195
  finding3(
@@ -2851,6 +3244,18 @@ async function checkManifest(manifest) {
2851
3244
  }
2852
3245
  }
2853
3246
  }
3247
+ if (manifest.orientation && !seen.has(manifest.orientation.root)) {
3248
+ findings.push(
3249
+ finding3(
3250
+ `${FLEET_LENS}/dangling-orientation:${manifest.orientation.root}`,
3251
+ "gap",
3252
+ `\`orientation.root\` names \`${manifest.orientation.root}\`, which is not a registered name`,
3253
+ [`${manifestFile}: orientation.root = ${manifest.orientation.root}`],
3254
+ "Every entry is guided by the orientation root implicitly \u2014 if it resolves to nothing, the whole fleet's orientation is a claim nothing can follow.",
3255
+ "Point it at a registered name, or drop the block if this fleet has no orientation root."
3256
+ )
3257
+ );
3258
+ }
2854
3259
  for (const name of Object.keys(manifest.localDirs)) {
2855
3260
  if (manifest.shape === "registry" && !seen.has(name)) {
2856
3261
  findings.push(
@@ -3061,6 +3466,52 @@ async function checkCorpEmails(manifest, findings, disclosures) {
3061
3466
  }
3062
3467
  }
3063
3468
  }
3469
+ async function checkGateDrift(manifest, findings, disclosures) {
3470
+ const { planWorkflow: planWorkflow2 } = await Promise.resolve().then(() => (init_generate(), generate_exports));
3471
+ const { scanProject: scanProject2 } = await Promise.resolve().then(() => (init_scan(), scan_exports));
3472
+ const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
3473
+ for (const entry of manifest.entries) {
3474
+ const root = entry.resolvedRoot;
3475
+ if (!root || !await isDirectory(root)) continue;
3476
+ if (entry.profile === "corp" || entry.private) continue;
3477
+ const facts = await scanProject2(root);
3478
+ if (!facts.git.isRepo) continue;
3479
+ if (facts.hooks.source === "husky" || facts.hooks.source === "husky-legacy") {
3480
+ disclosures.push(
3481
+ `${entry.name}: hooks managed by ${facts.hooks.source} \u2014 gate drift not comparable against the .githooks pack`
3482
+ );
3483
+ continue;
3484
+ }
3485
+ const { config } = await readConfig2(root);
3486
+ const planned = (await planWorkflow2(root, facts, { agents: false, gates: true, gateConfig: config.gates })).filter((f) => f.executable);
3487
+ const missing = planned.filter((f) => !f.exists).map((f) => f.path);
3488
+ const stale = planned.filter((f) => f.exists && f.differs).map((f) => f.path);
3489
+ if (missing.length) {
3490
+ findings.push(
3491
+ finding3(
3492
+ `${FLEET_LENS}/gate-missing:${entry.name}`,
3493
+ "gap",
3494
+ `\`${entry.name}\` is missing ${missing.length} gate${missing.length === 1 ? "" : "s"} the pack would install`,
3495
+ missing.map((m) => `${entry.name}: no ${m}`),
3496
+ "A gate absent in one repo is invisible from inside it \u2014 every commit there skips a check its siblings run.",
3497
+ "Run `etymd gates` in that repo."
3498
+ )
3499
+ );
3500
+ }
3501
+ if (stale.length) {
3502
+ findings.push(
3503
+ finding3(
3504
+ `${FLEET_LENS}/gate-stale:${entry.name}`,
3505
+ "polish",
3506
+ `\`${entry.name}\` has ${stale.length} gate${stale.length === 1 ? "" : "s"} that differ from what its own inputs generate`,
3507
+ stale.map((s) => `${entry.name}: ${s}`),
3508
+ "A hand-edited or outdated hook drifts from the pack silently \u2014 the repo believes it runs checks the file no longer contains.",
3509
+ "Re-run `etymd gates` (it asks before overwriting), or record the intent in .etymd/config.json."
3510
+ )
3511
+ );
3512
+ }
3513
+ }
3514
+ }
3064
3515
  async function collectWallFindings(manifest) {
3065
3516
  const findings = [];
3066
3517
  const disclosures = [];
@@ -3069,6 +3520,7 @@ async function collectWallFindings(manifest) {
3069
3520
  await checkManifestRepoMachinePaths(manifest, findings, disclosures);
3070
3521
  await checkHygieneNeedles(manifest, findings, disclosures);
3071
3522
  await checkCorpEmails(manifest, findings, disclosures);
3523
+ await checkGateDrift(manifest, findings, disclosures);
3072
3524
  return { findings, disclosures };
3073
3525
  }
3074
3526
  function stateAgeDays(facts) {
@@ -3194,4 +3646,8 @@ async function sweepFleet(manifest, opts = {}) {
3194
3646
  };
3195
3647
  }
3196
3648
 
3649
+ // src/index.ts
3650
+ init_version();
3651
+ init_version2();
3652
+
3197
3653
  export { CONFIG_FILE, DECISIONS_FORMAT_MARKER, DEFAULT_CONFIG, EXTRACTION_THRESHOLD, FLEET_JSON_SCHEMA, FLEET_LENS, LENSES, NAME, PACK_VERSION, TOTAL_BUDGET_WORDS, VERSION, applyFiles, baselineCarriesMachinePath, baselinePath, buildGateInventory, cacheFactsPath, checkManifest, collectWallFindings, configPath, contextEconomyLens, corpPersistenceRoot, deriveProfile, expandTilde, extractCommandClaims, extractPathClaims, gateIntegrityLens, instructionTruthLens, isAlwaysAppliedCursorRule, listInstructionFiles, loadFleetManifest, measureContext, planWorkflow, rankFindings, readBaseline, readCachedFacts, readConfig, readLedger, reconcileLedger, runAudit, scanProject, stateFreshnessLens, sweepFleet, withoutMachinePath, writeBaseline, writeCachedFacts, writeLedger };