etymd 0.2.1 → 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.
- package/CHANGELOG.md +71 -2
- package/README.md +276 -61
- package/dist/{approve-X3AW7KLD.js → approve-JEOEC2MV.js} +6 -4
- package/dist/audit-GYZ5YZ7U.js +12 -0
- package/dist/{brief-RT6KE4CN.js → brief-ZK7TXAC4.js} +6 -4
- package/dist/{chunk-4UZBCCFJ.js → chunk-3NYDVB4Z.js} +123 -25
- package/dist/chunk-CRJHIXJH.js +5 -0
- package/dist/chunk-EF6BPBG6.js +22 -0
- package/dist/chunk-LT67FU2Y.js +59 -0
- package/dist/{chunk-E2Q7WFPN.js → chunk-MKYPLNLH.js} +8 -5
- package/dist/{chunk-EUIHYD6F.js → chunk-MMPV67FW.js} +2 -2
- package/dist/{chunk-C4Z6NXDO.js → chunk-NFENQ3IK.js} +1 -1
- package/dist/{chunk-ANCCGYSH.js → chunk-NMZ3RHWW.js} +31 -59
- package/dist/{chunk-FZSIOCOZ.js → chunk-R74SJ7HS.js} +1 -1
- package/dist/{chunk-LBRNZZZF.js → chunk-TADKOW6P.js} +1 -99
- package/dist/{chunk-SS7VGLJ5.js → chunk-V3DPLOOM.js} +71 -15
- package/dist/{chunk-KBF3SW3N.js → chunk-WWBF4Y7K.js} +1 -1
- package/dist/{chunk-6W3J4PJ6.js → chunk-XTAX4YNT.js} +2 -2
- package/dist/chunk-YLNQQZ5F.js +102 -0
- package/dist/cli.js +50 -16
- package/dist/config-MX7OYI7F.js +4 -0
- package/dist/{context-GYCLR3TG.js → context-4DGXO7JR.js} +5 -3
- package/dist/doctor-C2RTL2QV.js +19 -0
- package/dist/{fleet-IS6YWDDV.js → fleet-YZGR4UC2.js} +243 -10
- package/dist/gates-ILE4PXDA.js +272 -0
- package/dist/generate-IYCQETQB.js +4 -0
- package/dist/index.d.ts +113 -33
- package/dist/index.js +777 -267
- package/dist/{init-ISEFZWPV.js → init-OVLO4OBZ.js} +8 -5
- package/dist/ledger-TMIV45AA.js +5 -0
- package/dist/{scan-ADZ2RKYY.js → scan-RAJUFTQR.js} +6 -4
- package/dist/scan-ZYZ7XDSJ.js +5 -0
- package/dist/screen-U7JMWZPS.js +134 -0
- package/package.json +2 -2
- package/dist/audit-YH74YZFQ.js +0 -9
- package/dist/doctor-EAA4IP4Y.js +0 -16
- package/dist/gates-3NPKF4IZ.js +0 -71
- 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
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
20
|
-
var
|
|
21
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
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
|
+
|
|
1097
|
+
exit 0
|
|
1098
|
+
`;
|
|
1099
|
+
}
|
|
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
|
+
|
|
955
1115
|
exit 0
|
|
956
1116
|
`;
|
|
957
1117
|
}
|
|
958
|
-
function generatePrePushHook(facts) {
|
|
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
|
|
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(
|
|
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],
|
|
@@ -1076,24 +1406,78 @@ var TOOL_MATCHERS = [
|
|
|
1076
1406
|
["lint", /\beslint\b/],
|
|
1077
1407
|
["test", /\bjest\b|\bvitest\b|react-scripts test|\bnx\b[^&|;]*\btest\b/]
|
|
1078
1408
|
];
|
|
1409
|
+
var PM_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
1410
|
+
"run",
|
|
1411
|
+
"run-script",
|
|
1412
|
+
"install",
|
|
1413
|
+
"i",
|
|
1414
|
+
"ci",
|
|
1415
|
+
"add",
|
|
1416
|
+
"remove",
|
|
1417
|
+
"rm",
|
|
1418
|
+
"uninstall",
|
|
1419
|
+
"un",
|
|
1420
|
+
"update",
|
|
1421
|
+
"up",
|
|
1422
|
+
"upgrade",
|
|
1423
|
+
"create",
|
|
1424
|
+
"init",
|
|
1425
|
+
"audit",
|
|
1426
|
+
"publish",
|
|
1427
|
+
"pack",
|
|
1428
|
+
"link",
|
|
1429
|
+
"unlink",
|
|
1430
|
+
"why",
|
|
1431
|
+
"list",
|
|
1432
|
+
"ls",
|
|
1433
|
+
"outdated",
|
|
1434
|
+
"prune",
|
|
1435
|
+
"dedupe",
|
|
1436
|
+
"import",
|
|
1437
|
+
"rebuild",
|
|
1438
|
+
"root",
|
|
1439
|
+
"config",
|
|
1440
|
+
"cache",
|
|
1441
|
+
"login",
|
|
1442
|
+
"logout",
|
|
1443
|
+
"whoami",
|
|
1444
|
+
"version",
|
|
1445
|
+
"workspace",
|
|
1446
|
+
"workspaces",
|
|
1447
|
+
"store",
|
|
1448
|
+
"fund",
|
|
1449
|
+
"deprecate",
|
|
1450
|
+
"access",
|
|
1451
|
+
"bin",
|
|
1452
|
+
"env",
|
|
1453
|
+
"help"
|
|
1454
|
+
]);
|
|
1455
|
+
var PM_BINARY_RUNNERS = /* @__PURE__ */ new Set(["exec", "dlx", "x"]);
|
|
1456
|
+
var NPX_SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["run-s", "run-p", "npm-run-all"]);
|
|
1079
1457
|
function expandScriptRefs(command, scripts, depth = 2) {
|
|
1080
1458
|
if (depth <= 0) return command;
|
|
1081
1459
|
let expanded = command;
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1460
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1461
|
+
const add = (name) => {
|
|
1462
|
+
if (seen.has(name)) return;
|
|
1463
|
+
seen.add(name);
|
|
1086
1464
|
const body = scripts[name];
|
|
1087
1465
|
if (body) expanded += `
|
|
1088
1466
|
${expandScriptRefs(body, scripts, depth - 1)}`;
|
|
1467
|
+
};
|
|
1468
|
+
for (const m of command.matchAll(/\b(yarn|pnpm|npm|bun|npx)\b([^&|;]*)/g)) {
|
|
1469
|
+
const manager = m[1];
|
|
1470
|
+
const tokens = (m[2] ?? "").split(/\s+/).map((t) => t.replace(/["']/g, "")).filter(Boolean);
|
|
1471
|
+
if (manager === "npx" && !(tokens[0] && NPX_SCRIPT_RUNNERS.has(tokens[0]))) continue;
|
|
1472
|
+
for (const token of tokens) {
|
|
1473
|
+
if (token.startsWith("-")) continue;
|
|
1474
|
+
if (PM_BINARY_RUNNERS.has(token)) break;
|
|
1475
|
+
if (PM_SUBCOMMANDS.has(token) || NPX_SCRIPT_RUNNERS.has(token)) continue;
|
|
1476
|
+
add(token);
|
|
1477
|
+
}
|
|
1089
1478
|
}
|
|
1090
1479
|
if (/npm-run-all|run-s|run-p/.test(command)) {
|
|
1091
|
-
for (const token of command.split(/\s+/))
|
|
1092
|
-
const clean = token.replace(/["']/g, "");
|
|
1093
|
-
const body = scripts[clean];
|
|
1094
|
-
if (body) expanded += `
|
|
1095
|
-
${expandScriptRefs(body, scripts, depth - 1)}`;
|
|
1096
|
-
}
|
|
1480
|
+
for (const token of command.split(/\s+/)) add(token.replace(/["']/g, ""));
|
|
1097
1481
|
}
|
|
1098
1482
|
return expanded;
|
|
1099
1483
|
}
|
|
@@ -1527,6 +1911,16 @@ var gateIntegrityLens = {
|
|
|
1527
1911
|
};
|
|
1528
1912
|
}
|
|
1529
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();
|
|
1530
1924
|
async function listInstructionFiles(root, facts, scope) {
|
|
1531
1925
|
const files = [];
|
|
1532
1926
|
const add = async (rel) => {
|
|
@@ -2035,6 +2429,10 @@ var instructionTruthLens = {
|
|
|
2035
2429
|
};
|
|
2036
2430
|
}
|
|
2037
2431
|
};
|
|
2432
|
+
|
|
2433
|
+
// src/lenses/state-freshness.ts
|
|
2434
|
+
init_config();
|
|
2435
|
+
init_util();
|
|
2038
2436
|
var LENS_ID4 = "state-freshness";
|
|
2039
2437
|
var DECISIONS_FORMAT_MARKER = "<!-- decisions-format: 1 -->";
|
|
2040
2438
|
var MS_PER_DAY = 864e5;
|
|
@@ -2244,6 +2642,9 @@ function rankFindings(findings) {
|
|
|
2244
2642
|
(a, b) => TIER_ORDER[a.tier] - TIER_ORDER[b.tier] || EFFORT_ORDER[a.effort] - EFFORT_ORDER[b.effort]
|
|
2245
2643
|
);
|
|
2246
2644
|
}
|
|
2645
|
+
|
|
2646
|
+
// src/engine/ledger.ts
|
|
2647
|
+
init_util();
|
|
2247
2648
|
function ledgerPath(root) {
|
|
2248
2649
|
return path.join(root, ".etymd", "ledger.json");
|
|
2249
2650
|
}
|
|
@@ -2389,6 +2790,13 @@ async function runAudit(root, opts = {}) {
|
|
|
2389
2790
|
diff
|
|
2390
2791
|
};
|
|
2391
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
|
+
}
|
|
2392
2800
|
function expandTilde(p) {
|
|
2393
2801
|
if (p === "~") return os.homedir();
|
|
2394
2802
|
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
|
|
@@ -2484,7 +2892,10 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
|
|
|
2484
2892
|
private: rec.private === true,
|
|
2485
2893
|
path: asString(rec.path),
|
|
2486
2894
|
upstream: asString(rec.upstream),
|
|
2487
|
-
|
|
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),
|
|
2488
2899
|
staleAfterDays: typeof rec.staleAfterDays === "number" ? rec.staleAfterDays : void 0,
|
|
2489
2900
|
stateBudget: typeof rec.stateBudget === "number" ? rec.stateBudget : void 0,
|
|
2490
2901
|
contract,
|
|
@@ -2601,6 +3012,24 @@ async function loadFleetManifest(manifestPath) {
|
|
|
2601
3012
|
manifest.machineProfile = local.data.machineProfile === "corp" || local.data.machineProfile === "personal" ? local.data.machineProfile : void 0;
|
|
2602
3013
|
const rootRaw = asString(local.data.root) ?? asString(parsed.root);
|
|
2603
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
|
+
}
|
|
2604
3033
|
loadRegistryEntries(parsed.projects, manifest, manifestFile);
|
|
2605
3034
|
} else if (Array.isArray(parsed.sources)) {
|
|
2606
3035
|
manifest.shape = "corpus";
|
|
@@ -2618,6 +3047,11 @@ async function loadFleetManifest(manifestPath) {
|
|
|
2618
3047
|
}
|
|
2619
3048
|
return manifest;
|
|
2620
3049
|
}
|
|
3050
|
+
|
|
3051
|
+
// src/engine/fleet.ts
|
|
3052
|
+
init_config();
|
|
3053
|
+
init_facts();
|
|
3054
|
+
init_util();
|
|
2621
3055
|
var pExecFile2 = promisify(execFile);
|
|
2622
3056
|
var FLEET_LENS = "fleet-manifest";
|
|
2623
3057
|
var FLEET_JSON_SCHEMA = "fleet-experimental-0.2";
|
|
@@ -2743,6 +3177,19 @@ async function checkManifest(manifest) {
|
|
|
2743
3177
|
);
|
|
2744
3178
|
}
|
|
2745
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
|
+
}
|
|
2746
3193
|
if (entry.private && entry.path) {
|
|
2747
3194
|
findings.push(
|
|
2748
3195
|
finding3(
|
|
@@ -2797,6 +3244,18 @@ async function checkManifest(manifest) {
|
|
|
2797
3244
|
}
|
|
2798
3245
|
}
|
|
2799
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
|
+
}
|
|
2800
3259
|
for (const name of Object.keys(manifest.localDirs)) {
|
|
2801
3260
|
if (manifest.shape === "registry" && !seen.has(name)) {
|
|
2802
3261
|
findings.push(
|
|
@@ -3007,6 +3466,52 @@ async function checkCorpEmails(manifest, findings, disclosures) {
|
|
|
3007
3466
|
}
|
|
3008
3467
|
}
|
|
3009
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
|
+
}
|
|
3010
3515
|
async function collectWallFindings(manifest) {
|
|
3011
3516
|
const findings = [];
|
|
3012
3517
|
const disclosures = [];
|
|
@@ -3015,6 +3520,7 @@ async function collectWallFindings(manifest) {
|
|
|
3015
3520
|
await checkManifestRepoMachinePaths(manifest, findings, disclosures);
|
|
3016
3521
|
await checkHygieneNeedles(manifest, findings, disclosures);
|
|
3017
3522
|
await checkCorpEmails(manifest, findings, disclosures);
|
|
3523
|
+
await checkGateDrift(manifest, findings, disclosures);
|
|
3018
3524
|
return { findings, disclosures };
|
|
3019
3525
|
}
|
|
3020
3526
|
function stateAgeDays(facts) {
|
|
@@ -3140,4 +3646,8 @@ async function sweepFleet(manifest, opts = {}) {
|
|
|
3140
3646
|
};
|
|
3141
3647
|
}
|
|
3142
3648
|
|
|
3649
|
+
// src/index.ts
|
|
3650
|
+
init_version();
|
|
3651
|
+
init_version2();
|
|
3652
|
+
|
|
3143
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 };
|