etymd 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +120 -14
- package/README.md +50 -2
- package/dist/{approve-HED3NCTE.js → approve-OUJ4UPGK.js} +5 -5
- package/dist/audit-A75FVXHD.js +12 -0
- package/dist/{brief-2CXAUDNN.js → brief-MYUESUMM.js} +7 -7
- package/dist/{chunk-ULM25GWG.js → chunk-2KVQBL4E.js} +1 -1
- package/dist/{chunk-V2SOHPXF.js → chunk-2VLNI3L2.js} +1 -1
- package/dist/{chunk-OAFYLBVG.js → chunk-3E2IPCRY.js} +3 -3
- package/dist/{chunk-IV3FYVTS.js → chunk-4VPBP6K6.js} +4 -3
- package/dist/{chunk-UN7IAYM6.js → chunk-5GQ52DK6.js} +5 -5
- package/dist/{chunk-D2RI3CCT.js → chunk-DWL2IKZH.js} +12 -3
- package/dist/{chunk-JSUYV26K.js → chunk-F75Q43BC.js} +4 -4
- package/dist/{chunk-EF6BPBG6.js → chunk-GWGKEPRX.js} +2 -2
- package/dist/{chunk-WENIJMVV.js → chunk-HRJJQCMT.js} +54 -18
- package/dist/{chunk-AQM4GYGN.js → chunk-J7XVOTZL.js} +1 -1
- package/dist/{chunk-C7LBUFNU.js → chunk-JHZ2BN4U.js} +3 -3
- package/dist/{chunk-KGVRZ6WC.js → chunk-JVLWM27I.js} +9 -9
- package/dist/{chunk-6DUDIVIC.js → chunk-LSZGCKIQ.js} +1 -1
- package/dist/cli.js +19 -19
- package/dist/config-XAH6PA5G.js +4 -0
- package/dist/{context-5ANOP2IJ.js → context-F63RSIBH.js} +4 -4
- package/dist/doctor-OZQTOUFR.js +19 -0
- package/dist/{fleet-DPJSZDEY.js → fleet-Z7W3QBEV.js} +18 -18
- package/dist/{gates-RR3V4PII.js → gates-47YTMMLZ.js} +10 -10
- package/dist/generate-PMCP37DC.js +6 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +192 -147
- package/dist/{init-RC35RS5J.js → init-QLGIYFCX.js} +10 -10
- package/dist/ledger-4FOZ5HAB.js +5 -0
- package/dist/{scan-MBXSD5RO.js → scan-VWEXWM6G.js} +5 -5
- package/dist/scan-W7RHZVSF.js +5 -0
- package/dist/{screen-EHNK6ANK.js → screen-NSWNC7VQ.js} +125 -16
- package/package.json +4 -4
- package/dist/audit-Y35XVD6Z.js +0 -12
- package/dist/config-7WCVZCM6.js +0 -4
- package/dist/doctor-DXXQJ2L3.js +0 -19
- package/dist/generate-IPY5QIR5.js +0 -6
- package/dist/ledger-B2XGANTH.js +0 -5
- package/dist/scan-KX522VKV.js +0 -5
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { execFile } from '
|
|
2
|
-
import { promises } from '
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import { createHash } from '
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { promises } from 'fs';
|
|
3
|
+
import path2 from 'path';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
6
|
import YAML from 'yaml';
|
|
7
|
-
import os from '
|
|
7
|
+
import os from 'os';
|
|
8
8
|
|
|
9
9
|
var __defProp = Object.defineProperty;
|
|
10
10
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -20,7 +20,7 @@ var __export = (target, all) => {
|
|
|
20
20
|
var PACK_VERSION;
|
|
21
21
|
var init_version = __esm({
|
|
22
22
|
"src/pack/version.ts"() {
|
|
23
|
-
PACK_VERSION = "
|
|
23
|
+
PACK_VERSION = "8";
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -30,7 +30,7 @@ var init_package = __esm({
|
|
|
30
30
|
"package.json"() {
|
|
31
31
|
package_default = {
|
|
32
32
|
name: "etymd",
|
|
33
|
-
version: "0.
|
|
33
|
+
version: "0.11.0",
|
|
34
34
|
description: "Keep your agent instructions true \u2014 verify AGENTS.md, CLAUDE.md, rules & skills against the actual repo, with drift caught over time and a regression ledger.",
|
|
35
35
|
keywords: [
|
|
36
36
|
"cli",
|
|
@@ -99,12 +99,12 @@ var init_package = __esm({
|
|
|
99
99
|
yaml: "2.9.0"
|
|
100
100
|
},
|
|
101
101
|
devDependencies: {
|
|
102
|
-
"@changesets/cli": "2.
|
|
102
|
+
"@changesets/cli": "2.31.1",
|
|
103
103
|
"@types/node": "22.10.2",
|
|
104
104
|
prettier: "3.4.2",
|
|
105
|
-
tsup: "8.
|
|
105
|
+
tsup: "8.5.1",
|
|
106
106
|
typescript: "5.7.2",
|
|
107
|
-
vitest: "
|
|
107
|
+
vitest: "4.1.10"
|
|
108
108
|
},
|
|
109
109
|
publishConfig: {
|
|
110
110
|
access: "public"
|
|
@@ -244,15 +244,15 @@ async function detectPackageManager(root, pkg) {
|
|
|
244
244
|
["bun.lockb", "bun"]
|
|
245
245
|
];
|
|
246
246
|
for (const [file, pm] of lock) {
|
|
247
|
-
if (await pathExists(
|
|
247
|
+
if (await pathExists(path2.join(root, file))) return pm;
|
|
248
248
|
}
|
|
249
249
|
return "unknown";
|
|
250
250
|
}
|
|
251
251
|
async function detectWorkspace(root, pkg) {
|
|
252
|
-
if (await pathExists(
|
|
253
|
-
if (await pathExists(
|
|
254
|
-
if (await pathExists(
|
|
255
|
-
const pnpmWs = await readText(
|
|
252
|
+
if (await pathExists(path2.join(root, "nx.json"))) return { kind: "nx", packageGlobs: [] };
|
|
253
|
+
if (await pathExists(path2.join(root, "turbo.json"))) return { kind: "turbo", packageGlobs: [] };
|
|
254
|
+
if (await pathExists(path2.join(root, "lerna.json"))) return { kind: "lerna", packageGlobs: [] };
|
|
255
|
+
const pnpmWs = await readText(path2.join(root, "pnpm-workspace.yaml"));
|
|
256
256
|
if (pnpmWs) {
|
|
257
257
|
const globs = pnpmWs.split("\n").map((l) => l.match(/^\s*-\s*["']?([^"'#]+)["']?/)?.[1]?.trim()).filter((g) => Boolean(g));
|
|
258
258
|
return { kind: "pnpm", packageGlobs: globs };
|
|
@@ -270,21 +270,21 @@ async function listWorkspacePackages(root, globs) {
|
|
|
270
270
|
const found = [];
|
|
271
271
|
const seen = /* @__PURE__ */ new Set();
|
|
272
272
|
const addFromDir = async (dir) => {
|
|
273
|
-
const pkgPath =
|
|
273
|
+
const pkgPath = path2.join(dir, "package.json");
|
|
274
274
|
if (seen.has(pkgPath)) return;
|
|
275
275
|
const pkg = await readJson(pkgPath);
|
|
276
276
|
if (!pkg?.name) return;
|
|
277
277
|
seen.add(pkgPath);
|
|
278
278
|
found.push({
|
|
279
279
|
name: pkg.name,
|
|
280
|
-
dir:
|
|
280
|
+
dir: path2.relative(root, dir) || ".",
|
|
281
281
|
private: Boolean(pkg.private),
|
|
282
282
|
version: pkg.version
|
|
283
283
|
});
|
|
284
284
|
};
|
|
285
285
|
for (const glob of globs) {
|
|
286
286
|
const base = glob.replace(/\/\*\*?$/, "");
|
|
287
|
-
const parent =
|
|
287
|
+
const parent = path2.join(root, base);
|
|
288
288
|
if (!await isDirectory(parent)) continue;
|
|
289
289
|
let entries;
|
|
290
290
|
try {
|
|
@@ -294,7 +294,7 @@ async function listWorkspacePackages(root, globs) {
|
|
|
294
294
|
}
|
|
295
295
|
for (const entry of entries) {
|
|
296
296
|
if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) continue;
|
|
297
|
-
const child =
|
|
297
|
+
const child = path2.join(parent, entry);
|
|
298
298
|
if (await isDirectory(child)) await addFromDir(child);
|
|
299
299
|
}
|
|
300
300
|
}
|
|
@@ -307,7 +307,7 @@ async function expandFileGlobs(root, globs, cap = 500) {
|
|
|
307
307
|
if (out.size >= cap) return;
|
|
308
308
|
let entries;
|
|
309
309
|
try {
|
|
310
|
-
entries = await promises.readdir(
|
|
310
|
+
entries = await promises.readdir(path2.join(root, rel), { withFileTypes: true });
|
|
311
311
|
} catch {
|
|
312
312
|
return;
|
|
313
313
|
}
|
|
@@ -334,7 +334,7 @@ async function expandFileGlobs(root, globs, cap = 500) {
|
|
|
334
334
|
(r) => ![...roots].some((other) => other !== r && (other === "" || r.startsWith(other + "/")))
|
|
335
335
|
);
|
|
336
336
|
for (const r of minimal) {
|
|
337
|
-
if (r && !await isDirectory(
|
|
337
|
+
if (r && !await isDirectory(path2.join(root, r))) continue;
|
|
338
338
|
await walk(r);
|
|
339
339
|
}
|
|
340
340
|
return [...out].sort();
|
|
@@ -363,13 +363,13 @@ function detectFrameworks(pkg) {
|
|
|
363
363
|
return out;
|
|
364
364
|
}
|
|
365
365
|
async function detectCi(root) {
|
|
366
|
-
if (await pathExists(
|
|
366
|
+
if (await pathExists(path2.join(root, ".gitlab-ci.yml"))) {
|
|
367
367
|
return { system: "gitlab", files: [".gitlab-ci.yml"] };
|
|
368
368
|
}
|
|
369
|
-
const workflows =
|
|
369
|
+
const workflows = path2.join(root, ".github", "workflows");
|
|
370
370
|
if (await isDirectory(workflows)) {
|
|
371
371
|
try {
|
|
372
|
-
const files = (await promises.readdir(workflows)).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")).map((f) =>
|
|
372
|
+
const files = (await promises.readdir(workflows)).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")).map((f) => path2.join(".github/workflows", f));
|
|
373
373
|
if (files.length) return { system: "github", files };
|
|
374
374
|
} catch {
|
|
375
375
|
}
|
|
@@ -384,7 +384,7 @@ async function hasLintStagedConfig(root, pkg) {
|
|
|
384
384
|
".lintstagedrc.js",
|
|
385
385
|
"lint-staged.config.js"
|
|
386
386
|
]) {
|
|
387
|
-
if (await pathExists(
|
|
387
|
+
if (await pathExists(path2.join(root, f))) return true;
|
|
388
388
|
}
|
|
389
389
|
return false;
|
|
390
390
|
}
|
|
@@ -392,50 +392,50 @@ async function detectHooks(root, hooksPath, pkg) {
|
|
|
392
392
|
const lintStaged = await hasLintStagedConfig(root, pkg);
|
|
393
393
|
if (hooksPath) {
|
|
394
394
|
if (hooksPath.replace(/\/+$/, "") === ".husky/_") {
|
|
395
|
-
const base2 =
|
|
395
|
+
const base2 = path2.join(root, ".husky");
|
|
396
396
|
return {
|
|
397
397
|
source: "husky",
|
|
398
398
|
dir: ".husky",
|
|
399
|
-
preCommit: await pathExists(
|
|
400
|
-
prePush: await pathExists(
|
|
401
|
-
commitMsg: await pathExists(
|
|
399
|
+
preCommit: await pathExists(path2.join(base2, "pre-commit")),
|
|
400
|
+
prePush: await pathExists(path2.join(base2, "pre-push")),
|
|
401
|
+
commitMsg: await pathExists(path2.join(base2, "commit-msg")),
|
|
402
402
|
lintStaged
|
|
403
403
|
};
|
|
404
404
|
}
|
|
405
|
-
const base =
|
|
405
|
+
const base = path2.join(root, hooksPath);
|
|
406
406
|
return {
|
|
407
407
|
source: hooksPath === ".githooks" ? "githooks" : "custom",
|
|
408
408
|
dir: hooksPath,
|
|
409
|
-
preCommit: await pathExists(
|
|
410
|
-
prePush: await pathExists(
|
|
411
|
-
commitMsg: await pathExists(
|
|
409
|
+
preCommit: await pathExists(path2.join(base, "pre-commit")),
|
|
410
|
+
prePush: await pathExists(path2.join(base, "pre-push")),
|
|
411
|
+
commitMsg: await pathExists(path2.join(base, "commit-msg")),
|
|
412
412
|
lintStaged
|
|
413
413
|
};
|
|
414
414
|
}
|
|
415
|
-
if (await isDirectory(
|
|
416
|
-
const base =
|
|
415
|
+
if (await isDirectory(path2.join(root, ".githooks"))) {
|
|
416
|
+
const base = path2.join(root, ".githooks");
|
|
417
417
|
return {
|
|
418
418
|
source: "githooks",
|
|
419
419
|
dir: ".githooks",
|
|
420
|
-
preCommit: await pathExists(
|
|
421
|
-
prePush: await pathExists(
|
|
422
|
-
commitMsg: await pathExists(
|
|
420
|
+
preCommit: await pathExists(path2.join(base, "pre-commit")),
|
|
421
|
+
prePush: await pathExists(path2.join(base, "pre-push")),
|
|
422
|
+
commitMsg: await pathExists(path2.join(base, "commit-msg")),
|
|
423
423
|
lintStaged
|
|
424
424
|
};
|
|
425
425
|
}
|
|
426
|
-
if (await isDirectory(
|
|
427
|
-
const base =
|
|
426
|
+
if (await isDirectory(path2.join(root, ".husky"))) {
|
|
427
|
+
const base = path2.join(root, ".husky");
|
|
428
428
|
return {
|
|
429
429
|
source: "husky",
|
|
430
430
|
dir: ".husky",
|
|
431
|
-
preCommit: await pathExists(
|
|
432
|
-
prePush: await pathExists(
|
|
433
|
-
commitMsg: await pathExists(
|
|
431
|
+
preCommit: await pathExists(path2.join(base, "pre-commit")),
|
|
432
|
+
prePush: await pathExists(path2.join(base, "pre-push")),
|
|
433
|
+
commitMsg: await pathExists(path2.join(base, "commit-msg")),
|
|
434
434
|
lintStaged
|
|
435
435
|
};
|
|
436
436
|
}
|
|
437
437
|
const legacyHooks = pkg?.husky?.hooks;
|
|
438
|
-
const configText = legacyHooks ? null : await readText(
|
|
438
|
+
const configText = legacyHooks ? null : await readText(path2.join(root, "husky.config.js"));
|
|
439
439
|
if (legacyHooks || configText) {
|
|
440
440
|
const hookNamed = (name) => legacyHooks ? Boolean(legacyHooks[name]) : Boolean(configText && configText.includes(name));
|
|
441
441
|
return {
|
|
@@ -466,10 +466,10 @@ async function detectShellSurface(root, isRepo) {
|
|
|
466
466
|
if (!listed) return { scripts: 0 };
|
|
467
467
|
const files = listed.split("\0").filter(Boolean);
|
|
468
468
|
const byExtension = files.filter((f) => SHELL_EXT.test(f));
|
|
469
|
-
const candidates = files.filter((f) => !
|
|
469
|
+
const candidates = files.filter((f) => !path2.basename(f).includes("."));
|
|
470
470
|
const SHEBANG_PROBE_CAP = 400;
|
|
471
471
|
const probed = await Promise.all(
|
|
472
|
-
candidates.slice(0, SHEBANG_PROBE_CAP).map(async (f) => SHELL_SHEBANG.test(await firstLine(
|
|
472
|
+
candidates.slice(0, SHEBANG_PROBE_CAP).map(async (f) => SHELL_SHEBANG.test(await firstLine(path2.join(root, f))))
|
|
473
473
|
);
|
|
474
474
|
return { scripts: byExtension.length + probed.filter(Boolean).length };
|
|
475
475
|
}
|
|
@@ -477,12 +477,12 @@ async function detectArtifacts(root) {
|
|
|
477
477
|
const artifacts = await Promise.all(
|
|
478
478
|
ARTIFACT_SPECS.map(async (spec) => ({
|
|
479
479
|
...spec,
|
|
480
|
-
exists: await pathExists(
|
|
480
|
+
exists: await pathExists(path2.join(root, spec.path))
|
|
481
481
|
}))
|
|
482
482
|
);
|
|
483
483
|
let adrFiles = false;
|
|
484
484
|
try {
|
|
485
|
-
adrFiles = (await promises.readdir(
|
|
485
|
+
adrFiles = (await promises.readdir(path2.join(root, "docs"))).some((f) => /^\d{4}-.+\.md$/.test(f));
|
|
486
486
|
} catch {
|
|
487
487
|
}
|
|
488
488
|
artifacts.push({
|
|
@@ -511,7 +511,7 @@ async function walkTree(root) {
|
|
|
511
511
|
for (const entry of entries) {
|
|
512
512
|
if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) continue;
|
|
513
513
|
if (entry.isDirectory()) {
|
|
514
|
-
total += await countFiles(
|
|
514
|
+
total += await countFiles(path2.join(dir, entry.name));
|
|
515
515
|
} else if (entry.isFile()) {
|
|
516
516
|
total += 1;
|
|
517
517
|
state.budget -= 1;
|
|
@@ -534,7 +534,7 @@ async function walkTree(root) {
|
|
|
534
534
|
if (!entry.isDirectory()) continue;
|
|
535
535
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
536
536
|
if (entry.name.startsWith(".") && !MEANINGFUL_DOT_DIRS.has(entry.name)) continue;
|
|
537
|
-
dirs.push({ name: entry.name, files: await countFiles(
|
|
537
|
+
dirs.push({ name: entry.name, files: await countFiles(path2.join(root, entry.name)) });
|
|
538
538
|
}
|
|
539
539
|
dirs.sort((a, b) => b.files - a.files);
|
|
540
540
|
return { dirs, truncated: state.truncated };
|
|
@@ -782,8 +782,8 @@ async function collectFreshness(abs, isRepo, artifacts, upstreamRemote) {
|
|
|
782
782
|
return facts;
|
|
783
783
|
}
|
|
784
784
|
async function scanProject(root, opts = {}) {
|
|
785
|
-
const abs =
|
|
786
|
-
const rootPkg = await readJson(
|
|
785
|
+
const abs = path2.resolve(root);
|
|
786
|
+
const rootPkg = await readJson(path2.join(abs, "package.json"));
|
|
787
787
|
const [packageManager, workspace, ci, artifacts, tree, isRepo] = await Promise.all([
|
|
788
788
|
detectPackageManager(abs, rootPkg),
|
|
789
789
|
detectWorkspace(abs, rootPkg),
|
|
@@ -810,7 +810,7 @@ async function scanProject(root, opts = {}) {
|
|
|
810
810
|
packVersion: PACK_VERSION,
|
|
811
811
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
812
812
|
root: abs,
|
|
813
|
-
name: rootPkg?.name ??
|
|
813
|
+
name: rootPkg?.name ?? path2.basename(abs),
|
|
814
814
|
git: {
|
|
815
815
|
isRepo,
|
|
816
816
|
branch: branch ?? void 0,
|
|
@@ -847,13 +847,13 @@ var init_scan = __esm({
|
|
|
847
847
|
}
|
|
848
848
|
});
|
|
849
849
|
function cacheFactsPath(root) {
|
|
850
|
-
return
|
|
850
|
+
return path2.join(root, CACHE_FACTS_FILE);
|
|
851
851
|
}
|
|
852
852
|
function baselinePath(root) {
|
|
853
|
-
return
|
|
853
|
+
return path2.join(root, BASELINE_FILE);
|
|
854
854
|
}
|
|
855
855
|
async function writeJsonFile(target, data) {
|
|
856
|
-
await promises.mkdir(
|
|
856
|
+
await promises.mkdir(path2.dirname(target), { recursive: true });
|
|
857
857
|
await promises.writeFile(target, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
858
858
|
return target;
|
|
859
859
|
}
|
|
@@ -887,9 +887,9 @@ var init_facts = __esm({
|
|
|
887
887
|
"src/core/facts.ts"() {
|
|
888
888
|
init_util();
|
|
889
889
|
ETYMD_DIR = ".etymd";
|
|
890
|
-
CACHE_DIR =
|
|
891
|
-
CACHE_FACTS_FILE =
|
|
892
|
-
BASELINE_FILE =
|
|
890
|
+
CACHE_DIR = path2.join(ETYMD_DIR, "cache");
|
|
891
|
+
CACHE_FACTS_FILE = path2.join(CACHE_DIR, "facts.json");
|
|
892
|
+
BASELINE_FILE = path2.join(ETYMD_DIR, "baseline.json");
|
|
893
893
|
}
|
|
894
894
|
});
|
|
895
895
|
|
|
@@ -902,7 +902,7 @@ __export(config_exports, {
|
|
|
902
902
|
readConfig: () => readConfig
|
|
903
903
|
});
|
|
904
904
|
function configPath(root) {
|
|
905
|
-
return
|
|
905
|
+
return path2.join(root, CONFIG_FILE);
|
|
906
906
|
}
|
|
907
907
|
function readGlobList(value, key, problems) {
|
|
908
908
|
if (value === void 0) return void 0;
|
|
@@ -961,6 +961,14 @@ async function readConfig(root) {
|
|
|
961
961
|
}
|
|
962
962
|
return gates.publishGate;
|
|
963
963
|
})();
|
|
964
|
+
const commitFormat = (() => {
|
|
965
|
+
if (gates.commitFormat === void 0) return void 0;
|
|
966
|
+
if (typeof gates.commitFormat !== "boolean") {
|
|
967
|
+
problems.push(`${CONFIG_FILE}: \`gates.commitFormat\` must be true or false \u2014 ignored.`);
|
|
968
|
+
return void 0;
|
|
969
|
+
}
|
|
970
|
+
return gates.commitFormat;
|
|
971
|
+
})();
|
|
964
972
|
return {
|
|
965
973
|
present: true,
|
|
966
974
|
problems,
|
|
@@ -982,6 +990,7 @@ async function readConfig(root) {
|
|
|
982
990
|
commands: readGlobList(gates.commands, "gates.commands", problems) ?? DEFAULT_CONFIG.gates.commands,
|
|
983
991
|
failOn: failOn ?? DEFAULT_CONFIG.gates.failOn,
|
|
984
992
|
publishGate,
|
|
993
|
+
commitFormat,
|
|
985
994
|
allowWriting: readGlobList(gates.allowWriting, "gates.allowWriting", problems) ?? DEFAULT_CONFIG.gates.allowWriting
|
|
986
995
|
}
|
|
987
996
|
}
|
|
@@ -992,7 +1001,7 @@ var init_config = __esm({
|
|
|
992
1001
|
"src/core/config.ts"() {
|
|
993
1002
|
init_facts();
|
|
994
1003
|
init_util();
|
|
995
|
-
CONFIG_FILE =
|
|
1004
|
+
CONFIG_FILE = path2.join(ETYMD_DIR, "config.json");
|
|
996
1005
|
DEFAULT_CONFIG = {
|
|
997
1006
|
instructions: { include: [], exclude: [] },
|
|
998
1007
|
context: { perFileWords: 4e3, totalWords: 8e3 },
|
|
@@ -1155,9 +1164,34 @@ fi
|
|
|
1155
1164
|
exit 0
|
|
1156
1165
|
`);
|
|
1157
1166
|
}
|
|
1158
|
-
function
|
|
1167
|
+
function commitFormatStep() {
|
|
1168
|
+
const types = COMMIT_TYPES.join("|");
|
|
1169
|
+
return `
|
|
1170
|
+
# Message format \u2014 <type>[(scope)][!]: <summary>. Needs nothing installed, so it always runs.
|
|
1171
|
+
subject=$(sed -e '/^#/d' -e '/^[[:space:]]*$/d' "$1" | head -1)
|
|
1172
|
+
case "$subject" in
|
|
1173
|
+
"Merge "*|"Revert "*|fixup!*|squash!*|amend!*) ;;
|
|
1174
|
+
*)
|
|
1175
|
+
if ! printf '%s' "$subject" | grep -qE '^(${types})(\\([a-z0-9._/-]+\\))?!?: .+'; then
|
|
1176
|
+
echo "\u2717 commit message: expected '<type>[(scope)][!]: <summary>'"
|
|
1177
|
+
echo " got: $subject"
|
|
1178
|
+
echo " types: ${COMMIT_TYPES.join(" ")}"
|
|
1179
|
+
echo " a '!' after the type or scope marks a breaking change"
|
|
1180
|
+
exit 1
|
|
1181
|
+
fi
|
|
1182
|
+
# Length is advice, not a block: the format is what tooling reads, the length is what a
|
|
1183
|
+
# person reads, and only one of the two can break anything.
|
|
1184
|
+
if [ "\${#subject}" -gt ${SUBJECT_ADVISORY_LENGTH} ]; then
|
|
1185
|
+
echo "\u203A note: subject is \${#subject} characters; ${SUBJECT_ADVISORY_LENGTH} or fewer reads better in git log"
|
|
1186
|
+
fi
|
|
1187
|
+
;;
|
|
1188
|
+
esac`;
|
|
1189
|
+
}
|
|
1190
|
+
function generateCommitMsgHook(gates) {
|
|
1191
|
+
const format = gates?.commitFormat === true ? `${commitFormatStep()}
|
|
1192
|
+
` : "";
|
|
1159
1193
|
return stampGenerated(`#!/usr/bin/env sh
|
|
1160
|
-
# etymd:
|
|
1194
|
+
# etymd: the commit message itself \u2014 content screen, then format.
|
|
1161
1195
|
#
|
|
1162
1196
|
# The staged-content gate reads file bytes and never sees the message, yet a message is as
|
|
1163
1197
|
# permanently published as any file. No-op where no checker is installed.
|
|
@@ -1167,7 +1201,7 @@ GATE="\${COMMIT_MSG_GATE:-$(command -v etymd || true)}"
|
|
|
1167
1201
|
if [ -x "$GATE" ]; then
|
|
1168
1202
|
"$GATE" screen --message "$1" || exit 1
|
|
1169
1203
|
fi
|
|
1170
|
-
|
|
1204
|
+
${format}
|
|
1171
1205
|
${localHookCall("commit-msg")}
|
|
1172
1206
|
|
|
1173
1207
|
exit 0
|
|
@@ -1235,7 +1269,7 @@ ${auditStep}
|
|
|
1235
1269
|
# with --no-verify and anything a rebase or merge brought in from elsewhere. Advisory here (it
|
|
1236
1270
|
# never blocks the push): the blocking decision belongs at commit time, where the fix is cheap.
|
|
1237
1271
|
${CONTENT_GATE_RESOLUTION}
|
|
1238
|
-
if [ -x "$GATE" ]
|
|
1272
|
+
if [ -x "$GATE" ]; then
|
|
1239
1273
|
"$GATE" screen --tree --advisory || true
|
|
1240
1274
|
fi
|
|
1241
1275
|
|
|
@@ -1249,14 +1283,10 @@ function generateArtifactCheckScript() {
|
|
|
1249
1283
|
# Wire it into the irreversible moment:
|
|
1250
1284
|
# package.json \u2192 "prepublishOnly": "./scripts/artifact-check.sh"
|
|
1251
1285
|
#
|
|
1252
|
-
#
|
|
1286
|
+
# The artifact gate is the one check that sees what actually SHIPS \u2014 bypass with
|
|
1287
|
+
# .etymd-screen-allow entries (with provenance) if you must exempt a string.
|
|
1253
1288
|
set -eu
|
|
1254
1289
|
|
|
1255
|
-
if [ "\${ARTIFACT_CHECK_SKIP:-0}" = "1" ]; then
|
|
1256
|
-
echo "\u203A artifact-check: SKIPPED by ARTIFACT_CHECK_SKIP=1"
|
|
1257
|
-
exit 0
|
|
1258
|
-
fi
|
|
1259
|
-
|
|
1260
1290
|
${CONTENT_GATE_RESOLUTION}
|
|
1261
1291
|
[ -x "$GATE" ] || { echo "\u203A artifact-check: no checker installed \u2014 skipping."; exit 0; }
|
|
1262
1292
|
|
|
@@ -1274,12 +1304,26 @@ fi
|
|
|
1274
1304
|
exit 0
|
|
1275
1305
|
`);
|
|
1276
1306
|
}
|
|
1277
|
-
var GENERATION_MARKER_RE, CONTENT_GATE_RESOLUTION;
|
|
1307
|
+
var GENERATION_MARKER_RE, CONTENT_GATE_RESOLUTION, COMMIT_TYPES, SUBJECT_ADVISORY_LENGTH;
|
|
1278
1308
|
var init_templates = __esm({
|
|
1279
1309
|
"src/pack/templates.ts"() {
|
|
1280
1310
|
init_version();
|
|
1281
1311
|
GENERATION_MARKER_RE = /^(?:# |<!-- )etymd:generated pack-v\S+ ([0-9a-f]{16})(?: -->)?$/;
|
|
1282
|
-
CONTENT_GATE_RESOLUTION = `GATE="\${CONTENT_GATE:-$(command -v etymd || true)}"`;
|
|
1312
|
+
CONTENT_GATE_RESOLUTION = `GATE="\${CONTENT_GATE:-$(if [ -x ./dist/cli.js ]; then echo ./dist/cli.js; else command -v etymd || true; fi)}"`;
|
|
1313
|
+
COMMIT_TYPES = [
|
|
1314
|
+
"feat",
|
|
1315
|
+
"fix",
|
|
1316
|
+
"docs",
|
|
1317
|
+
"style",
|
|
1318
|
+
"refactor",
|
|
1319
|
+
"perf",
|
|
1320
|
+
"test",
|
|
1321
|
+
"build",
|
|
1322
|
+
"ci",
|
|
1323
|
+
"chore",
|
|
1324
|
+
"revert"
|
|
1325
|
+
];
|
|
1326
|
+
SUBJECT_ADVISORY_LENGTH = 72;
|
|
1283
1327
|
}
|
|
1284
1328
|
});
|
|
1285
1329
|
|
|
@@ -1322,7 +1366,7 @@ function deriveFailOn(facts, recorded) {
|
|
|
1322
1366
|
async function planWorkflow(root, facts, opts) {
|
|
1323
1367
|
const out = [];
|
|
1324
1368
|
const add = async (rel, contents, label, executable = false) => {
|
|
1325
|
-
const abs =
|
|
1369
|
+
const abs = path2.join(root, rel);
|
|
1326
1370
|
const exists = await pathExists(abs);
|
|
1327
1371
|
const existing = exists ? await readText(abs) : null;
|
|
1328
1372
|
const differs = exists ? existing !== contents : void 0;
|
|
@@ -1341,18 +1385,19 @@ async function planWorkflow(root, facts, opts) {
|
|
|
1341
1385
|
await add("AGENTS.md", generateAgentsMd(facts), "Minimal operating contract (scaffold)");
|
|
1342
1386
|
}
|
|
1343
1387
|
if (opts.gates) {
|
|
1344
|
-
const existingPrePush = await readText(
|
|
1388
|
+
const existingPrePush = await readText(path2.join(root, ".githooks", "pre-push"));
|
|
1345
1389
|
await add(".githooks/pre-commit", generatePreCommitHook(), "Process gate (pre-commit)", true);
|
|
1346
1390
|
await add(
|
|
1347
1391
|
".githooks/commit-msg",
|
|
1348
|
-
generateCommitMsgHook(),
|
|
1349
|
-
"
|
|
1392
|
+
generateCommitMsgHook(opts.gateConfig),
|
|
1393
|
+
"Message gate (content screen, format)",
|
|
1350
1394
|
true
|
|
1351
1395
|
);
|
|
1352
1396
|
const gateConfig = opts.gateConfig?.commands?.length ? opts.gateConfig : {
|
|
1353
1397
|
commands: derivedCommands(facts, existingPrePush ?? void 0),
|
|
1354
1398
|
failOn: opts.gateConfig?.failOn ?? DEFAULT_CONFIG.gates.failOn,
|
|
1355
1399
|
publishGate: opts.gateConfig?.publishGate,
|
|
1400
|
+
commitFormat: opts.gateConfig?.commitFormat,
|
|
1356
1401
|
allowWriting: opts.gateConfig?.allowWriting ?? []
|
|
1357
1402
|
};
|
|
1358
1403
|
await add(
|
|
@@ -1404,18 +1449,18 @@ function isAlwaysAppliedCursorRule(text) {
|
|
|
1404
1449
|
async function measureContext(root, perFileWords = EXTRACTION_THRESHOLD) {
|
|
1405
1450
|
const files = [];
|
|
1406
1451
|
for (const spec of ALWAYS_LOADED) {
|
|
1407
|
-
const abs =
|
|
1452
|
+
const abs = path2.join(root, spec.path);
|
|
1408
1453
|
const text = await readText(abs);
|
|
1409
1454
|
if (text === null) continue;
|
|
1410
1455
|
const words = wordCount(text);
|
|
1411
1456
|
files.push({ path: spec.path, role: spec.role, words, approxTokens: approxTokens(words) });
|
|
1412
1457
|
}
|
|
1413
|
-
const rulesDir =
|
|
1458
|
+
const rulesDir = path2.join(root, ".cursor", "rules");
|
|
1414
1459
|
if (await pathExists(rulesDir)) {
|
|
1415
1460
|
try {
|
|
1416
1461
|
for (const entry of await promises.readdir(rulesDir)) {
|
|
1417
1462
|
if (!entry.endsWith(".mdc") && !entry.endsWith(".md")) continue;
|
|
1418
|
-
const text = await readText(
|
|
1463
|
+
const text = await readText(path2.join(rulesDir, entry));
|
|
1419
1464
|
if (text === null || !isAlwaysAppliedCursorRule(text)) continue;
|
|
1420
1465
|
const words = wordCount(text);
|
|
1421
1466
|
files.push({
|
|
@@ -1448,8 +1493,8 @@ async function applyFiles(root, files, overwrite = /* @__PURE__ */ new Set()) {
|
|
|
1448
1493
|
skipped.push(file.path);
|
|
1449
1494
|
continue;
|
|
1450
1495
|
}
|
|
1451
|
-
const abs =
|
|
1452
|
-
await promises.mkdir(
|
|
1496
|
+
const abs = path2.join(root, file.path);
|
|
1497
|
+
await promises.mkdir(path2.dirname(abs), { recursive: true });
|
|
1453
1498
|
await promises.writeFile(abs, file.contents, "utf8");
|
|
1454
1499
|
if (file.executable) await promises.chmod(abs, 493);
|
|
1455
1500
|
written.push(file.path);
|
|
@@ -1750,14 +1795,14 @@ function parseGithubWorkflow(text, file, scripts) {
|
|
|
1750
1795
|
return { jobs, parseErrors };
|
|
1751
1796
|
}
|
|
1752
1797
|
async function companionOf(root, hooksDir, hook, hookText, scripts) {
|
|
1753
|
-
const rel =
|
|
1798
|
+
const rel = path2.posix.join(hooksDir.split(path2.sep).join("/"), `${hook}.local`);
|
|
1754
1799
|
if (!hookText.includes(`${hook}.local`)) return null;
|
|
1755
|
-
const abs =
|
|
1800
|
+
const abs = path2.join(root, hooksDir, `${hook}.local`);
|
|
1756
1801
|
const text = await readText(abs);
|
|
1757
1802
|
if (text === null) return null;
|
|
1758
1803
|
const [executable, hookExecutable] = await Promise.all([
|
|
1759
1804
|
isExecutable(abs),
|
|
1760
|
-
isExecutable(
|
|
1805
|
+
isExecutable(path2.join(root, hooksDir, hook))
|
|
1761
1806
|
]);
|
|
1762
1807
|
if (executable === false && hookExecutable === true) return { rel, tools: [], state: "inert" };
|
|
1763
1808
|
return {
|
|
@@ -1774,7 +1819,7 @@ async function localHookTools(root, facts, scripts) {
|
|
|
1774
1819
|
const inertCompanions = [];
|
|
1775
1820
|
const readHook = async (name) => {
|
|
1776
1821
|
if (!hooks.dir) return empty;
|
|
1777
|
-
const text = await readText(
|
|
1822
|
+
const text = await readText(path2.join(root, hooks.dir, name));
|
|
1778
1823
|
if (!text) return empty;
|
|
1779
1824
|
const tools = new Set(matchTools(text, scripts));
|
|
1780
1825
|
const companion = await companionOf(root, hooks.dir, name, text, scripts);
|
|
@@ -1798,10 +1843,10 @@ async function localHookTools(root, facts, scripts) {
|
|
|
1798
1843
|
]);
|
|
1799
1844
|
} else if (hooks.source === "husky-legacy") {
|
|
1800
1845
|
const pkg = await readJson(
|
|
1801
|
-
|
|
1846
|
+
path2.join(root, "package.json")
|
|
1802
1847
|
);
|
|
1803
1848
|
const legacy = pkg?.husky?.hooks ?? {};
|
|
1804
|
-
const configText = await readText(
|
|
1849
|
+
const configText = await readText(path2.join(root, "husky.config.js"));
|
|
1805
1850
|
const forHook = (name) => {
|
|
1806
1851
|
const cmd = legacy[name] ?? (configText?.includes(name) ? configText : void 0);
|
|
1807
1852
|
return cmd ? matchTools(cmd, scripts) : [];
|
|
@@ -1812,7 +1857,7 @@ async function localHookTools(root, facts, scripts) {
|
|
|
1812
1857
|
}
|
|
1813
1858
|
let lintStaged = [];
|
|
1814
1859
|
if (hooks.lintStaged) {
|
|
1815
|
-
const pkg = await readJson(
|
|
1860
|
+
const pkg = await readJson(path2.join(root, "package.json"));
|
|
1816
1861
|
const config = pkg?.["lint-staged"];
|
|
1817
1862
|
const commands = [];
|
|
1818
1863
|
const collect = (v) => {
|
|
@@ -1842,7 +1887,7 @@ async function localHookTools(root, facts, scripts) {
|
|
|
1842
1887
|
};
|
|
1843
1888
|
}
|
|
1844
1889
|
async function detectThresholds(root, scripts, ciJobs) {
|
|
1845
|
-
const sonarConfigured = await pathExists(
|
|
1890
|
+
const sonarConfigured = await pathExists(path2.join(root, "sonar-project.properties")) || ciJobs.some((j) => j.tools.includes("sonar"));
|
|
1846
1891
|
let coverageThresholdLocal = false;
|
|
1847
1892
|
for (const f of [
|
|
1848
1893
|
"jest.config.js",
|
|
@@ -1854,7 +1899,7 @@ async function detectThresholds(root, scripts, ciJobs) {
|
|
|
1854
1899
|
"vitest.config.js",
|
|
1855
1900
|
"vitest.config.mts"
|
|
1856
1901
|
]) {
|
|
1857
|
-
const text = await readText(
|
|
1902
|
+
const text = await readText(path2.join(root, f));
|
|
1858
1903
|
if (text && /coverageThreshold|thresholds\s*:/.test(text)) {
|
|
1859
1904
|
coverageThresholdLocal = true;
|
|
1860
1905
|
break;
|
|
@@ -1862,7 +1907,7 @@ async function detectThresholds(root, scripts, ciJobs) {
|
|
|
1862
1907
|
}
|
|
1863
1908
|
if (!coverageThresholdLocal) {
|
|
1864
1909
|
const pkg = await readJson(
|
|
1865
|
-
|
|
1910
|
+
path2.join(root, "package.json")
|
|
1866
1911
|
);
|
|
1867
1912
|
coverageThresholdLocal = Boolean(pkg?.jest?.coverageThreshold);
|
|
1868
1913
|
}
|
|
@@ -1875,7 +1920,7 @@ async function buildGateInventory(root, facts) {
|
|
|
1875
1920
|
const inheritedIncludes = [];
|
|
1876
1921
|
const parseErrors = [];
|
|
1877
1922
|
if (facts.ci.system === "gitlab") {
|
|
1878
|
-
const text = await readText(
|
|
1923
|
+
const text = await readText(path2.join(root, ".gitlab-ci.yml"));
|
|
1879
1924
|
if (text) {
|
|
1880
1925
|
const parsed = parseGitlabCi(text, ".gitlab-ci.yml", scripts);
|
|
1881
1926
|
jobs.push(...parsed.jobs);
|
|
@@ -1883,7 +1928,7 @@ async function buildGateInventory(root, facts) {
|
|
|
1883
1928
|
for (const inc of parsed.inheritedIncludes) {
|
|
1884
1929
|
if (inc.startsWith("local: ")) {
|
|
1885
1930
|
const rel = inc.slice("local: ".length).replace(/^\//, "");
|
|
1886
|
-
const subText = await readText(
|
|
1931
|
+
const subText = await readText(path2.join(root, rel));
|
|
1887
1932
|
if (subText) {
|
|
1888
1933
|
const sub = parseGitlabCi(subText, rel, scripts);
|
|
1889
1934
|
jobs.push(...sub.jobs);
|
|
@@ -1899,7 +1944,7 @@ async function buildGateInventory(root, facts) {
|
|
|
1899
1944
|
}
|
|
1900
1945
|
} else if (facts.ci.system === "github") {
|
|
1901
1946
|
for (const file of facts.ci.files) {
|
|
1902
|
-
const text = await readText(
|
|
1947
|
+
const text = await readText(path2.join(root, file));
|
|
1903
1948
|
if (!text) continue;
|
|
1904
1949
|
const parsed = parseGithubWorkflow(text, file, scripts);
|
|
1905
1950
|
jobs.push(...parsed.jobs);
|
|
@@ -1910,7 +1955,7 @@ async function buildGateInventory(root, facts) {
|
|
|
1910
1955
|
localHookTools(root, facts, scripts),
|
|
1911
1956
|
detectThresholds(root, scripts, jobs)
|
|
1912
1957
|
]);
|
|
1913
|
-
const pkg = await readJson(
|
|
1958
|
+
const pkg = await readJson(path2.join(root, "package.json"));
|
|
1914
1959
|
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
1915
1960
|
const commitlintDep = Boolean(deps["@commitlint/cli"] ?? deps["commitlint"]);
|
|
1916
1961
|
return {
|
|
@@ -2332,7 +2377,7 @@ var stateFreshnessLens = {
|
|
|
2332
2377
|
}
|
|
2333
2378
|
}
|
|
2334
2379
|
for (const a of stateArtifacts) {
|
|
2335
|
-
const text = await readText(
|
|
2380
|
+
const text = await readText(path2.join(ctx.root, a.path));
|
|
2336
2381
|
if (text === null) {
|
|
2337
2382
|
disclosures.push(`${a.path} could not be read \u2014 unexamined, not clean.`);
|
|
2338
2383
|
outOfScope.push(a.path);
|
|
@@ -2353,7 +2398,7 @@ var stateFreshnessLens = {
|
|
|
2353
2398
|
}
|
|
2354
2399
|
}
|
|
2355
2400
|
for (const a of decisionArtifacts) {
|
|
2356
|
-
const text = await readText(
|
|
2401
|
+
const text = await readText(path2.join(ctx.root, a.path));
|
|
2357
2402
|
if (text === null) {
|
|
2358
2403
|
disclosures.push(
|
|
2359
2404
|
`${a.path} recognized as a decisions convention (directory) \u2014 age-exempt; per-entry format checks apply only to marker-carrying decisions files.`
|
|
@@ -2406,7 +2451,7 @@ init_util();
|
|
|
2406
2451
|
async function listInstructionFiles(root, facts, scope) {
|
|
2407
2452
|
const files = [];
|
|
2408
2453
|
const add = async (rel) => {
|
|
2409
|
-
const text = await readText(
|
|
2454
|
+
const text = await readText(path2.join(root, rel));
|
|
2410
2455
|
if (text !== null) files.push({ path: normalizeRelPath(rel), text });
|
|
2411
2456
|
};
|
|
2412
2457
|
const singleFileArtifacts = [
|
|
@@ -2422,21 +2467,21 @@ async function listInstructionFiles(root, facts, scope) {
|
|
|
2422
2467
|
const artifact = facts.artifacts.find((a) => a.id === id);
|
|
2423
2468
|
if (artifact?.exists) await add(artifact.path);
|
|
2424
2469
|
}
|
|
2425
|
-
const rulesDir =
|
|
2470
|
+
const rulesDir = path2.join(root, ".cursor", "rules");
|
|
2426
2471
|
if (await isDirectory(rulesDir)) {
|
|
2427
2472
|
try {
|
|
2428
2473
|
for (const entry of await promises.readdir(rulesDir)) {
|
|
2429
2474
|
if (entry.endsWith(".md") || entry.endsWith(".mdc"))
|
|
2430
|
-
await add(
|
|
2475
|
+
await add(path2.join(".cursor/rules", entry));
|
|
2431
2476
|
}
|
|
2432
2477
|
} catch {
|
|
2433
2478
|
}
|
|
2434
2479
|
}
|
|
2435
|
-
const skillsDir =
|
|
2480
|
+
const skillsDir = path2.join(root, ".claude", "skills");
|
|
2436
2481
|
if (await isDirectory(skillsDir)) {
|
|
2437
2482
|
try {
|
|
2438
2483
|
for (const entry of await promises.readdir(skillsDir)) {
|
|
2439
|
-
const skill =
|
|
2484
|
+
const skill = path2.join(".claude/skills", entry, "SKILL.md");
|
|
2440
2485
|
await add(skill);
|
|
2441
2486
|
}
|
|
2442
2487
|
} catch {
|
|
@@ -2464,7 +2509,7 @@ async function listStateDocuments(root, facts) {
|
|
|
2464
2509
|
const docs = [];
|
|
2465
2510
|
for (const artifact of facts.artifacts) {
|
|
2466
2511
|
if (artifact.kind !== "state" || !artifact.exists) continue;
|
|
2467
|
-
const text = await readText(
|
|
2512
|
+
const text = await readText(path2.join(root, artifact.path));
|
|
2468
2513
|
if (text !== null) docs.push({ path: normalizeRelPath(artifact.path), text });
|
|
2469
2514
|
}
|
|
2470
2515
|
return docs;
|
|
@@ -2748,28 +2793,28 @@ var instructionTruthLens = {
|
|
|
2748
2793
|
const knownScripts = new Set(Object.keys(facts.commands.raw));
|
|
2749
2794
|
for (const pkg of facts.packages) {
|
|
2750
2795
|
const pkgJson = await readJson(
|
|
2751
|
-
|
|
2796
|
+
path2.join(root, pkg.dir, "package.json")
|
|
2752
2797
|
);
|
|
2753
2798
|
for (const key of Object.keys(pkgJson?.scripts ?? {})) knownScripts.add(key);
|
|
2754
2799
|
}
|
|
2755
2800
|
const pathResolves = async (claim) => {
|
|
2756
|
-
const bases = [root, ...facts.packages.map((p) =>
|
|
2801
|
+
const bases = [root, ...facts.packages.map((p) => path2.join(root, p.dir))];
|
|
2757
2802
|
for (const base of bases) {
|
|
2758
|
-
if (await pathExists(
|
|
2759
|
-
if (await pathExists(
|
|
2760
|
-
if (await pathExists(
|
|
2803
|
+
if (await pathExists(path2.join(base, claim))) return true;
|
|
2804
|
+
if (await pathExists(path2.join(base, "src", claim))) return true;
|
|
2805
|
+
if (await pathExists(path2.join(base, "scripts", claim))) return true;
|
|
2761
2806
|
}
|
|
2762
2807
|
return false;
|
|
2763
2808
|
};
|
|
2764
2809
|
const binResolves = async (name) => {
|
|
2765
|
-
const bases = [root, ...facts.packages.map((p) =>
|
|
2810
|
+
const bases = [root, ...facts.packages.map((p) => path2.join(root, p.dir))];
|
|
2766
2811
|
for (const base of bases) {
|
|
2767
|
-
if (await pathExists(
|
|
2812
|
+
if (await pathExists(path2.join(base, "node_modules", ".bin", name))) return true;
|
|
2768
2813
|
}
|
|
2769
2814
|
return false;
|
|
2770
2815
|
};
|
|
2771
|
-
const nodeModulesInstalled = await pathExists(
|
|
2772
|
-
const manifestExists = await pathExists(
|
|
2816
|
+
const nodeModulesInstalled = await pathExists(path2.join(root, "node_modules"));
|
|
2817
|
+
const manifestExists = await pathExists(path2.join(root, "package.json")) || facts.packages.length > 0;
|
|
2773
2818
|
let totalFilteredSkipped = 0;
|
|
2774
2819
|
let binaryResolved = 0;
|
|
2775
2820
|
let unverifiableCommands = 0;
|
|
@@ -2861,7 +2906,7 @@ var instructionTruthLens = {
|
|
|
2861
2906
|
}
|
|
2862
2907
|
}
|
|
2863
2908
|
for (const ref of extractDocRefs(file.text)) {
|
|
2864
|
-
if (await pathExists(
|
|
2909
|
+
if (await pathExists(path2.join(root, ref))) continue;
|
|
2865
2910
|
findings.push(
|
|
2866
2911
|
finding2({
|
|
2867
2912
|
id: `${LENS_ID4}/dangling-ref:${file.path}:${ref}`,
|
|
@@ -2885,7 +2930,7 @@ var instructionTruthLens = {
|
|
|
2885
2930
|
if (stateDocs.length) {
|
|
2886
2931
|
for (const artifact of facts.artifacts) {
|
|
2887
2932
|
if (artifact.kind !== "decisions" || !artifact.exists) continue;
|
|
2888
|
-
const text = await readText(
|
|
2933
|
+
const text = await readText(path2.join(root, artifact.path));
|
|
2889
2934
|
if (text === null) continue;
|
|
2890
2935
|
const entries = parseDecisionEntries(text);
|
|
2891
2936
|
if (!entries.length) continue;
|
|
@@ -3019,14 +3064,14 @@ function rankFindings(findings) {
|
|
|
3019
3064
|
// src/engine/ledger.ts
|
|
3020
3065
|
init_util();
|
|
3021
3066
|
function ledgerPath(root) {
|
|
3022
|
-
return
|
|
3067
|
+
return path2.join(root, ".etymd", "ledger.json");
|
|
3023
3068
|
}
|
|
3024
3069
|
async function readLedger(root) {
|
|
3025
3070
|
return await readJson(ledgerPath(root)) ?? { version: 1, entries: [] };
|
|
3026
3071
|
}
|
|
3027
3072
|
async function writeLedger(root, ledger) {
|
|
3028
3073
|
const target = ledgerPath(root);
|
|
3029
|
-
await promises.mkdir(
|
|
3074
|
+
await promises.mkdir(path2.dirname(target), { recursive: true });
|
|
3030
3075
|
await promises.writeFile(target, JSON.stringify(ledger, null, 2) + "\n", "utf8");
|
|
3031
3076
|
}
|
|
3032
3077
|
function belongsToAny(id, paths) {
|
|
@@ -3111,7 +3156,7 @@ var LENSES = [
|
|
|
3111
3156
|
async function runAudit(root, opts = {}) {
|
|
3112
3157
|
const facts = await scanProject(root, { upstreamRemote: opts.upstreamRemote });
|
|
3113
3158
|
const ledgerRoot = opts.ledgerRoot ?? root;
|
|
3114
|
-
if (!opts.readOnlyRoot && ((opts.persistLedger ?? true) || await pathExists(
|
|
3159
|
+
if (!opts.readOnlyRoot && ((opts.persistLedger ?? true) || await pathExists(path2.join(root, ETYMD_DIR)))) {
|
|
3115
3160
|
await writeCachedFacts(root, facts);
|
|
3116
3161
|
}
|
|
3117
3162
|
const baseline = await readBaseline(root);
|
|
@@ -3171,7 +3216,7 @@ function isFleetTrust(value) {
|
|
|
3171
3216
|
}
|
|
3172
3217
|
function expandTilde(p) {
|
|
3173
3218
|
if (p === "~") return os.homedir();
|
|
3174
|
-
if (p.startsWith("~/")) return
|
|
3219
|
+
if (p.startsWith("~/")) return path2.join(os.homedir(), p.slice(2));
|
|
3175
3220
|
return p;
|
|
3176
3221
|
}
|
|
3177
3222
|
var REBUILD_RECIPE = 'rebuild it beside the manifest: {"machineProfile":"corp","dirs":{"<name>":"~/path/to/worktree"}}';
|
|
@@ -3206,7 +3251,7 @@ async function readLocal(localPath, problems) {
|
|
|
3206
3251
|
if (!parsed) {
|
|
3207
3252
|
problems.push({
|
|
3208
3253
|
kind: "parse-error",
|
|
3209
|
-
file:
|
|
3254
|
+
file: path2.basename(localPath),
|
|
3210
3255
|
detail: "must contain a JSON object \u2014 local mappings unusable"
|
|
3211
3256
|
});
|
|
3212
3257
|
return { present: true, data: {} };
|
|
@@ -3215,7 +3260,7 @@ async function readLocal(localPath, problems) {
|
|
|
3215
3260
|
} catch (err) {
|
|
3216
3261
|
problems.push({
|
|
3217
3262
|
kind: "parse-error",
|
|
3218
|
-
file:
|
|
3263
|
+
file: path2.basename(localPath),
|
|
3219
3264
|
detail: `not valid JSON (${err instanceof Error ? err.message : String(err)}) \u2014 local mappings unusable`
|
|
3220
3265
|
});
|
|
3221
3266
|
return { present: true, data: {} };
|
|
@@ -3283,13 +3328,13 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
|
|
|
3283
3328
|
} else {
|
|
3284
3329
|
const dir = manifest.localDirs[name];
|
|
3285
3330
|
if (!dir) {
|
|
3286
|
-
entry.unresolved = `no \`dirs\` mapping for \`${name}\` in ${
|
|
3331
|
+
entry.unresolved = `no \`dirs\` mapping for \`${name}\` in ${path2.basename(manifest.localPath)}`;
|
|
3287
3332
|
} else {
|
|
3288
|
-
entry.resolvedRoot =
|
|
3333
|
+
entry.resolvedRoot = path2.resolve(manifest.dir, expandTilde(dir));
|
|
3289
3334
|
}
|
|
3290
3335
|
}
|
|
3291
3336
|
} else if (entry.path) {
|
|
3292
|
-
entry.resolvedRoot =
|
|
3337
|
+
entry.resolvedRoot = path2.resolve(manifest.root ?? manifest.dir, expandTilde(entry.path));
|
|
3293
3338
|
} else {
|
|
3294
3339
|
entry.unresolved = "personal entry declares no `path` \u2014 nothing to resolve";
|
|
3295
3340
|
}
|
|
@@ -3298,7 +3343,7 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
|
|
|
3298
3343
|
if (corpNames.length && !manifest.localPresent) {
|
|
3299
3344
|
manifest.problems.push({
|
|
3300
3345
|
kind: "local-missing",
|
|
3301
|
-
file:
|
|
3346
|
+
file: path2.basename(manifest.localPath),
|
|
3302
3347
|
detail: `${corpNames.length} corp/private entr${corpNames.length === 1 ? "y" : "ies"} (${corpNames.join(", ")}) cannot resolve without it \u2014 ${REBUILD_RECIPE}`
|
|
3303
3348
|
});
|
|
3304
3349
|
}
|
|
@@ -3327,19 +3372,19 @@ function loadCorpusEntries(sources, manifest, manifestFile) {
|
|
|
3327
3372
|
};
|
|
3328
3373
|
const localDir = manifest.localDirs[name];
|
|
3329
3374
|
if (localDir) {
|
|
3330
|
-
entry.resolvedRoot =
|
|
3375
|
+
entry.resolvedRoot = path2.resolve(manifest.dir, "..", expandTilde(localDir));
|
|
3331
3376
|
} else if (entry.path) {
|
|
3332
|
-
entry.resolvedRoot =
|
|
3377
|
+
entry.resolvedRoot = path2.resolve(manifest.dir, expandTilde(entry.path));
|
|
3333
3378
|
} else {
|
|
3334
|
-
entry.resolvedRoot =
|
|
3379
|
+
entry.resolvedRoot = path2.resolve(manifest.dir, "..", name);
|
|
3335
3380
|
}
|
|
3336
3381
|
manifest.entries.push(entry);
|
|
3337
3382
|
}
|
|
3338
3383
|
}
|
|
3339
3384
|
async function loadFleetManifest(manifestPath) {
|
|
3340
|
-
const abs =
|
|
3341
|
-
const dir =
|
|
3342
|
-
const manifestFile =
|
|
3385
|
+
const abs = path2.resolve(manifestPath);
|
|
3386
|
+
const dir = path2.dirname(abs);
|
|
3387
|
+
const manifestFile = path2.basename(abs);
|
|
3343
3388
|
const raw = await readText(abs);
|
|
3344
3389
|
if (raw === null) {
|
|
3345
3390
|
throw new Error(`cannot read fleet manifest \`${abs}\``);
|
|
@@ -3351,7 +3396,7 @@ async function loadFleetManifest(manifestPath) {
|
|
|
3351
3396
|
corpHosts: [],
|
|
3352
3397
|
labels: {},
|
|
3353
3398
|
localDirs: {},
|
|
3354
|
-
localPath:
|
|
3399
|
+
localPath: path2.join(dir, "registry.local.json"),
|
|
3355
3400
|
localPresent: false,
|
|
3356
3401
|
entries: [],
|
|
3357
3402
|
problems: []
|
|
@@ -3384,7 +3429,7 @@ async function loadFleetManifest(manifestPath) {
|
|
|
3384
3429
|
manifest.corpHosts = Array.isArray(local.data.corpHosts) ? local.data.corpHosts.filter((h) => typeof h === "string") : [];
|
|
3385
3430
|
manifest.machineProfile = local.data.machineProfile === "corp" || local.data.machineProfile === "personal" ? local.data.machineProfile : void 0;
|
|
3386
3431
|
const rootRaw = asString(local.data.root) ?? asString(parsed.root);
|
|
3387
|
-
if (rootRaw) manifest.root =
|
|
3432
|
+
if (rootRaw) manifest.root = path2.resolve(dir, expandTilde(rootRaw));
|
|
3388
3433
|
const orientationRec = asRecord(parsed.orientation);
|
|
3389
3434
|
if (orientationRec) {
|
|
3390
3435
|
const orientationRoot = asString(orientationRec.root);
|
|
@@ -3406,7 +3451,7 @@ async function loadFleetManifest(manifestPath) {
|
|
|
3406
3451
|
loadRegistryEntries(parsed.projects, manifest, manifestFile);
|
|
3407
3452
|
} else if (Array.isArray(parsed.sources)) {
|
|
3408
3453
|
manifest.shape = "corpus";
|
|
3409
|
-
manifest.localPath =
|
|
3454
|
+
manifest.localPath = path2.join(dir, "sources.local.json");
|
|
3410
3455
|
const local = await readLocal(manifest.localPath, manifest.problems);
|
|
3411
3456
|
manifest.localPresent = local.present;
|
|
3412
3457
|
manifest.localDirs = asStringMap(local.data.dirs);
|
|
@@ -3462,9 +3507,9 @@ function finding3(id, tier, claim, evidence, why, action) {
|
|
|
3462
3507
|
};
|
|
3463
3508
|
}
|
|
3464
3509
|
function corpPersistenceRoot(manifest, name) {
|
|
3465
|
-
const corpDir =
|
|
3466
|
-
const root =
|
|
3467
|
-
if (
|
|
3510
|
+
const corpDir = path2.join(manifest.dir, "corp");
|
|
3511
|
+
const root = path2.resolve(corpDir, name);
|
|
3512
|
+
if (path2.dirname(root) !== corpDir || root === corpDir) {
|
|
3468
3513
|
throw new Error(
|
|
3469
3514
|
`entry name ${JSON.stringify(name)} escapes the corp persistence zone \u2014 refusing to resolve a ledger root for it`
|
|
3470
3515
|
);
|
|
@@ -3496,7 +3541,7 @@ async function realpathOr(p) {
|
|
|
3496
3541
|
try {
|
|
3497
3542
|
return await promises.realpath(p);
|
|
3498
3543
|
} catch {
|
|
3499
|
-
return
|
|
3544
|
+
return path2.resolve(p);
|
|
3500
3545
|
}
|
|
3501
3546
|
}
|
|
3502
3547
|
function emailMatchesCorpHosts(email, hosts) {
|
|
@@ -3519,8 +3564,8 @@ var PROBLEM_TIER = {
|
|
|
3519
3564
|
async function checkManifest(manifest) {
|
|
3520
3565
|
const findings = [];
|
|
3521
3566
|
const disclosures = [];
|
|
3522
|
-
const manifestFile =
|
|
3523
|
-
const localFile =
|
|
3567
|
+
const manifestFile = path2.basename(manifest.manifestPath);
|
|
3568
|
+
const localFile = path2.basename(manifest.localPath);
|
|
3524
3569
|
for (const p of manifest.problems) {
|
|
3525
3570
|
findings.push(
|
|
3526
3571
|
finding3(
|
|
@@ -3672,7 +3717,7 @@ async function checkCorpWallArtifacts(entries, findings) {
|
|
|
3672
3717
|
for (const entry of entries) {
|
|
3673
3718
|
if (entry.profile !== "corp" || !entry.resolvedRoot) continue;
|
|
3674
3719
|
for (const artifact of CORP_WALL_ARTIFACTS) {
|
|
3675
|
-
if (await pathExists(
|
|
3720
|
+
if (await pathExists(path2.join(entry.resolvedRoot, artifact))) {
|
|
3676
3721
|
findings.push(
|
|
3677
3722
|
finding3(
|
|
3678
3723
|
`${FLEET_LENS}/corp-artifact-in-repo:${entry.name}:${artifact}`,
|
|
@@ -3706,9 +3751,9 @@ async function checkCoverage(manifest, findings, disclosures) {
|
|
|
3706
3751
|
const dirents = await promises.readdir(manifest.root, { withFileTypes: true });
|
|
3707
3752
|
for (const d of dirents) {
|
|
3708
3753
|
if (!d.isDirectory() || d.name.startsWith(".")) continue;
|
|
3709
|
-
const dir =
|
|
3754
|
+
const dir = path2.join(manifest.root, d.name);
|
|
3710
3755
|
if (registered.has(await realpathOr(dir))) continue;
|
|
3711
|
-
if (!await pathExists(
|
|
3756
|
+
if (!await pathExists(path2.join(dir, ".git"))) continue;
|
|
3712
3757
|
const remotes = await git(dir, ["config", "--get-regexp", "^remote\\..*\\.url"]);
|
|
3713
3758
|
if (!remotes) continue;
|
|
3714
3759
|
const corpRemote = manifest.corpHosts.find((h) => remotes.includes(h));
|
|
@@ -3767,7 +3812,7 @@ async function checkHygieneNeedles(manifest, findings, disclosures) {
|
|
|
3767
3812
|
...new Set(
|
|
3768
3813
|
[
|
|
3769
3814
|
...Object.values(manifest.labels),
|
|
3770
|
-
...Object.values(manifest.localDirs).map((d) =>
|
|
3815
|
+
...Object.values(manifest.localDirs).map((d) => path2.basename(expandNeedleDir(d))),
|
|
3771
3816
|
...manifest.corpHosts
|
|
3772
3817
|
].filter((n) => n.length >= 3)
|
|
3773
3818
|
)
|
|
@@ -3970,7 +4015,7 @@ async function sweepEntry(manifest, entry, opts) {
|
|
|
3970
4015
|
}
|
|
3971
4016
|
}
|
|
3972
4017
|
const corp = entry.profile === "corp";
|
|
3973
|
-
const optedIn = !corp && await pathExists(
|
|
4018
|
+
const optedIn = !corp && await pathExists(path2.join(root, ETYMD_DIR));
|
|
3974
4019
|
const result = await runAudit(root, {
|
|
3975
4020
|
kind: opts.kind,
|
|
3976
4021
|
persistLedger: !corp && opts.persistLedgers === true && optedIn,
|
|
@@ -3988,7 +4033,7 @@ async function sweepEntry(manifest, entry, opts) {
|
|
|
3988
4033
|
project.staleAfterDays = result.config.config.state.staleAfterDays;
|
|
3989
4034
|
for (const [key, value] of Object.entries(entry.contract)) {
|
|
3990
4035
|
if (key === "placement" || !value) continue;
|
|
3991
|
-
const exists = await pathExists(
|
|
4036
|
+
const exists = await pathExists(path2.join(root, value.replace(/#.*$/, "")));
|
|
3992
4037
|
project.disclosures.push(
|
|
3993
4038
|
`contract override \`${key}: ${value}\` \u2014 registered, existence-checked only (${exists ? "present" : "MISSING"})`
|
|
3994
4039
|
);
|