etymd 0.5.0 → 0.7.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 +114 -0
- package/README.md +9 -4
- package/dist/{approve-ASWRO6OD.js → approve-TXF3MA3G.js} +6 -6
- package/dist/audit-354X7QN6.js +12 -0
- package/dist/{brief-HWD7CILP.js → brief-QUYHRWH6.js} +6 -6
- package/dist/{chunk-TADKOW6P.js → chunk-5BVKFJWM.js} +7 -3
- package/dist/{chunk-IGHES6AK.js → chunk-6DUDIVIC.js} +1 -1
- package/dist/{chunk-CYSEGWIW.js → chunk-7LVCGCHB.js} +2 -2
- package/dist/{chunk-R74SJ7HS.js → chunk-C7LBUFNU.js} +1 -1
- package/dist/{chunk-YLNQQZ5F.js → chunk-IV3FYVTS.js} +11 -1
- package/dist/{chunk-NMZ3RHWW.js → chunk-K7QBTANO.js} +2 -2
- package/dist/{chunk-OX3LUOZR.js → chunk-KTIEYKFK.js} +46 -22
- package/dist/{chunk-QJRPKZPA.js → chunk-LXEYYITD.js} +146 -12
- package/dist/{chunk-WWBF4Y7K.js → chunk-OAFYLBVG.js} +1 -1
- package/dist/{chunk-OJVQXB5Z.js → chunk-OGIHOTLP.js} +1 -1
- package/dist/{chunk-LT67FU2Y.js → chunk-OKU3HXIH.js} +2 -2
- package/dist/{chunk-MMPV67FW.js → chunk-V2SOHPXF.js} +2 -2
- package/dist/{chunk-4KOP36RD.js → chunk-XJMN2HMH.js} +3 -3
- package/dist/cli.js +18 -18
- package/dist/config-UJRSCDMD.js +4 -0
- package/dist/{context-4DGXO7JR.js → context-ZEUI5ESZ.js} +5 -5
- package/dist/doctor-2BU6WCI6.js +19 -0
- package/dist/{fleet-SAFXVN44.js → fleet-3UQ65X4Y.js} +24 -17
- package/dist/{gates-D5SBAVXB.js → gates-MYNOZCON.js} +21 -13
- package/dist/generate-UTKQ4GE6.js +6 -0
- package/dist/index.d.ts +26 -2
- package/dist/index.js +207 -34
- package/dist/{init-AVE2GDJ6.js → init-52PR24Q4.js} +8 -8
- package/dist/ledger-B2XGANTH.js +5 -0
- package/dist/{scan-5DSECI43.js → scan-GSDICQCX.js} +6 -6
- package/dist/scan-M6RKDNYR.js +5 -0
- package/dist/{screen-U7JMWZPS.js → screen-6R6NXDSY.js} +2 -2
- package/package.json +3 -4
- package/dist/audit-LPZOGOHE.js +0 -12
- package/dist/config-MX7OYI7F.js +0 -4
- package/dist/doctor-MJ6RBZ2Q.js +0 -19
- package/dist/generate-CBMTNN7P.js +0 -6
- package/dist/ledger-TMIV45AA.js +0 -5
- package/dist/scan-ES7IOXOC.js +0 -5
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execFile } from 'node:child_process';
|
|
|
2
2
|
import { promises } from 'node:fs';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
import YAML from 'yaml';
|
|
6
7
|
import os from 'node:os';
|
|
7
8
|
|
|
@@ -19,7 +20,7 @@ var __export = (target, all) => {
|
|
|
19
20
|
var PACK_VERSION;
|
|
20
21
|
var init_version = __esm({
|
|
21
22
|
"src/pack/version.ts"() {
|
|
22
|
-
PACK_VERSION = "
|
|
23
|
+
PACK_VERSION = "6";
|
|
23
24
|
}
|
|
24
25
|
});
|
|
25
26
|
|
|
@@ -29,7 +30,7 @@ var init_package = __esm({
|
|
|
29
30
|
"package.json"() {
|
|
30
31
|
package_default = {
|
|
31
32
|
name: "etymd",
|
|
32
|
-
version: "0.
|
|
33
|
+
version: "0.7.0",
|
|
33
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.",
|
|
34
35
|
keywords: [
|
|
35
36
|
"cli",
|
|
@@ -89,14 +90,13 @@ var init_package = __esm({
|
|
|
89
90
|
prepare: "npm run build",
|
|
90
91
|
prepublishOnly: "npm run build && ./scripts/artifact-check.sh",
|
|
91
92
|
changeset: "changeset",
|
|
92
|
-
"version:packages": "changeset version"
|
|
93
|
-
release: "npm run build && changeset publish"
|
|
93
|
+
"version:packages": "changeset version"
|
|
94
94
|
},
|
|
95
95
|
dependencies: {
|
|
96
96
|
"@clack/prompts": "0.7.0",
|
|
97
97
|
commander: "12.1.0",
|
|
98
98
|
picocolors: "1.1.1",
|
|
99
|
-
yaml: "2.
|
|
99
|
+
yaml: "2.9.0"
|
|
100
100
|
},
|
|
101
101
|
devDependencies: {
|
|
102
102
|
"@changesets/cli": "2.27.11",
|
|
@@ -152,6 +152,16 @@ async function isDirectory(p) {
|
|
|
152
152
|
return false;
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
+
async function isExecutable(p) {
|
|
156
|
+
try {
|
|
157
|
+
const stat = await promises.stat(p);
|
|
158
|
+
if (!stat.isFile()) return false;
|
|
159
|
+
if (process.platform === "win32") return null;
|
|
160
|
+
return (stat.mode & 73) !== 0;
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
155
165
|
async function git(root, args) {
|
|
156
166
|
try {
|
|
157
167
|
const { stdout } = await pExecFile("git", args, { cwd: root, timeout: 4e3 });
|
|
@@ -990,8 +1000,24 @@ var init_config = __esm({
|
|
|
990
1000
|
};
|
|
991
1001
|
}
|
|
992
1002
|
});
|
|
993
|
-
|
|
994
|
-
|
|
1003
|
+
function digestOf(body) {
|
|
1004
|
+
return createHash("sha256").update(body).digest("hex").slice(0, 16);
|
|
1005
|
+
}
|
|
1006
|
+
function stampGenerated(body, comment = "sh") {
|
|
1007
|
+
const marker = `etymd:generated pack-v${PACK_VERSION} ${digestOf(body)}`;
|
|
1008
|
+
return `${body}${comment === "md" ? `<!-- ${marker} -->` : `# ${marker}`}
|
|
1009
|
+
`;
|
|
1010
|
+
}
|
|
1011
|
+
function fileOrigin(text) {
|
|
1012
|
+
const lines = text.split("\n");
|
|
1013
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1014
|
+
const match = GENERATION_MARKER_RE.exec(lines[i]);
|
|
1015
|
+
if (!match) continue;
|
|
1016
|
+
const body = [...lines.slice(0, i), ...lines.slice(i + 1)].join("\n");
|
|
1017
|
+
return digestOf(body) === match[1] ? "pack" : "edited";
|
|
1018
|
+
}
|
|
1019
|
+
return "unstamped";
|
|
1020
|
+
}
|
|
995
1021
|
function runPrefix(pm) {
|
|
996
1022
|
switch (pm) {
|
|
997
1023
|
case "pnpm":
|
|
@@ -1044,7 +1070,8 @@ function generateAgentsMd(facts) {
|
|
|
1044
1070
|
const frameworks = facts.frameworks.length ? facts.frameworks.join(", ") : "none detected";
|
|
1045
1071
|
const workspace = facts.workspace.kind === "none" ? "single package" : `${facts.workspace.kind} workspace`;
|
|
1046
1072
|
const topDirs = facts.tree.dirs.slice(0, 14);
|
|
1047
|
-
return
|
|
1073
|
+
return stampGenerated(
|
|
1074
|
+
`# AGENTS.md
|
|
1048
1075
|
|
|
1049
1076
|
Operating contract for AI agents working in **${facts.name}**. One source of truth \u2014 most agents
|
|
1050
1077
|
(Claude Code, Codex, Cursor, Copilot, Gemini, \u2026) read this file natively. Kept true by
|
|
@@ -1087,16 +1114,17 @@ ${done.map((d) => `- ${d}`).join("\n")}` : `Define the check commands that gate
|
|
|
1087
1114
|
|
|
1088
1115
|
\`\`\`bash
|
|
1089
1116
|
${[
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1117
|
+
facts.commands.dev && `${run} ${facts.commands.dev}`,
|
|
1118
|
+
facts.commands.build && `${run} ${facts.commands.build}`,
|
|
1119
|
+
facts.commands.test && `${run} ${facts.commands.test}`,
|
|
1120
|
+
facts.commands.lint && `${run} ${facts.commands.lint}`,
|
|
1121
|
+
facts.commands.typecheck && `${run} ${facts.commands.typecheck}`
|
|
1122
|
+
].filter(Boolean).join("\n") || "# add the project's key commands"}
|
|
1096
1123
|
\`\`\`
|
|
1097
1124
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1125
|
+
`,
|
|
1126
|
+
"md"
|
|
1127
|
+
);
|
|
1100
1128
|
}
|
|
1101
1129
|
function localHookCall(hook) {
|
|
1102
1130
|
return `# Repo-owned checks. This file is generated and will be overwritten; \`.githooks/${hook}.local\`
|
|
@@ -1107,7 +1135,7 @@ if [ -x "$LOCAL" ]; then
|
|
|
1107
1135
|
fi`;
|
|
1108
1136
|
}
|
|
1109
1137
|
function generatePreCommitHook() {
|
|
1110
|
-
return `#!/usr/bin/env sh
|
|
1138
|
+
return stampGenerated(`#!/usr/bin/env sh
|
|
1111
1139
|
# etymd: process gate. Cheap, locally-knowable checks belong here (fast, blocks the commit).
|
|
1112
1140
|
|
|
1113
1141
|
${localHookCall("pre-commit")}
|
|
@@ -1124,10 +1152,10 @@ if [ -x "$GATE" ]; then
|
|
|
1124
1152
|
fi
|
|
1125
1153
|
|
|
1126
1154
|
exit 0
|
|
1127
|
-
|
|
1155
|
+
`);
|
|
1128
1156
|
}
|
|
1129
1157
|
function generateCommitMsgHook() {
|
|
1130
|
-
return `#!/usr/bin/env sh
|
|
1158
|
+
return stampGenerated(`#!/usr/bin/env sh
|
|
1131
1159
|
# etymd: content screen \u2014 the commit message itself.
|
|
1132
1160
|
#
|
|
1133
1161
|
# The staged-content gate reads file bytes and never sees the message, yet a message is as
|
|
@@ -1142,7 +1170,7 @@ fi
|
|
|
1142
1170
|
${localHookCall("commit-msg")}
|
|
1143
1171
|
|
|
1144
1172
|
exit 0
|
|
1145
|
-
|
|
1173
|
+
`);
|
|
1146
1174
|
}
|
|
1147
1175
|
function shellcheckStep() {
|
|
1148
1176
|
return `
|
|
@@ -1195,7 +1223,7 @@ if command -v etymd >/dev/null 2>&1; then
|
|
|
1195
1223
|
else
|
|
1196
1224
|
echo "\u203A etymd audit skipped (not on PATH)"
|
|
1197
1225
|
fi`;
|
|
1198
|
-
return `#!/usr/bin/env sh
|
|
1226
|
+
return stampGenerated(`#!/usr/bin/env sh
|
|
1199
1227
|
# etymd: correctness gate. Mirrors CI cheapest-first; blocks the push on any failure.
|
|
1200
1228
|
|
|
1201
1229
|
${localHookCall("pre-push")}
|
|
@@ -1211,10 +1239,10 @@ if [ -x "$GATE" ] && [ "\${CONTENT_GATE_PREPUSH:-1}" = "1" ]; then
|
|
|
1211
1239
|
fi
|
|
1212
1240
|
|
|
1213
1241
|
exit 0
|
|
1214
|
-
|
|
1242
|
+
`);
|
|
1215
1243
|
}
|
|
1216
1244
|
function generateArtifactCheckScript() {
|
|
1217
|
-
return `#!/usr/bin/env sh
|
|
1245
|
+
return stampGenerated(`#!/usr/bin/env sh
|
|
1218
1246
|
# etymd: content screen \u2014 the published ARTIFACT, not the repository.
|
|
1219
1247
|
#
|
|
1220
1248
|
# Wire it into the irreversible moment:
|
|
@@ -1243,12 +1271,13 @@ fi
|
|
|
1243
1271
|
|
|
1244
1272
|
"$GATE" screen --dir "$WORK" || exit 1
|
|
1245
1273
|
exit 0
|
|
1246
|
-
|
|
1274
|
+
`);
|
|
1247
1275
|
}
|
|
1248
|
-
var CONTENT_GATE_RESOLUTION;
|
|
1276
|
+
var GENERATION_MARKER_RE, CONTENT_GATE_RESOLUTION;
|
|
1249
1277
|
var init_templates = __esm({
|
|
1250
1278
|
"src/pack/templates.ts"() {
|
|
1251
1279
|
init_version();
|
|
1280
|
+
GENERATION_MARKER_RE = /^(?:# |<!-- )etymd:generated pack-v\S+ ([0-9a-f]{16})(?: -->)?$/;
|
|
1252
1281
|
CONTENT_GATE_RESOLUTION = `GATE="\${CONTENT_GATE:-$(command -v etymd || true)}"`;
|
|
1253
1282
|
}
|
|
1254
1283
|
});
|
|
@@ -1275,11 +1304,14 @@ async function planWorkflow(root, facts, opts) {
|
|
|
1275
1304
|
const abs = path.join(root, rel);
|
|
1276
1305
|
const exists = await pathExists(abs);
|
|
1277
1306
|
const existing = exists ? await readText(abs) : null;
|
|
1307
|
+
const differs = exists ? existing !== contents : void 0;
|
|
1308
|
+
const origin = differs && existing !== null ? fileOrigin(existing) : void 0;
|
|
1278
1309
|
out.push({
|
|
1279
1310
|
path: rel,
|
|
1280
1311
|
contents,
|
|
1281
1312
|
exists,
|
|
1282
|
-
differs
|
|
1313
|
+
differs,
|
|
1314
|
+
drift: origin === "pack" ? "stale" : origin,
|
|
1283
1315
|
executable,
|
|
1284
1316
|
label
|
|
1285
1317
|
});
|
|
@@ -1696,13 +1728,43 @@ function parseGithubWorkflow(text, file, scripts) {
|
|
|
1696
1728
|
}
|
|
1697
1729
|
return { jobs, parseErrors };
|
|
1698
1730
|
}
|
|
1731
|
+
async function companionOf(root, hooksDir, hook, hookText, scripts) {
|
|
1732
|
+
const rel = path.posix.join(hooksDir.split(path.sep).join("/"), `${hook}.local`);
|
|
1733
|
+
if (!hookText.includes(`${hook}.local`)) return null;
|
|
1734
|
+
const abs = path.join(root, hooksDir, `${hook}.local`);
|
|
1735
|
+
const text = await readText(abs);
|
|
1736
|
+
if (text === null) return null;
|
|
1737
|
+
const [executable, hookExecutable] = await Promise.all([
|
|
1738
|
+
isExecutable(abs),
|
|
1739
|
+
isExecutable(path.join(root, hooksDir, hook))
|
|
1740
|
+
]);
|
|
1741
|
+
if (executable === false && hookExecutable === true) return { rel, tools: [], state: "inert" };
|
|
1742
|
+
return {
|
|
1743
|
+
rel,
|
|
1744
|
+
tools: matchTools(text, scripts),
|
|
1745
|
+
state: executable === true ? "counted" : "unverified"
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1699
1748
|
async function localHookTools(root, facts, scripts) {
|
|
1700
1749
|
const hooks = facts.hooks;
|
|
1701
1750
|
const empty = [];
|
|
1751
|
+
const companions = [];
|
|
1752
|
+
const unverifiedCompanions = [];
|
|
1753
|
+
const inertCompanions = [];
|
|
1702
1754
|
const readHook = async (name) => {
|
|
1703
1755
|
if (!hooks.dir) return empty;
|
|
1704
1756
|
const text = await readText(path.join(root, hooks.dir, name));
|
|
1705
|
-
|
|
1757
|
+
if (!text) return empty;
|
|
1758
|
+
const tools = new Set(matchTools(text, scripts));
|
|
1759
|
+
const companion = await companionOf(root, hooks.dir, name, text, scripts);
|
|
1760
|
+
if (companion) {
|
|
1761
|
+
if (companion.state === "inert") inertCompanions.push(companion.rel);
|
|
1762
|
+
else {
|
|
1763
|
+
(companion.state === "counted" ? companions : unverifiedCompanions).push(companion.rel);
|
|
1764
|
+
for (const t of companion.tools) tools.add(t);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
return [...tools];
|
|
1706
1768
|
};
|
|
1707
1769
|
let preCommit = [];
|
|
1708
1770
|
let prePush = [];
|
|
@@ -1746,7 +1808,17 @@ async function localHookTools(root, facts, scripts) {
|
|
|
1746
1808
|
}
|
|
1747
1809
|
}
|
|
1748
1810
|
const wired = hooks.source === "custom" || hooks.source === "husky" || hooks.source === "husky-legacy" ? true : hooks.source === "githooks" ? facts.git.hooksPath === hooks.dir : false;
|
|
1749
|
-
return {
|
|
1811
|
+
return {
|
|
1812
|
+
source: hooks.source,
|
|
1813
|
+
wired,
|
|
1814
|
+
preCommit,
|
|
1815
|
+
prePush,
|
|
1816
|
+
commitMsg,
|
|
1817
|
+
lintStaged,
|
|
1818
|
+
companions,
|
|
1819
|
+
unverifiedCompanions,
|
|
1820
|
+
inertCompanions
|
|
1821
|
+
};
|
|
1750
1822
|
}
|
|
1751
1823
|
async function detectThresholds(root, scripts, ciJobs) {
|
|
1752
1824
|
const sonarConfigured = await pathExists(path.join(root, "sonar-project.properties")) || ciJobs.some((j) => j.tools.includes("sonar"));
|
|
@@ -1879,6 +1951,24 @@ function deriveGateFindings(inv, disclosures = []) {
|
|
|
1879
1951
|
);
|
|
1880
1952
|
}
|
|
1881
1953
|
}
|
|
1954
|
+
if (inv.local.inertCompanions?.length) {
|
|
1955
|
+
const files = inv.local.inertCompanions;
|
|
1956
|
+
findings.push(
|
|
1957
|
+
finding({
|
|
1958
|
+
id: `${LENS_ID2}/companion-not-executable`,
|
|
1959
|
+
tier: "gap",
|
|
1960
|
+
kind: "truth",
|
|
1961
|
+
claim: `${files.join(", ")} ${files.length === 1 ? "is" : "are"} called by a hook but not executable \u2014 the checks inside never run`,
|
|
1962
|
+
evidence: files,
|
|
1963
|
+
why: "The hook guards the call with `[ -x ]`, so it skips the file in silence \u2014 the repo looks guarded and is not, on every clone that carries the same mode bit.",
|
|
1964
|
+
// chmod alone does not survive a fresh clone: git tracks exactly one permission bit, so
|
|
1965
|
+
// the fix has to reach the index or the dead gate propagates to everyone else.
|
|
1966
|
+
action: `Run \`chmod +x ${files[0]}\` and commit the mode with \`git update-index --chmod=+x ${files[0]}\`.`,
|
|
1967
|
+
effort: "S",
|
|
1968
|
+
confidence: "high"
|
|
1969
|
+
})
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1882
1972
|
for (const tool of CORRECTNESS_TOOLS) {
|
|
1883
1973
|
if (!enforcedCiTools.has(tool) || localTools.has(tool)) continue;
|
|
1884
1974
|
const jobs = enforcedCiJobs.filter((j) => j.tools.includes(tool));
|
|
@@ -1979,6 +2069,16 @@ var gateIntegrityLens = {
|
|
|
1979
2069
|
}
|
|
1980
2070
|
if (inv.ci.parseErrors.length)
|
|
1981
2071
|
disclosures.push(...inv.ci.parseErrors.map((e) => `CI parse: ${e}`));
|
|
2072
|
+
if (inv.local.companions.length) {
|
|
2073
|
+
disclosures.push(
|
|
2074
|
+
`Checks in ${inv.local.companions.join(", ")} are counted as locally enforced \u2014 the generated hook calls the companion, and etymd reads it without owning it.`
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
if (inv.local.unverifiedCompanions.length) {
|
|
2078
|
+
disclosures.push(
|
|
2079
|
+
`Checks in ${inv.local.unverifiedCompanions.join(", ")} are counted as locally enforced, but the execute bit could not be judged here (no POSIX mode bits, or the hook beside it is not executable either) \u2014 whether ${inv.local.unverifiedCompanions.length === 1 ? "it runs" : "they run"} was not verified.`
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
1982
2082
|
if (inv.ci.system === "none")
|
|
1983
2083
|
disclosures.push("No CI configuration found \u2014 CI\u2194local comparisons skipped.");
|
|
1984
2084
|
const findings = deriveGateFindings(inv, disclosures);
|
|
@@ -2517,7 +2617,52 @@ init_config();
|
|
|
2517
2617
|
init_util();
|
|
2518
2618
|
var LENS_ID4 = "state-freshness";
|
|
2519
2619
|
var DECISIONS_FORMAT_MARKER = "<!-- decisions-format: 1 -->";
|
|
2620
|
+
var KNOWN_FORMAT_VERSION = 1;
|
|
2621
|
+
var MARKER_RE = /<!--\s*decisions-format:\s*(\d+)([^>]*?)-->/;
|
|
2622
|
+
var FIELD_NAME_RE = /^[A-Za-z0-9 _-]+$/;
|
|
2623
|
+
var BUILT_IN_FIELDS = /* @__PURE__ */ new Set(["scope"]);
|
|
2520
2624
|
var MS_PER_DAY = 864e5;
|
|
2625
|
+
function parseDecisionsFormat(text) {
|
|
2626
|
+
const m = MARKER_RE.exec(text);
|
|
2627
|
+
if (!m) return null;
|
|
2628
|
+
const problems = [];
|
|
2629
|
+
const fields = [];
|
|
2630
|
+
const version = Number(m[1]);
|
|
2631
|
+
if (version !== KNOWN_FORMAT_VERSION) {
|
|
2632
|
+
problems.push(
|
|
2633
|
+
`declares decisions-format version ${version}; this etymd understands version ${KNOWN_FORMAT_VERSION} \u2014 checked as version ${KNOWN_FORMAT_VERSION}.`
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
const attrs = (m[2] ?? "").trim();
|
|
2637
|
+
if (!attrs) return { fields, problems };
|
|
2638
|
+
const declared = /^fields=(.*)$/.exec(attrs);
|
|
2639
|
+
if (!declared) {
|
|
2640
|
+
problems.push(`marker attribute \`${attrs}\` is not understood \u2014 ignored (only \`fields=\`).`);
|
|
2641
|
+
return { fields, problems };
|
|
2642
|
+
}
|
|
2643
|
+
const seen = new Set(BUILT_IN_FIELDS);
|
|
2644
|
+
for (const raw of declared[1].split(",")) {
|
|
2645
|
+
const name = raw.trim();
|
|
2646
|
+
if (!name) continue;
|
|
2647
|
+
if (!FIELD_NAME_RE.test(name)) {
|
|
2648
|
+
problems.push(
|
|
2649
|
+
`declared field \`${name}\` is not a usable field name (letters, digits, spaces, \`-\`, \`_\`) \u2014 not checked.`
|
|
2650
|
+
);
|
|
2651
|
+
continue;
|
|
2652
|
+
}
|
|
2653
|
+
const key = name.toLowerCase();
|
|
2654
|
+
if (seen.has(key)) continue;
|
|
2655
|
+
seen.add(key);
|
|
2656
|
+
fields.push(name);
|
|
2657
|
+
}
|
|
2658
|
+
if (fields.length === 0 && problems.length === 0) {
|
|
2659
|
+
problems.push("marker declares `fields=` with no field names \u2014 no extra fields checked.");
|
|
2660
|
+
}
|
|
2661
|
+
return { fields, problems };
|
|
2662
|
+
}
|
|
2663
|
+
function hasField(block, name) {
|
|
2664
|
+
return new RegExp(`${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s*]*:`).test(block);
|
|
2665
|
+
}
|
|
2521
2666
|
function parseDecisionEntries(text) {
|
|
2522
2667
|
const headings = [...text.matchAll(/^## .*$/gm)];
|
|
2523
2668
|
const entries = [];
|
|
@@ -2573,9 +2718,23 @@ function checkIdSequence(file, entries) {
|
|
|
2573
2718
|
}
|
|
2574
2719
|
return findings;
|
|
2575
2720
|
}
|
|
2576
|
-
function checkFormatFields(file, entries, today) {
|
|
2721
|
+
function checkFormatFields(file, entries, today, declaredFields) {
|
|
2577
2722
|
const findings = [];
|
|
2578
2723
|
for (const entry of entries) {
|
|
2724
|
+
for (const field of declaredFields) {
|
|
2725
|
+
if (hasField(entry.block, field)) continue;
|
|
2726
|
+
findings.push({
|
|
2727
|
+
id: `${LENS_ID4}/field-missing:${file}:${entry.id}:${field}`,
|
|
2728
|
+
lens: LENS_ID4,
|
|
2729
|
+
tier: "gap",
|
|
2730
|
+
claim: `${file} ${entry.id} has no ${field}: field`,
|
|
2731
|
+
evidence: [`${file}: ${entry.id}`, `${file} marker declares required field \`${field}\``],
|
|
2732
|
+
why: "The file declares this field required on every entry after the marker; whatever reads the record for it finds nothing here.",
|
|
2733
|
+
action: `Add a ${field}: line to ${entry.id}.`,
|
|
2734
|
+
effort: "S",
|
|
2735
|
+
confidence: "high"
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2579
2738
|
if (!/Scope[\s*]*:/.test(entry.block)) {
|
|
2580
2739
|
findings.push({
|
|
2581
2740
|
id: `${LENS_ID4}/scope-missing:${file}:${entry.id}`,
|
|
@@ -2691,14 +2850,21 @@ var stateFreshnessLens = {
|
|
|
2691
2850
|
}
|
|
2692
2851
|
const entries = parseDecisionEntries(text);
|
|
2693
2852
|
findings.push(...checkIdSequence(a.path, entries));
|
|
2694
|
-
|
|
2853
|
+
const format = parseDecisionsFormat(text);
|
|
2854
|
+
if (!format) {
|
|
2695
2855
|
disclosures.push(
|
|
2696
2856
|
`${a.path} carries no \`${DECISIONS_FORMAT_MARKER}\` marker \u2014 format checks skipped (forward-only, never retroactive); id-sequence checks still ran.`
|
|
2697
2857
|
);
|
|
2698
2858
|
outOfScope.push(a.path);
|
|
2699
2859
|
continue;
|
|
2700
2860
|
}
|
|
2701
|
-
|
|
2861
|
+
for (const problem of format.problems) disclosures.push(`${a.path}: ${problem}`);
|
|
2862
|
+
if (format.fields.length > 0) {
|
|
2863
|
+
disclosures.push(
|
|
2864
|
+
`${a.path} declares required entry fields: ${format.fields.join(", ")} \u2014 checked on every entry (etymd attaches no meaning to the names).`
|
|
2865
|
+
);
|
|
2866
|
+
}
|
|
2867
|
+
findings.push(...checkFormatFields(a.path, entries, today, format.fields));
|
|
2702
2868
|
}
|
|
2703
2869
|
disclosures.push(
|
|
2704
2870
|
`Thresholds: staleAfterDays ${budgets.staleAfterDays} (3x escalates to risk), state budget ${budgets.maxChars} chars (${budgets.staleAfterDays === DEFAULT_CONFIG.state.staleAfterDays && budgets.maxChars === DEFAULT_CONFIG.state.maxChars ? `defaults \u2014 override under \`state\` in ${CONFIG_FILE}` : `set in ${CONFIG_FILE}`}). Decisions artifacts are exempt from age \u2014 old decisions are history, not defects.`
|
|
@@ -3571,7 +3737,14 @@ async function checkGateDrift(manifest, findings, disclosures) {
|
|
|
3571
3737
|
const { config } = await readConfig2(root);
|
|
3572
3738
|
const planned = (await planWorkflow2(root, facts, { agents: false, gates: true, gateConfig: config.gates })).filter((f) => f.executable);
|
|
3573
3739
|
const missing = planned.filter((f) => !f.exists).map((f) => f.path);
|
|
3574
|
-
const
|
|
3740
|
+
const differing = planned.filter((f) => f.exists && f.differs);
|
|
3741
|
+
const stale = differing.filter((f) => f.drift !== "edited").map((f) => f.path);
|
|
3742
|
+
const edited = differing.filter((f) => f.drift === "edited").map((f) => f.path);
|
|
3743
|
+
if (edited.length) {
|
|
3744
|
+
disclosures.push(
|
|
3745
|
+
`${entry.name}: ${edited.join(", ")} edited since etymd generated ${edited.length === 1 ? "it" : "them"} \u2014 a customisation, not drift; etymd will not overwrite ${edited.length === 1 ? "it" : "them"}`
|
|
3746
|
+
);
|
|
3747
|
+
}
|
|
3575
3748
|
if (missing.length) {
|
|
3576
3749
|
findings.push(
|
|
3577
3750
|
finding3(
|
|
@@ -3591,8 +3764,8 @@ async function checkGateDrift(manifest, findings, disclosures) {
|
|
|
3591
3764
|
"polish",
|
|
3592
3765
|
`\`${entry.name}\` has ${stale.length} gate${stale.length === 1 ? "" : "s"} that differ from what its own inputs generate`,
|
|
3593
3766
|
stale.map((s) => `${entry.name}: ${s}`),
|
|
3594
|
-
"
|
|
3595
|
-
"Re-run `etymd gates`
|
|
3767
|
+
"An outdated hook drifts from the pack silently \u2014 the repo believes it runs checks the file no longer contains.",
|
|
3768
|
+
"Re-run `etymd gates` in that repo \u2014 it regenerates a gate it provably wrote and asks before touching anything else."
|
|
3596
3769
|
)
|
|
3597
3770
|
);
|
|
3598
3771
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { applyFiles } from './chunk-EF6BPBG6.js';
|
|
3
|
-
import { planWorkflow } from './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import { theme, renderFacts, print, renderPlan, section, glyph } from './chunk-
|
|
6
|
-
import { scanProject } from './chunk-
|
|
7
|
-
import { VERSION } from './chunk-
|
|
8
|
-
import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-
|
|
9
|
-
import { PACK_VERSION } from './chunk-
|
|
10
|
-
import { git, readText } from './chunk-
|
|
3
|
+
import { planWorkflow } from './chunk-KTIEYKFK.js';
|
|
4
|
+
import './chunk-K7QBTANO.js';
|
|
5
|
+
import { theme, renderFacts, print, renderPlan, section, glyph } from './chunk-5BVKFJWM.js';
|
|
6
|
+
import { scanProject } from './chunk-XJMN2HMH.js';
|
|
7
|
+
import { VERSION } from './chunk-OGIHOTLP.js';
|
|
8
|
+
import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-C7LBUFNU.js';
|
|
9
|
+
import { PACK_VERSION } from './chunk-6DUDIVIC.js';
|
|
10
|
+
import { git, readText } from './chunk-IV3FYVTS.js';
|
|
11
11
|
import { promises } from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { intro, spinner, select, confirm, outro, isCancel, cancel } from '@clack/prompts';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { print, renderFacts, theme } from './chunk-
|
|
3
|
-
import { scanProject } from './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import { writeCachedFacts } from './chunk-
|
|
6
|
-
import './chunk-
|
|
7
|
-
import './chunk-
|
|
2
|
+
import { print, renderFacts, theme } from './chunk-5BVKFJWM.js';
|
|
3
|
+
import { scanProject } from './chunk-XJMN2HMH.js';
|
|
4
|
+
import './chunk-OGIHOTLP.js';
|
|
5
|
+
import { writeCachedFacts } from './chunk-C7LBUFNU.js';
|
|
6
|
+
import './chunk-6DUDIVIC.js';
|
|
7
|
+
import './chunk-IV3FYVTS.js';
|
|
8
8
|
|
|
9
9
|
// src/commands/scan.ts
|
|
10
10
|
async function run(opts) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { print, glyph, theme, section } from './chunk-
|
|
3
|
-
import { readText, git, pathExists } from './chunk-
|
|
2
|
+
import { print, glyph, theme, section } from './chunk-5BVKFJWM.js';
|
|
3
|
+
import { readText, git, pathExists } from './chunk-IV3FYVTS.js';
|
|
4
4
|
import { promises } from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "etymd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Keep your agent instructions true — verify AGENTS.md, CLAUDE.md, rules & skills against the actual repo, with drift caught over time and a regression ledger.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -60,14 +60,13 @@
|
|
|
60
60
|
"prepare": "npm run build",
|
|
61
61
|
"prepublishOnly": "npm run build && ./scripts/artifact-check.sh",
|
|
62
62
|
"changeset": "changeset",
|
|
63
|
-
"version:packages": "changeset version"
|
|
64
|
-
"release": "npm run build && changeset publish"
|
|
63
|
+
"version:packages": "changeset version"
|
|
65
64
|
},
|
|
66
65
|
"dependencies": {
|
|
67
66
|
"@clack/prompts": "0.7.0",
|
|
68
67
|
"commander": "12.1.0",
|
|
69
68
|
"picocolors": "1.1.1",
|
|
70
|
-
"yaml": "2.
|
|
69
|
+
"yaml": "2.9.0"
|
|
71
70
|
},
|
|
72
71
|
"devDependencies": {
|
|
73
72
|
"@changesets/cli": "2.27.11",
|
package/dist/audit-LPZOGOHE.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
export { run } from './chunk-CYSEGWIW.js';
|
|
3
|
-
import './chunk-QJRPKZPA.js';
|
|
4
|
-
import './chunk-LT67FU2Y.js';
|
|
5
|
-
import './chunk-WWBF4Y7K.js';
|
|
6
|
-
import './chunk-NMZ3RHWW.js';
|
|
7
|
-
import './chunk-TADKOW6P.js';
|
|
8
|
-
import './chunk-4KOP36RD.js';
|
|
9
|
-
import './chunk-OJVQXB5Z.js';
|
|
10
|
-
import './chunk-R74SJ7HS.js';
|
|
11
|
-
import './chunk-IGHES6AK.js';
|
|
12
|
-
import './chunk-YLNQQZ5F.js';
|
package/dist/config-MX7OYI7F.js
DELETED
package/dist/doctor-MJ6RBZ2Q.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { run } from './chunk-CYSEGWIW.js';
|
|
3
|
-
import './chunk-QJRPKZPA.js';
|
|
4
|
-
import './chunk-LT67FU2Y.js';
|
|
5
|
-
import './chunk-WWBF4Y7K.js';
|
|
6
|
-
import './chunk-NMZ3RHWW.js';
|
|
7
|
-
import './chunk-TADKOW6P.js';
|
|
8
|
-
import './chunk-4KOP36RD.js';
|
|
9
|
-
import './chunk-OJVQXB5Z.js';
|
|
10
|
-
import './chunk-R74SJ7HS.js';
|
|
11
|
-
import './chunk-IGHES6AK.js';
|
|
12
|
-
import './chunk-YLNQQZ5F.js';
|
|
13
|
-
|
|
14
|
-
// src/commands/doctor.ts
|
|
15
|
-
async function run2(opts) {
|
|
16
|
-
await run({ cwd: opts.cwd, json: opts.json, truth: true });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export { run2 as run };
|
package/dist/ledger-TMIV45AA.js
DELETED