etymd 0.13.0 → 0.15.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 +108 -0
- package/README.md +30 -19
- package/dist/{approve-YUT43YLC.js → approve-MA4Z3TBT.js} +5 -5
- package/dist/audit-YSALDC2L.js +11 -0
- package/dist/{brief-Z5S6OY2M.js → brief-DPZYSAMC.js} +5 -5
- package/dist/{chunk-PXRLOEN5.js → chunk-2RNQ6OLV.js} +146 -717
- package/dist/{chunk-DSAQ5S5D.js → chunk-CEO3BXQB.js} +3 -2
- package/dist/{chunk-D3R74TJ2.js → chunk-DBWDMIYO.js} +1 -1
- package/dist/{chunk-5BVKFJWM.js → chunk-HI7NWPRA.js} +79 -4
- package/dist/{chunk-2VLNI3L2.js → chunk-HOR4M6EC.js} +1 -1
- package/dist/chunk-IWG77WV3.js +758 -0
- package/dist/{chunk-DWL2IKZH.js → chunk-P6ATKV2R.js} +65 -3
- package/dist/{chunk-HRJJQCMT.js → chunk-UFNETE6P.js} +69 -20
- package/dist/{chunk-YXOAPMQH.js → chunk-Y6RZRED3.js} +2 -2
- package/dist/{chunk-LSZGCKIQ.js → chunk-YQZDYDAK.js} +1 -1
- package/dist/cli.js +42 -21
- package/dist/{config-XAH6PA5G.js → config-724Y3IOB.js} +1 -2
- package/dist/{context-F63RSIBH.js → context-JGKU4M7Z.js} +2 -4
- package/dist/doctor-Y3DWDEBT.js +18 -0
- package/dist/{fleet-YQ35KGEP.js → fleet-4FYZ3GBK.js} +11 -12
- package/dist/{gates-DU6SDH2M.js → gates-IBQ7HCAN.js} +6 -7
- package/dist/generate-KEX75XNG.js +5 -0
- package/dist/index.d.ts +173 -2
- package/dist/index.js +713 -198
- package/dist/{init-XJEYU2HC.js → init-H57H2JBE.js} +14 -13
- package/dist/ledger-T54JDHGP.js +6 -0
- package/dist/premise-VMC3UUIB.js +333 -0
- package/dist/scan-W4US23MU.js +5 -0
- package/dist/{scan-B24RZAV2.js → scan-XBOHDXAH.js} +5 -5
- package/dist/{screen-CS6PSG7U.js → screen-E4FC7W5N.js} +2 -1
- package/package.json +2 -2
- package/dist/audit-YRT4SWSQ.js +0 -12
- package/dist/chunk-F75Q43BC.js +0 -59
- package/dist/chunk-JHZ2BN4U.js +0 -67
- package/dist/doctor-7YKDXSKM.js +0 -19
- package/dist/generate-PMCP37DC.js +0 -6
- package/dist/ledger-4FOZ5HAB.js +0 -5
- package/dist/scan-QDNN7ER3.js +0 -5
package/dist/index.js
CHANGED
|
@@ -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 = "12";
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -30,8 +30,8 @@ var init_package = __esm({
|
|
|
30
30
|
"package.json"() {
|
|
31
31
|
package_default = {
|
|
32
32
|
name: "etymd",
|
|
33
|
-
version: "0.
|
|
34
|
-
description: "Keep your agent instructions true \u2014 verify AGENTS.md, CLAUDE.md, rules
|
|
33
|
+
version: "0.15.0",
|
|
34
|
+
description: "Keep your agent instructions true \u2014 verify AGENTS.md, CLAUDE.md, rules, skills, and the task you hand an agent against the actual repo, with drift caught over time and a regression ledger.",
|
|
35
35
|
keywords: [
|
|
36
36
|
"cli",
|
|
37
37
|
"ai",
|
|
@@ -1136,15 +1136,36 @@ ${[
|
|
|
1136
1136
|
"md"
|
|
1137
1137
|
);
|
|
1138
1138
|
}
|
|
1139
|
+
function isSelfBuildRepo(facts) {
|
|
1140
|
+
return facts.publishRoute !== "none" && facts.name === SELF_PACKAGE_NAME;
|
|
1141
|
+
}
|
|
1142
|
+
function contentGateResolution(selfBuild) {
|
|
1143
|
+
if (!selfBuild) return `GATE="\${CONTENT_GATE:-$(command -v etymd || true)}"`;
|
|
1144
|
+
return `GATE="\${CONTENT_GATE:-$(if [ -x ./dist/cli.js ]; then echo ./dist/cli.js; else command -v etymd || true; fi)}"`;
|
|
1145
|
+
}
|
|
1146
|
+
function contentScreenCall(opts) {
|
|
1147
|
+
const { args, envVar, blocking, indent } = opts;
|
|
1148
|
+
const hint = `etymd: this checker does not understand 'screen' (needs etymd 0.11+) \u2014 upgrade it, or set ${envVar} to a checker that does.`;
|
|
1149
|
+
return [
|
|
1150
|
+
`${indent}if ! "$GATE" screen ${args}; then`,
|
|
1151
|
+
`${indent} "$GATE" screen --help >/dev/null 2>&1 ||`,
|
|
1152
|
+
`${indent} echo "${hint}" >&2`,
|
|
1153
|
+
...blocking ? [`${indent} exit 1`] : [],
|
|
1154
|
+
`${indent}fi`
|
|
1155
|
+
].join("\n");
|
|
1156
|
+
}
|
|
1139
1157
|
function localHookCall(hook) {
|
|
1140
1158
|
return `# Repo-owned checks. This file is generated and will be overwritten; \`.githooks/${hook}.local\`
|
|
1141
1159
|
# is yours \u2014 etymd never reads, writes, or regenerates it. Put project-specific guards there.
|
|
1160
|
+
# A guard running tests that build fixture repositories should scrub git's exported GIT_* names
|
|
1161
|
+
# first \u2014 a child git inherits them and ignores its cwd, so the suite would hit the real repo:
|
|
1162
|
+
# env $(env | grep -o '^GIT_[A-Za-z0-9_]*' | sed 's/^/-u /') <your command>
|
|
1142
1163
|
LOCAL="$(dirname "$0")/${hook}.local"
|
|
1143
1164
|
if [ -x "$LOCAL" ]; then
|
|
1144
1165
|
"$LOCAL" "$@" || exit 1
|
|
1145
1166
|
fi`;
|
|
1146
1167
|
}
|
|
1147
|
-
function generatePreCommitHook() {
|
|
1168
|
+
function generatePreCommitHook(selfBuild = false) {
|
|
1148
1169
|
return stampGenerated(`#!/usr/bin/env sh
|
|
1149
1170
|
# etymd: process gate. Cheap, locally-knowable checks belong here (fast, blocks the commit).
|
|
1150
1171
|
|
|
@@ -1156,9 +1177,9 @@ ${localHookCall("pre-commit")}
|
|
|
1156
1177
|
# commit anywhere, active only where you opted in.
|
|
1157
1178
|
#
|
|
1158
1179
|
# Bypass, with a reason: git commit --no-verify
|
|
1159
|
-
${
|
|
1180
|
+
${contentGateResolution(selfBuild)}
|
|
1160
1181
|
if [ -x "$GATE" ]; then
|
|
1161
|
-
|
|
1182
|
+
${contentScreenCall({ args: "--staged", envVar: "CONTENT_GATE", blocking: true, indent: " " })}
|
|
1162
1183
|
fi
|
|
1163
1184
|
|
|
1164
1185
|
exit 0
|
|
@@ -1199,7 +1220,7 @@ function generateCommitMsgHook(gates) {
|
|
|
1199
1220
|
# Bypass, with a reason: git commit --no-verify
|
|
1200
1221
|
GATE="\${COMMIT_MSG_GATE:-$(command -v etymd || true)}"
|
|
1201
1222
|
if [ -x "$GATE" ]; then
|
|
1202
|
-
|
|
1223
|
+
${contentScreenCall({ args: '--message "$1"', envVar: "COMMIT_MSG_GATE", blocking: true, indent: " " })}
|
|
1203
1224
|
fi
|
|
1204
1225
|
${format}
|
|
1205
1226
|
${localHookCall("commit-msg")}
|
|
@@ -1210,11 +1231,24 @@ exit 0
|
|
|
1210
1231
|
function shellcheckStep() {
|
|
1211
1232
|
return `
|
|
1212
1233
|
# Shell correctness. Scripts are discovered by shebang over TRACKED files at push time, so a
|
|
1213
|
-
# script added later is covered without regenerating this hook.
|
|
1234
|
+
# script added later is covered without regenerating this hook. zsh is NOT in the checked set:
|
|
1235
|
+
# the checker cannot parse it (SC1071 is a parser-level error no inline directive can silence),
|
|
1236
|
+
# so checking it would fail every push on the parser, not on the script. Excluded \u2014 and said so
|
|
1237
|
+
# at run time below, because a coverage hole that is silent is indistinguishable from coverage.
|
|
1238
|
+
#
|
|
1239
|
+
# "the checker", not its name, on purpose: a comment whose first word is that name is read as
|
|
1240
|
+
# a DIRECTIVE, and an unparseable directive is itself an error (SC1072/SC1073). A hook that
|
|
1241
|
+
# explains why it skips a shell dialect must not break the checker while doing it.
|
|
1214
1242
|
if command -v shellcheck >/dev/null 2>&1; then
|
|
1215
1243
|
scripts=$(git ls-files -z \\
|
|
1216
|
-
| xargs -0 -I{} sh -c 'head -1 "{}" 2>/dev/null | grep -qE "^#!.*[/ ](ba|da
|
|
1244
|
+
| xargs -0 -I{} sh -c 'head -1 "{}" 2>/dev/null | grep -qE "^#!.*[/ ](ba|da)?sh( |$)" && echo "{}"' \\
|
|
1217
1245
|
| sort)
|
|
1246
|
+
zsh_scripts=$(git ls-files -z \\
|
|
1247
|
+
| xargs -0 -I{} sh -c 'head -1 "{}" 2>/dev/null | grep -qE "^#!.*[/ ]zsh( |$)" && echo "{}"' \\
|
|
1248
|
+
| sort)
|
|
1249
|
+
if [ -n "$zsh_scripts" ]; then
|
|
1250
|
+
echo "\u203A shellcheck: $(printf '%s\\n' "$zsh_scripts" | wc -l | tr -d ' ') zsh script(s) excluded \u2014 shellcheck cannot parse zsh (SC1071); not checked, not failed"
|
|
1251
|
+
fi
|
|
1218
1252
|
if [ -n "$scripts" ]; then
|
|
1219
1253
|
echo "\u203A shellcheck ($(printf '%s\\n' "$scripts" | wc -l | tr -d ' ') scripts, blocking at severity=warning)"
|
|
1220
1254
|
printf '%s\\n' "$scripts" | xargs shellcheck -S warning || {
|
|
@@ -1234,7 +1268,7 @@ else
|
|
|
1234
1268
|
echo "\u203A shellcheck skipped (not on PATH) \u2014 install it to gate this repo's shell scripts"
|
|
1235
1269
|
fi`;
|
|
1236
1270
|
}
|
|
1237
|
-
function generatePrePushHook(facts, gates) {
|
|
1271
|
+
function generatePrePushHook(facts, gates, selfBuild = false) {
|
|
1238
1272
|
const run = runPrefix(facts.packageManager);
|
|
1239
1273
|
const c = facts.commands;
|
|
1240
1274
|
const candidates = gates?.commands?.length ? gates.commands : [c.formatCheck, c.typecheck, c.lint];
|
|
@@ -1243,8 +1277,9 @@ function generatePrePushHook(facts, gates) {
|
|
|
1243
1277
|
(key) => Boolean(key) && (allowed.has(key) || isSafeGateCommand(c.raw[key]))
|
|
1244
1278
|
).map((key) => `${run} ${key}`);
|
|
1245
1279
|
const shellStep = facts.shell?.scripts ? shellcheckStep() : "";
|
|
1280
|
+
const runner = steps.length ? SCRUBBED_RUNNER : "";
|
|
1246
1281
|
const body = steps.length ? steps.map((s) => `echo "\u203A ${s}"
|
|
1247
|
-
${s} || exit 1`).join("\n") : shellStep ? (
|
|
1282
|
+
run_gate ${s} || exit 1`).join("\n") : shellStep ? (
|
|
1248
1283
|
// A repo whose executable surface is shell HAS a correctness command — it just is not in
|
|
1249
1284
|
// package.json. Claiming "none detected" beside a step that is about to run would be the
|
|
1250
1285
|
// tool contradicting itself.
|
|
@@ -1261,22 +1296,22 @@ fi`;
|
|
|
1261
1296
|
return stampGenerated(`#!/usr/bin/env sh
|
|
1262
1297
|
# etymd: correctness gate. Mirrors CI cheapest-first; blocks the push on any failure.
|
|
1263
1298
|
|
|
1264
|
-
${localHookCall("pre-push")}
|
|
1299
|
+
${localHookCall("pre-push")}${runner}
|
|
1265
1300
|
${body}${shellStep}
|
|
1266
1301
|
${auditStep}
|
|
1267
1302
|
|
|
1268
1303
|
# Content screen, second pass \u2014 the WHOLE TREE rather than one diff. Catches anything committed
|
|
1269
1304
|
# with --no-verify and anything a rebase or merge brought in from elsewhere. Advisory here (it
|
|
1270
1305
|
# never blocks the push): the blocking decision belongs at commit time, where the fix is cheap.
|
|
1271
|
-
${
|
|
1306
|
+
${contentGateResolution(selfBuild)}
|
|
1272
1307
|
if [ -x "$GATE" ]; then
|
|
1273
|
-
|
|
1308
|
+
${contentScreenCall({ args: "--tree --advisory", envVar: "CONTENT_GATE", blocking: false, indent: " " })}
|
|
1274
1309
|
fi
|
|
1275
1310
|
|
|
1276
1311
|
exit 0
|
|
1277
1312
|
`);
|
|
1278
1313
|
}
|
|
1279
|
-
function generateArtifactCheckScript() {
|
|
1314
|
+
function generateArtifactCheckScript(selfBuild = false) {
|
|
1280
1315
|
return stampGenerated(`#!/usr/bin/env sh
|
|
1281
1316
|
# etymd: content screen \u2014 the published ARTIFACT, not the repository.
|
|
1282
1317
|
#
|
|
@@ -1287,7 +1322,7 @@ function generateArtifactCheckScript() {
|
|
|
1287
1322
|
# .etymd-screen-allow entries (with provenance) if you must exempt a string.
|
|
1288
1323
|
set -eu
|
|
1289
1324
|
|
|
1290
|
-
${
|
|
1325
|
+
${contentGateResolution(selfBuild)}
|
|
1291
1326
|
[ -x "$GATE" ] || { echo "\u203A artifact-check: no checker installed \u2014 skipping."; exit 0; }
|
|
1292
1327
|
|
|
1293
1328
|
WORK=$(mktemp -d)
|
|
@@ -1300,16 +1335,16 @@ if [ -f package.json ]; then
|
|
|
1300
1335
|
tar -xzf "$WORK"/*.tgz -C "$WORK" 2>/dev/null || true
|
|
1301
1336
|
fi
|
|
1302
1337
|
|
|
1303
|
-
|
|
1338
|
+
${contentScreenCall({ args: '--dir "$WORK"', envVar: "CONTENT_GATE", blocking: true, indent: "" })}
|
|
1304
1339
|
exit 0
|
|
1305
1340
|
`);
|
|
1306
1341
|
}
|
|
1307
|
-
var GENERATION_MARKER_RE,
|
|
1342
|
+
var GENERATION_MARKER_RE, SELF_PACKAGE_NAME, COMMIT_TYPES, SUBJECT_ADVISORY_LENGTH, SCRUBBED_RUNNER;
|
|
1308
1343
|
var init_templates = __esm({
|
|
1309
1344
|
"src/pack/templates.ts"() {
|
|
1310
1345
|
init_version();
|
|
1311
1346
|
GENERATION_MARKER_RE = /^(?:# |<!-- )etymd:generated pack-v\S+ ([0-9a-f]{16})(?: -->)?$/;
|
|
1312
|
-
|
|
1347
|
+
SELF_PACKAGE_NAME = "etymd";
|
|
1313
1348
|
COMMIT_TYPES = [
|
|
1314
1349
|
"feat",
|
|
1315
1350
|
"fix",
|
|
@@ -1324,6 +1359,14 @@ var init_templates = __esm({
|
|
|
1324
1359
|
"revert"
|
|
1325
1360
|
];
|
|
1326
1361
|
SUBJECT_ADVISORY_LENGTH = 72;
|
|
1362
|
+
SCRUBBED_RUNNER = `
|
|
1363
|
+
# Gate steps run scrubbed of git's exported GIT_* names: a child git that inherits them ignores
|
|
1364
|
+
# its cwd, so a hook-run suite building fixture repositories would operate on the real repo.
|
|
1365
|
+
run_gate() (
|
|
1366
|
+
# shellcheck disable=SC2046 # word-splitting is the point: one -u per exported GIT_* name
|
|
1367
|
+
env $(env | grep -o '^GIT_[A-Za-z0-9_]*' | sed 's/^/-u /') "$@"
|
|
1368
|
+
)
|
|
1369
|
+
`;
|
|
1327
1370
|
}
|
|
1328
1371
|
});
|
|
1329
1372
|
|
|
@@ -1386,7 +1429,13 @@ async function planWorkflow(root, facts, opts) {
|
|
|
1386
1429
|
}
|
|
1387
1430
|
if (opts.gates) {
|
|
1388
1431
|
const existingPrePush = await readText(path2.join(root, ".githooks", "pre-push"));
|
|
1389
|
-
|
|
1432
|
+
const selfBuild = isSelfBuildRepo(facts);
|
|
1433
|
+
await add(
|
|
1434
|
+
".githooks/pre-commit",
|
|
1435
|
+
generatePreCommitHook(selfBuild),
|
|
1436
|
+
"Process gate (pre-commit)",
|
|
1437
|
+
true
|
|
1438
|
+
);
|
|
1390
1439
|
await add(
|
|
1391
1440
|
".githooks/commit-msg",
|
|
1392
1441
|
generateCommitMsgHook(opts.gateConfig),
|
|
@@ -1402,14 +1451,14 @@ async function planWorkflow(root, facts, opts) {
|
|
|
1402
1451
|
};
|
|
1403
1452
|
await add(
|
|
1404
1453
|
".githooks/pre-push",
|
|
1405
|
-
generatePrePushHook(facts, gateConfig),
|
|
1454
|
+
generatePrePushHook(facts, gateConfig, selfBuild),
|
|
1406
1455
|
"Correctness gate (pre-push)",
|
|
1407
1456
|
true
|
|
1408
1457
|
);
|
|
1409
1458
|
if (opts.gateConfig?.publishGate ?? opts.publishGate ?? facts.publishable) {
|
|
1410
1459
|
await add(
|
|
1411
1460
|
"scripts/artifact-check.sh",
|
|
1412
|
-
generateArtifactCheckScript(),
|
|
1461
|
+
generateArtifactCheckScript(selfBuild),
|
|
1413
1462
|
"Content screen (published artifact)",
|
|
1414
1463
|
true
|
|
1415
1464
|
);
|
|
@@ -1446,14 +1495,36 @@ function isAlwaysAppliedCursorRule(text) {
|
|
|
1446
1495
|
const frontmatter = fm[1] ?? "";
|
|
1447
1496
|
return /^\s*alwaysApply\s*:\s*true\s*$/m.test(frontmatter);
|
|
1448
1497
|
}
|
|
1498
|
+
async function inodeKey(abs) {
|
|
1499
|
+
try {
|
|
1500
|
+
const st = await promises.stat(abs);
|
|
1501
|
+
return st.ino ? `${st.dev}:${st.ino}` : null;
|
|
1502
|
+
} catch {
|
|
1503
|
+
return null;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
function contextFileLabel(file) {
|
|
1507
|
+
return file.aliases?.length ? [file.path, ...file.aliases].join(" \u2192 ") : file.path;
|
|
1508
|
+
}
|
|
1449
1509
|
async function measureContext(root, perFileWords = EXTRACTION_THRESHOLD) {
|
|
1450
1510
|
const files = [];
|
|
1511
|
+
const byInode = /* @__PURE__ */ new Map();
|
|
1512
|
+
const record = async (rel, role, text) => {
|
|
1513
|
+
const words = wordCount(text);
|
|
1514
|
+
const key = await inodeKey(path2.join(root, rel));
|
|
1515
|
+
const seen = key ? byInode.get(key) : void 0;
|
|
1516
|
+
if (seen) {
|
|
1517
|
+
seen.aliases = [...seen.aliases ?? [], rel];
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const file = { path: rel, role, words, approxTokens: approxTokens(words) };
|
|
1521
|
+
if (key) byInode.set(key, file);
|
|
1522
|
+
files.push(file);
|
|
1523
|
+
};
|
|
1451
1524
|
for (const spec of ALWAYS_LOADED) {
|
|
1452
|
-
const
|
|
1453
|
-
const text = await readText(abs);
|
|
1525
|
+
const text = await readText(path2.join(root, spec.path));
|
|
1454
1526
|
if (text === null) continue;
|
|
1455
|
-
|
|
1456
|
-
files.push({ path: spec.path, role: spec.role, words, approxTokens: approxTokens(words) });
|
|
1527
|
+
await record(spec.path, spec.role, text);
|
|
1457
1528
|
}
|
|
1458
1529
|
const rulesDir = path2.join(root, ".cursor", "rules");
|
|
1459
1530
|
if (await pathExists(rulesDir)) {
|
|
@@ -1462,13 +1533,7 @@ async function measureContext(root, perFileWords = EXTRACTION_THRESHOLD) {
|
|
|
1462
1533
|
if (!entry.endsWith(".mdc") && !entry.endsWith(".md")) continue;
|
|
1463
1534
|
const text = await readText(path2.join(rulesDir, entry));
|
|
1464
1535
|
if (text === null || !isAlwaysAppliedCursorRule(text)) continue;
|
|
1465
|
-
|
|
1466
|
-
files.push({
|
|
1467
|
-
path: `.cursor/rules/${entry}`,
|
|
1468
|
-
role: "Cursor rule (always applied)",
|
|
1469
|
-
words,
|
|
1470
|
-
approxTokens: approxTokens(words)
|
|
1471
|
-
});
|
|
1536
|
+
await record(`.cursor/rules/${entry}`, "Cursor rule (always applied)", text);
|
|
1472
1537
|
}
|
|
1473
1538
|
} catch {
|
|
1474
1539
|
}
|
|
@@ -1525,13 +1590,15 @@ var contextEconomyLens = {
|
|
|
1525
1590
|
const budgets = ctx.config?.config.context ?? DEFAULT_CONFIG.context;
|
|
1526
1591
|
const budget = await measureContext(ctx.root, budgets.perFileWords);
|
|
1527
1592
|
const findings = [];
|
|
1593
|
+
const aliased = budget.files.filter((f) => f.aliases?.length);
|
|
1528
1594
|
for (const f of budget.extractionCandidates) {
|
|
1595
|
+
const label = contextFileLabel(f);
|
|
1529
1596
|
findings.push({
|
|
1530
1597
|
id: `${LENS_ID}/heavy-file:${f.path}`,
|
|
1531
1598
|
lens: LENS_ID,
|
|
1532
1599
|
tier: "gap",
|
|
1533
|
-
claim: `${
|
|
1534
|
-
evidence: [`${
|
|
1600
|
+
claim: `${label} loads ${f.words} words (~${f.approxTokens} tokens) into every session`,
|
|
1601
|
+
evidence: [`${label}: ${f.words} words`],
|
|
1535
1602
|
why: "Reference material that loads every session taxes attention and cost on tasks that never need it.",
|
|
1536
1603
|
action: "Extract the reference bulk into an on-demand skill/doc and keep a pointer.",
|
|
1537
1604
|
effort: "M",
|
|
@@ -1544,7 +1611,7 @@ var contextEconomyLens = {
|
|
|
1544
1611
|
lens: LENS_ID,
|
|
1545
1612
|
tier: "gap",
|
|
1546
1613
|
claim: `The always-loaded footprint is ${budget.totalWords} words (~${budget.totalApproxTokens} tokens) \u2014 over the ${budgets.totalWords}-word budget`,
|
|
1547
|
-
evidence: budget.files.slice(0, 5).map((f) => `${f
|
|
1614
|
+
evidence: budget.files.slice(0, 5).map((f) => `${contextFileLabel(f)}: ${f.words}w`),
|
|
1548
1615
|
why: "Every session pays this before the task begins; instruction-following degrades as the resident context grows.",
|
|
1549
1616
|
action: "Run `etymd context` for the per-file breakdown and extract the heaviest block.",
|
|
1550
1617
|
effort: "M",
|
|
@@ -1559,6 +1626,9 @@ var contextEconomyLens = {
|
|
|
1559
1626
|
status: "ran",
|
|
1560
1627
|
disclosures: [
|
|
1561
1628
|
...ctx.config?.problems ?? [],
|
|
1629
|
+
...aliased.map(
|
|
1630
|
+
(f) => `${contextFileLabel(f)} are one file (same inode) \u2014 counted once, at ${f.words} words.`
|
|
1631
|
+
),
|
|
1562
1632
|
`Budgets: ${budgets.perFileWords} words/file, ${budgets.totalWords} words total (${budgets.perFileWords === DEFAULT_CONFIG.context.perFileWords && budgets.totalWords === DEFAULT_CONFIG.context.totalWords ? `defaults \u2014 override under \`context\` in ${CONFIG_FILE}` : `set in ${CONFIG_FILE}`}). Scoped Cursor rules excluded.`
|
|
1563
1633
|
],
|
|
1564
1634
|
findings
|
|
@@ -1966,6 +2036,73 @@ async function buildGateInventory(root, facts) {
|
|
|
1966
2036
|
};
|
|
1967
2037
|
}
|
|
1968
2038
|
|
|
2039
|
+
// src/lenses/gate-integrity/screener.ts
|
|
2040
|
+
init_util();
|
|
2041
|
+
var pExecFile2 = promisify(execFile);
|
|
2042
|
+
var HOOK_FILES = ["pre-commit", "pre-push", "commit-msg"];
|
|
2043
|
+
var SCREEN_CALL_RE = /"\$GATE"\s+screen\b/;
|
|
2044
|
+
var DEV_BUILD_ARM = "[ -x ./dist/cli.js ]";
|
|
2045
|
+
async function probeScreener(root, facts) {
|
|
2046
|
+
const absent = {
|
|
2047
|
+
present: false,
|
|
2048
|
+
doors: [],
|
|
2049
|
+
runner: null,
|
|
2050
|
+
source: null,
|
|
2051
|
+
answersScreen: null
|
|
2052
|
+
};
|
|
2053
|
+
const dir = facts.hooks.dir;
|
|
2054
|
+
if (!dir) return absent;
|
|
2055
|
+
const doors = [];
|
|
2056
|
+
let devBuildArm = false;
|
|
2057
|
+
for (const name of HOOK_FILES) {
|
|
2058
|
+
const text = await readText(path2.join(root, dir, name));
|
|
2059
|
+
if (!text || !SCREEN_CALL_RE.test(text)) continue;
|
|
2060
|
+
doors.push(`${dir}/${name}`);
|
|
2061
|
+
if (text.includes(DEV_BUILD_ARM)) devBuildArm = true;
|
|
2062
|
+
}
|
|
2063
|
+
if (!doors.length) return absent;
|
|
2064
|
+
let runner = null;
|
|
2065
|
+
let source = null;
|
|
2066
|
+
const override = process.env.CONTENT_GATE;
|
|
2067
|
+
if (override) {
|
|
2068
|
+
runner = override;
|
|
2069
|
+
source = "CONTENT_GATE";
|
|
2070
|
+
} else if (devBuildArm && await pathExists(path2.join(root, "dist", "cli.js"))) {
|
|
2071
|
+
runner = path2.join(root, "dist", "cli.js");
|
|
2072
|
+
source = "dev-build";
|
|
2073
|
+
} else {
|
|
2074
|
+
runner = "etymd";
|
|
2075
|
+
source = "path";
|
|
2076
|
+
}
|
|
2077
|
+
try {
|
|
2078
|
+
await pExecFile2(runner, ["screen", "--help"], { cwd: root, timeout: 1e4 });
|
|
2079
|
+
return { present: true, doors, runner, source, answersScreen: true };
|
|
2080
|
+
} catch (err) {
|
|
2081
|
+
const e = err;
|
|
2082
|
+
if (e.code === "ENOENT") {
|
|
2083
|
+
return {
|
|
2084
|
+
present: true,
|
|
2085
|
+
doors,
|
|
2086
|
+
runner: null,
|
|
2087
|
+
source,
|
|
2088
|
+
answersScreen: null,
|
|
2089
|
+
skipped: `no checker named \`${runner}\` is installed \u2014 the screen doors no-op here by design, and nothing was verified.`
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
if (e.killed || e.code === "ETIMEDOUT") {
|
|
2093
|
+
return {
|
|
2094
|
+
present: true,
|
|
2095
|
+
doors,
|
|
2096
|
+
runner,
|
|
2097
|
+
source,
|
|
2098
|
+
answersScreen: null,
|
|
2099
|
+
skipped: `\`${runner} screen --help\` did not return within 10s \u2014 the runner was not verified either way.`
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
return { present: true, doors, runner, source, answersScreen: false };
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
1969
2106
|
// src/lenses/gate-integrity/lens.ts
|
|
1970
2107
|
var LENS_ID2 = "gate-integrity";
|
|
1971
2108
|
var CORRECTNESS_TOOLS = ["typecheck", "lint", "format-check", "test"];
|
|
@@ -2108,6 +2245,28 @@ function deriveGateFindings(inv, disclosures = []) {
|
|
|
2108
2245
|
function localToolInHook(inv, tool) {
|
|
2109
2246
|
return inv.local.preCommit.includes(tool) || inv.local.prePush.includes(tool) || inv.local.commitMsg.includes(tool);
|
|
2110
2247
|
}
|
|
2248
|
+
function deriveScreenerFindings(probe, disclosures) {
|
|
2249
|
+
if (!probe.present) return [];
|
|
2250
|
+
if (probe.skipped) {
|
|
2251
|
+
disclosures.push(`Content screen: ${probe.skipped}`);
|
|
2252
|
+
return [];
|
|
2253
|
+
}
|
|
2254
|
+
if (probe.answersScreen !== false) return [];
|
|
2255
|
+
const via = probe.source === "CONTENT_GATE" ? "resolved from the CONTENT_GATE override" : probe.source === "dev-build" ? "resolved to this repo's own ./dist/cli.js build" : "resolved from PATH";
|
|
2256
|
+
return [
|
|
2257
|
+
finding({
|
|
2258
|
+
id: `${LENS_ID2}/content-screen-unrunnable`,
|
|
2259
|
+
tier: "risk",
|
|
2260
|
+
kind: "truth",
|
|
2261
|
+
claim: `The content screen resolves to \`${probe.runner}\`, which does not understand \`screen\` \u2014 the gate cannot run`,
|
|
2262
|
+
evidence: [...probe.doors, `${probe.runner} screen --help \u2192 failed (${via})`],
|
|
2263
|
+
why: "The commit door fails closed on an error that explains nothing, and the push door \u2014 advisory by design \u2014 skips the whole-tree pass in silence, so the repo reads as screened when nothing screened it.",
|
|
2264
|
+
action: "Install or upgrade etymd (`screen` needs 0.11+), or point CONTENT_GATE at a checker that provides it, then re-run `etymd gates` to refresh the hooks.",
|
|
2265
|
+
effort: "S",
|
|
2266
|
+
confidence: "high"
|
|
2267
|
+
})
|
|
2268
|
+
];
|
|
2269
|
+
}
|
|
2111
2270
|
var gateIntegrityLens = {
|
|
2112
2271
|
id: LENS_ID2,
|
|
2113
2272
|
version: "1",
|
|
@@ -2148,6 +2307,7 @@ var gateIntegrityLens = {
|
|
|
2148
2307
|
if (inv.ci.system === "none")
|
|
2149
2308
|
disclosures.push("No CI configuration found \u2014 CI\u2194local comparisons skipped.");
|
|
2150
2309
|
const findings = deriveGateFindings(inv, disclosures);
|
|
2310
|
+
findings.push(...deriveScreenerFindings(await probeScreener(ctx.root, ctx.facts), disclosures));
|
|
2151
2311
|
return {
|
|
2152
2312
|
lens: LENS_ID2,
|
|
2153
2313
|
version: "1",
|
|
@@ -2164,6 +2324,8 @@ var gateIntegrityLens = {
|
|
|
2164
2324
|
init_version();
|
|
2165
2325
|
init_config();
|
|
2166
2326
|
init_facts();
|
|
2327
|
+
|
|
2328
|
+
// src/lenses/instruction-truth/checks.ts
|
|
2167
2329
|
init_util();
|
|
2168
2330
|
|
|
2169
2331
|
// src/lenses/state-freshness.ts
|
|
@@ -2692,8 +2854,221 @@ var KNOWN_DOC_REFS = [
|
|
|
2692
2854
|
"DECISIONS.md",
|
|
2693
2855
|
"GEMINI.md"
|
|
2694
2856
|
];
|
|
2857
|
+
var PATH_TOKEN_CHARS = /[A-Za-z0-9_.$~/-]/;
|
|
2695
2858
|
function extractDocRefs(text) {
|
|
2696
|
-
|
|
2859
|
+
const refs = [];
|
|
2860
|
+
let tildeSkipped = 0;
|
|
2861
|
+
for (const name of KNOWN_DOC_REFS) {
|
|
2862
|
+
let claimed = false;
|
|
2863
|
+
let at = text.indexOf(name);
|
|
2864
|
+
while (at !== -1) {
|
|
2865
|
+
let head = at;
|
|
2866
|
+
while (head > 0 && PATH_TOKEN_CHARS.test(text[head - 1])) head -= 1;
|
|
2867
|
+
if (text[head] === "~") tildeSkipped += 1;
|
|
2868
|
+
else claimed = true;
|
|
2869
|
+
at = text.indexOf(name, at + name.length);
|
|
2870
|
+
}
|
|
2871
|
+
if (claimed) refs.push(name);
|
|
2872
|
+
}
|
|
2873
|
+
return { refs, tildeSkipped };
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
// src/lenses/instruction-truth/checks.ts
|
|
2877
|
+
async function buildTruthEnv(root, facts) {
|
|
2878
|
+
const knownScripts = new Set(Object.keys(facts.commands.raw));
|
|
2879
|
+
for (const pkg of facts.packages) {
|
|
2880
|
+
const pkgJson = await readJson(
|
|
2881
|
+
path2.join(root, pkg.dir, "package.json")
|
|
2882
|
+
);
|
|
2883
|
+
for (const key of Object.keys(pkgJson?.scripts ?? {})) knownScripts.add(key);
|
|
2884
|
+
}
|
|
2885
|
+
const bases = [root, ...facts.packages.map((p) => path2.join(root, p.dir))];
|
|
2886
|
+
const pathResolves = async (claim) => {
|
|
2887
|
+
for (const base of bases) {
|
|
2888
|
+
if (await pathExists(path2.join(base, claim))) return true;
|
|
2889
|
+
if (await pathExists(path2.join(base, "src", claim))) return true;
|
|
2890
|
+
if (await pathExists(path2.join(base, "scripts", claim))) return true;
|
|
2891
|
+
}
|
|
2892
|
+
return false;
|
|
2893
|
+
};
|
|
2894
|
+
const binResolves = async (name) => {
|
|
2895
|
+
for (const base of bases) {
|
|
2896
|
+
if (await pathExists(path2.join(base, "node_modules", ".bin", name))) return true;
|
|
2897
|
+
}
|
|
2898
|
+
return false;
|
|
2899
|
+
};
|
|
2900
|
+
const nodeModulesInstalled = await pathExists(path2.join(root, "node_modules"));
|
|
2901
|
+
const manifestExists = await pathExists(path2.join(root, "package.json")) || facts.packages.length > 0;
|
|
2902
|
+
return {
|
|
2903
|
+
root,
|
|
2904
|
+
facts,
|
|
2905
|
+
knownScripts,
|
|
2906
|
+
nodeModulesInstalled,
|
|
2907
|
+
manifestExists,
|
|
2908
|
+
bases,
|
|
2909
|
+
pathResolves,
|
|
2910
|
+
binResolves
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
function emptyCounters() {
|
|
2914
|
+
return {
|
|
2915
|
+
filteredSkipped: 0,
|
|
2916
|
+
tildeSkipped: 0,
|
|
2917
|
+
binaryResolved: 0,
|
|
2918
|
+
unverifiableCommands: 0,
|
|
2919
|
+
gitignoredSkipped: 0,
|
|
2920
|
+
prospectiveSkipped: 0,
|
|
2921
|
+
placeholderSkipped: 0,
|
|
2922
|
+
qualifiedRefsSkipped: 0,
|
|
2923
|
+
unresolvableRefs: 0
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2926
|
+
async function checkTextClaims(env, file, opts, counters) {
|
|
2927
|
+
const findings = [];
|
|
2928
|
+
const disclosures = [];
|
|
2929
|
+
const examined = [];
|
|
2930
|
+
const subject = opts.subject ?? file.path;
|
|
2931
|
+
const { scripts: claimed, filteredSkipped } = extractCommandClaims(file.text);
|
|
2932
|
+
counters.filteredSkipped += filteredSkipped;
|
|
2933
|
+
for (const [script, raw] of claimed) {
|
|
2934
|
+
if (env.knownScripts.has(script)) {
|
|
2935
|
+
examined.push({ kind: "script", value: script, exists: true });
|
|
2936
|
+
continue;
|
|
2937
|
+
}
|
|
2938
|
+
if (await env.binResolves(script)) {
|
|
2939
|
+
counters.binaryResolved += 1;
|
|
2940
|
+
examined.push({ kind: "script", value: script, exists: true });
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
if (!env.nodeModulesInstalled && env.manifestExists) {
|
|
2944
|
+
counters.unverifiableCommands += 1;
|
|
2945
|
+
examined.push({ kind: "script", value: script, exists: null });
|
|
2946
|
+
continue;
|
|
2947
|
+
}
|
|
2948
|
+
examined.push({ kind: "script", value: script, exists: false });
|
|
2949
|
+
findings.push({
|
|
2950
|
+
lens: opts.lensId,
|
|
2951
|
+
id: `${opts.lensId}/stale-command:${file.path}:${script}`,
|
|
2952
|
+
tier: "risk",
|
|
2953
|
+
claim: `${subject} tells agents to run \`${script}\` \u2014 no such script exists`,
|
|
2954
|
+
evidence: [`${file.path}: \`${raw}\``, "package.json scripts (root + workspaces)"],
|
|
2955
|
+
why: opts.whyCommand ?? "An agent following this instruction runs a command that fails \u2014 or silently skips the check it was meant to run.",
|
|
2956
|
+
action: opts.actionCommand ?? "Update the instruction to the current script name (or restore the script).",
|
|
2957
|
+
effort: "S",
|
|
2958
|
+
confidence: "high"
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
const { paths, prospective, placeholder } = extractPathClaims(file.text);
|
|
2962
|
+
counters.prospectiveSkipped += prospective.length;
|
|
2963
|
+
counters.placeholderSkipped += placeholder.length;
|
|
2964
|
+
const missing = [];
|
|
2965
|
+
for (const claim of paths) {
|
|
2966
|
+
if (await env.pathResolves(claim)) examined.push({ kind: "path", value: claim, exists: true });
|
|
2967
|
+
else missing.push(claim);
|
|
2968
|
+
}
|
|
2969
|
+
const ignoredOut = missing.length ? await git(env.root, ["check-ignore", ...missing]) : null;
|
|
2970
|
+
const gitignored = new Set((ignoredOut ?? "").split("\n").filter(Boolean));
|
|
2971
|
+
let pathFindings = 0;
|
|
2972
|
+
for (const claim of missing) {
|
|
2973
|
+
if (gitignored.has(claim)) {
|
|
2974
|
+
counters.gitignoredSkipped += 1;
|
|
2975
|
+
examined.push({ kind: "path", value: claim, exists: null });
|
|
2976
|
+
continue;
|
|
2977
|
+
}
|
|
2978
|
+
examined.push({ kind: "path", value: claim, exists: false });
|
|
2979
|
+
if (pathFindings >= opts.maxPathFindings) {
|
|
2980
|
+
if (pathFindings === opts.maxPathFindings) {
|
|
2981
|
+
disclosures.push(
|
|
2982
|
+
`${file.path}: more than ${opts.maxPathFindings} missing-path claims \u2014 truncated.`
|
|
2983
|
+
);
|
|
2984
|
+
pathFindings += 1;
|
|
2985
|
+
}
|
|
2986
|
+
continue;
|
|
2987
|
+
}
|
|
2988
|
+
pathFindings += 1;
|
|
2989
|
+
findings.push({
|
|
2990
|
+
lens: opts.lensId,
|
|
2991
|
+
id: `${opts.lensId}/stale-path:${file.path}:${claim}`,
|
|
2992
|
+
tier: opts.missingPathTier,
|
|
2993
|
+
claim: `${subject} references \`${claim}\` \u2014 it does not exist in the repo`,
|
|
2994
|
+
evidence: [file.path, `missing: ${claim}`],
|
|
2995
|
+
why: opts.whyPath ?? "Agents navigate by these references; a dead path wastes a lookup and erodes trust in the rest of the file.",
|
|
2996
|
+
action: opts.actionPath ?? "Fix or remove the reference.",
|
|
2997
|
+
effort: "S",
|
|
2998
|
+
confidence: "medium"
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
return { findings, disclosures, examined };
|
|
3002
|
+
}
|
|
3003
|
+
async function checkDocRefs(env, file, lensId, counters, subject = file.path) {
|
|
3004
|
+
const findings = [];
|
|
3005
|
+
const examined = [];
|
|
3006
|
+
const { refs, tildeSkipped } = extractDocRefs(file.text);
|
|
3007
|
+
counters.tildeSkipped += tildeSkipped;
|
|
3008
|
+
for (const ref of refs) {
|
|
3009
|
+
const exists = await pathExists(path2.join(env.root, ref));
|
|
3010
|
+
examined.push({ kind: "doc", value: ref, exists });
|
|
3011
|
+
if (exists) continue;
|
|
3012
|
+
findings.push({
|
|
3013
|
+
lens: lensId,
|
|
3014
|
+
id: `${lensId}/dangling-ref:${file.path}:${ref}`,
|
|
3015
|
+
tier: "gap",
|
|
3016
|
+
claim: `${subject} references ${ref} \u2014 no such file exists`,
|
|
3017
|
+
evidence: [file.path, `missing: ${ref}`],
|
|
3018
|
+
why: "The pointer chain agents follow breaks at a file they can never read.",
|
|
3019
|
+
action: `Create ${ref} or remove the reference.`,
|
|
3020
|
+
effort: "S",
|
|
3021
|
+
confidence: "high"
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
return { findings, examined };
|
|
3025
|
+
}
|
|
3026
|
+
async function loadDecisionLedger(root, facts) {
|
|
3027
|
+
let ids = null;
|
|
3028
|
+
const sources = [];
|
|
3029
|
+
for (const artifact of facts.artifacts) {
|
|
3030
|
+
if (artifact.kind !== "decisions" || !artifact.exists) continue;
|
|
3031
|
+
const text = await readText(path2.join(root, artifact.path));
|
|
3032
|
+
if (text === null) continue;
|
|
3033
|
+
const entries = parseDecisionEntries(text);
|
|
3034
|
+
if (!entries.length) continue;
|
|
3035
|
+
ids ??= /* @__PURE__ */ new Set();
|
|
3036
|
+
for (const entry of entries) ids.add(entry.num);
|
|
3037
|
+
sources.push(artifact.path);
|
|
3038
|
+
}
|
|
3039
|
+
return { ids, sources };
|
|
3040
|
+
}
|
|
3041
|
+
function checkDecisionRefs(file, ledger, opts, counters) {
|
|
3042
|
+
const findings = [];
|
|
3043
|
+
const examined = [];
|
|
3044
|
+
const subject = opts.subject ?? file.path;
|
|
3045
|
+
const { refs, qualifiedSkipped } = extractDecisionRefs(file.text);
|
|
3046
|
+
counters.qualifiedRefsSkipped += qualifiedSkipped;
|
|
3047
|
+
if (!refs.size) return { findings, examined };
|
|
3048
|
+
if (!ledger.ids) {
|
|
3049
|
+
counters.unresolvableRefs += refs.size;
|
|
3050
|
+
for (const asWritten of refs.values()) {
|
|
3051
|
+
examined.push({ kind: "decision", value: asWritten, exists: null });
|
|
3052
|
+
}
|
|
3053
|
+
return { findings, examined };
|
|
3054
|
+
}
|
|
3055
|
+
for (const [num, asWritten] of refs) {
|
|
3056
|
+
const exists = ledger.ids.has(num);
|
|
3057
|
+
examined.push({ kind: "decision", value: asWritten, exists });
|
|
3058
|
+
if (exists) continue;
|
|
3059
|
+
findings.push({
|
|
3060
|
+
lens: opts.lensId,
|
|
3061
|
+
id: `${opts.lensId}/dead-decision-ref:${file.path}:${asWritten}`,
|
|
3062
|
+
tier: "gap",
|
|
3063
|
+
claim: `${subject} cites ${asWritten} \u2014 no such entry exists in ${ledger.sources.join(", ")}`,
|
|
3064
|
+
evidence: [file.path, `${ledger.sources.join(", ")}: no ${asWritten} entry`],
|
|
3065
|
+
why: opts.why ?? "A state doc is read as ground truth on return; a citation the decision record cannot back sends readers to a ruling that was never written.",
|
|
3066
|
+
action: opts.action ?? "Fix the reference \u2014 or record the missing decision.",
|
|
3067
|
+
effort: "S",
|
|
3068
|
+
confidence: "medium"
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
return { findings, examined };
|
|
2697
3072
|
}
|
|
2698
3073
|
|
|
2699
3074
|
// src/lenses/instruction-truth/lens.ts
|
|
@@ -2790,98 +3165,17 @@ var instructionTruthLens = {
|
|
|
2790
3165
|
})
|
|
2791
3166
|
);
|
|
2792
3167
|
}
|
|
2793
|
-
const
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
}
|
|
2800
|
-
const pathResolves = async (claim) => {
|
|
2801
|
-
const bases = [root, ...facts.packages.map((p) => path2.join(root, p.dir))];
|
|
2802
|
-
for (const base of bases) {
|
|
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;
|
|
2806
|
-
}
|
|
2807
|
-
return false;
|
|
2808
|
-
};
|
|
2809
|
-
const binResolves = async (name) => {
|
|
2810
|
-
const bases = [root, ...facts.packages.map((p) => path2.join(root, p.dir))];
|
|
2811
|
-
for (const base of bases) {
|
|
2812
|
-
if (await pathExists(path2.join(base, "node_modules", ".bin", name))) return true;
|
|
2813
|
-
}
|
|
2814
|
-
return false;
|
|
3168
|
+
const env = await buildTruthEnv(root, facts);
|
|
3169
|
+
const counters = emptyCounters();
|
|
3170
|
+
const claimOpts = {
|
|
3171
|
+
lensId: LENS_ID4,
|
|
3172
|
+
missingPathTier: "gap",
|
|
3173
|
+
maxPathFindings: MAX_PATH_FINDINGS_PER_FILE
|
|
2815
3174
|
};
|
|
2816
|
-
const nodeModulesInstalled = await pathExists(path2.join(root, "node_modules"));
|
|
2817
|
-
const manifestExists = await pathExists(path2.join(root, "package.json")) || facts.packages.length > 0;
|
|
2818
|
-
let totalFilteredSkipped = 0;
|
|
2819
|
-
let binaryResolved = 0;
|
|
2820
|
-
let unverifiableCommands = 0;
|
|
2821
|
-
let gitignoredSkipped = 0;
|
|
2822
|
-
let prospectiveSkipped = 0;
|
|
2823
|
-
let placeholderSkipped = 0;
|
|
2824
3175
|
const auditClaims = async (file) => {
|
|
2825
|
-
const
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
if (knownScripts.has(script)) continue;
|
|
2829
|
-
if (await binResolves(script)) {
|
|
2830
|
-
binaryResolved += 1;
|
|
2831
|
-
continue;
|
|
2832
|
-
}
|
|
2833
|
-
if (!nodeModulesInstalled && manifestExists) {
|
|
2834
|
-
unverifiableCommands += 1;
|
|
2835
|
-
continue;
|
|
2836
|
-
}
|
|
2837
|
-
findings.push(
|
|
2838
|
-
finding2({
|
|
2839
|
-
id: `${LENS_ID4}/stale-command:${file.path}:${script}`,
|
|
2840
|
-
tier: "risk",
|
|
2841
|
-
claim: `${file.path} tells agents to run \`${script}\` \u2014 no such script exists`,
|
|
2842
|
-
evidence: [`${file.path}: \`${raw}\``, "package.json scripts (root + workspaces)"],
|
|
2843
|
-
why: "An agent following this instruction runs a command that fails \u2014 or silently skips the check it was meant to run.",
|
|
2844
|
-
action: "Update the instruction to the current script name (or restore the script).",
|
|
2845
|
-
effort: "S",
|
|
2846
|
-
confidence: "high"
|
|
2847
|
-
})
|
|
2848
|
-
);
|
|
2849
|
-
}
|
|
2850
|
-
const { paths, prospective, placeholder } = extractPathClaims(file.text);
|
|
2851
|
-
prospectiveSkipped += prospective.length;
|
|
2852
|
-
placeholderSkipped += placeholder.length;
|
|
2853
|
-
const missing = [];
|
|
2854
|
-
for (const claim of paths) {
|
|
2855
|
-
if (!await pathResolves(claim)) missing.push(claim);
|
|
2856
|
-
}
|
|
2857
|
-
const ignoredOut = missing.length ? await git(root, ["check-ignore", ...missing]) : null;
|
|
2858
|
-
const gitignored = new Set((ignoredOut ?? "").split("\n").filter(Boolean));
|
|
2859
|
-
let pathFindings = 0;
|
|
2860
|
-
for (const claim of missing) {
|
|
2861
|
-
if (gitignored.has(claim)) {
|
|
2862
|
-
gitignoredSkipped += 1;
|
|
2863
|
-
continue;
|
|
2864
|
-
}
|
|
2865
|
-
if (pathFindings >= MAX_PATH_FINDINGS_PER_FILE) {
|
|
2866
|
-
disclosures.push(
|
|
2867
|
-
`${file.path}: more than ${MAX_PATH_FINDINGS_PER_FILE} missing-path claims \u2014 truncated.`
|
|
2868
|
-
);
|
|
2869
|
-
break;
|
|
2870
|
-
}
|
|
2871
|
-
pathFindings += 1;
|
|
2872
|
-
findings.push(
|
|
2873
|
-
finding2({
|
|
2874
|
-
id: `${LENS_ID4}/stale-path:${file.path}:${claim}`,
|
|
2875
|
-
tier: "gap",
|
|
2876
|
-
claim: `${file.path} references \`${claim}\` \u2014 it does not exist in the repo`,
|
|
2877
|
-
evidence: [file.path, `missing: ${claim}`],
|
|
2878
|
-
why: "Agents navigate by these references; a dead path wastes a lookup and erodes trust in the rest of the file.",
|
|
2879
|
-
action: "Fix or remove the reference.",
|
|
2880
|
-
effort: "S",
|
|
2881
|
-
confidence: "medium"
|
|
2882
|
-
})
|
|
2883
|
-
);
|
|
2884
|
-
}
|
|
3176
|
+
const result = await checkTextClaims(env, file, claimOpts, counters);
|
|
3177
|
+
findings.push(...result.findings);
|
|
3178
|
+
disclosures.push(...result.disclosures);
|
|
2885
3179
|
};
|
|
2886
3180
|
for (const file of files) {
|
|
2887
3181
|
await auditClaims(file);
|
|
@@ -2905,64 +3199,15 @@ var instructionTruthLens = {
|
|
|
2905
3199
|
break;
|
|
2906
3200
|
}
|
|
2907
3201
|
}
|
|
2908
|
-
|
|
2909
|
-
if (await pathExists(path2.join(root, ref))) continue;
|
|
2910
|
-
findings.push(
|
|
2911
|
-
finding2({
|
|
2912
|
-
id: `${LENS_ID4}/dangling-ref:${file.path}:${ref}`,
|
|
2913
|
-
tier: "gap",
|
|
2914
|
-
claim: `${file.path} references ${ref} \u2014 no such file exists`,
|
|
2915
|
-
evidence: [file.path, `missing: ${ref}`],
|
|
2916
|
-
why: "The pointer chain agents follow breaks at a file they can never read.",
|
|
2917
|
-
action: `Create ${ref} or remove the reference.`,
|
|
2918
|
-
effort: "S",
|
|
2919
|
-
confidence: "high"
|
|
2920
|
-
})
|
|
2921
|
-
);
|
|
2922
|
-
}
|
|
3202
|
+
findings.push(...(await checkDocRefs(env, file, LENS_ID4, counters)).findings);
|
|
2923
3203
|
}
|
|
2924
3204
|
const auditedPaths = new Set(files.map((f) => f.path));
|
|
2925
3205
|
const stateDocs = await listStateDocuments(root, facts);
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
let ledgerIds = null;
|
|
2929
|
-
const ledgerSources = [];
|
|
2930
|
-
if (stateDocs.length) {
|
|
2931
|
-
for (const artifact of facts.artifacts) {
|
|
2932
|
-
if (artifact.kind !== "decisions" || !artifact.exists) continue;
|
|
2933
|
-
const text = await readText(path2.join(root, artifact.path));
|
|
2934
|
-
if (text === null) continue;
|
|
2935
|
-
const entries = parseDecisionEntries(text);
|
|
2936
|
-
if (!entries.length) continue;
|
|
2937
|
-
ledgerIds ??= /* @__PURE__ */ new Set();
|
|
2938
|
-
for (const entry of entries) ledgerIds.add(entry.num);
|
|
2939
|
-
ledgerSources.push(artifact.path);
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
3206
|
+
const ledger = stateDocs.length ? await loadDecisionLedger(root, facts) : { ids: null, sources: [] };
|
|
3207
|
+
const ledgerSources = ledger.sources;
|
|
2942
3208
|
for (const doc of stateDocs) {
|
|
2943
3209
|
if (!auditedPaths.has(doc.path)) await auditClaims(doc);
|
|
2944
|
-
|
|
2945
|
-
qualifiedRefsSkipped += qualifiedSkipped;
|
|
2946
|
-
if (!refs.size) continue;
|
|
2947
|
-
if (!ledgerIds) {
|
|
2948
|
-
unresolvableRefs += refs.size;
|
|
2949
|
-
continue;
|
|
2950
|
-
}
|
|
2951
|
-
for (const [num, asWritten] of refs) {
|
|
2952
|
-
if (ledgerIds.has(num)) continue;
|
|
2953
|
-
findings.push(
|
|
2954
|
-
finding2({
|
|
2955
|
-
id: `${LENS_ID4}/dead-decision-ref:${doc.path}:${asWritten}`,
|
|
2956
|
-
tier: "gap",
|
|
2957
|
-
claim: `${doc.path} cites ${asWritten} \u2014 no such entry exists in ${ledgerSources.join(", ")}`,
|
|
2958
|
-
evidence: [doc.path, `${ledgerSources.join(", ")}: no ${asWritten} entry`],
|
|
2959
|
-
why: "A state doc is read as ground truth on return; a citation the decision record cannot back sends readers to a ruling that was never written.",
|
|
2960
|
-
action: "Fix the reference \u2014 or record the missing decision.",
|
|
2961
|
-
effort: "S",
|
|
2962
|
-
confidence: "medium"
|
|
2963
|
-
})
|
|
2964
|
-
);
|
|
2965
|
-
}
|
|
3210
|
+
findings.push(...checkDecisionRefs(doc, ledger, { lensId: LENS_ID4 }, counters).findings);
|
|
2966
3211
|
}
|
|
2967
3212
|
if (ctx.baseline) {
|
|
2968
3213
|
findings.push(
|
|
@@ -2985,29 +3230,34 @@ var instructionTruthLens = {
|
|
|
2985
3230
|
"No committed baseline (.etymd/baseline.json) \u2014 drift over time is not measurable; run `etymd init` to approve one."
|
|
2986
3231
|
);
|
|
2987
3232
|
}
|
|
2988
|
-
if (binaryResolved) {
|
|
3233
|
+
if (counters.binaryResolved) {
|
|
3234
|
+
disclosures.push(
|
|
3235
|
+
`${counters.binaryResolved} command claim(s) are installed binaries (node_modules/.bin), not package scripts \u2014 treated as true.`
|
|
3236
|
+
);
|
|
3237
|
+
}
|
|
3238
|
+
if (counters.unverifiableCommands) {
|
|
2989
3239
|
disclosures.push(
|
|
2990
|
-
|
|
3240
|
+
`node_modules is not installed \u2014 ${counters.unverifiableCommands} command claim(s) matching no package script could not be checked against installed binaries; skipped, not flagged.`
|
|
2991
3241
|
);
|
|
2992
3242
|
}
|
|
2993
|
-
if (
|
|
3243
|
+
if (counters.gitignoredSkipped) {
|
|
2994
3244
|
disclosures.push(
|
|
2995
|
-
|
|
3245
|
+
`${counters.gitignoredSkipped} missing path claim(s) are gitignored (machine-local, e.g. .env) \u2014 existence is not verifiable from the repo; skipped, not flagged.`
|
|
2996
3246
|
);
|
|
2997
3247
|
}
|
|
2998
|
-
if (
|
|
3248
|
+
if (counters.prospectiveSkipped) {
|
|
2999
3249
|
disclosures.push(
|
|
3000
|
-
`${
|
|
3250
|
+
`${counters.prospectiveSkipped} path claim(s) sit in create-this prose (the file instructs generating them) \u2014 forward-looking, not stale; skipped, not flagged.`
|
|
3001
3251
|
);
|
|
3002
3252
|
}
|
|
3003
|
-
if (
|
|
3253
|
+
if (counters.placeholderSkipped) {
|
|
3004
3254
|
disclosures.push(
|
|
3005
|
-
`${
|
|
3255
|
+
`${counters.placeholderSkipped} path claim(s) are naming stand-ins (e.g. \`my-custom-skill\`) rather than real references; skipped, not flagged.`
|
|
3006
3256
|
);
|
|
3007
3257
|
}
|
|
3008
|
-
if (
|
|
3258
|
+
if (counters.tildeSkipped) {
|
|
3009
3259
|
disclosures.push(
|
|
3010
|
-
`${
|
|
3260
|
+
`${counters.tildeSkipped} well-known doc mention(s) sit inside \`~/\` home paths (e.g. \`~/.claude/CLAUDE.md\`) \u2014 machine-global files, not this repo's; skipped, not flagged.`
|
|
3011
3261
|
);
|
|
3012
3262
|
}
|
|
3013
3263
|
if (stateDocs.length) {
|
|
@@ -3015,14 +3265,14 @@ var instructionTruthLens = {
|
|
|
3015
3265
|
`Checked ${stateDocs.length} state document(s) for command, path, and decision-reference claims (same skip classes as instruction files); decision ids resolved against ${ledgerSources.length ? ledgerSources.join(", ") : "nothing \u2014 no decisions file with D-NNN entries"}.`
|
|
3016
3266
|
);
|
|
3017
3267
|
}
|
|
3018
|
-
if (qualifiedRefsSkipped) {
|
|
3268
|
+
if (counters.qualifiedRefsSkipped) {
|
|
3019
3269
|
disclosures.push(
|
|
3020
|
-
`${qualifiedRefsSkipped} decision reference(s) name another record (e.g. a fleet-level ledger) \u2014 not claims about this repo's decisions; skipped, not flagged.`
|
|
3270
|
+
`${counters.qualifiedRefsSkipped} decision reference(s) name another record (e.g. a fleet-level ledger) \u2014 not claims about this repo's decisions; skipped, not flagged.`
|
|
3021
3271
|
);
|
|
3022
3272
|
}
|
|
3023
|
-
if (unresolvableRefs) {
|
|
3273
|
+
if (counters.unresolvableRefs) {
|
|
3024
3274
|
disclosures.push(
|
|
3025
|
-
`${unresolvableRefs} decision reference(s) could not be resolved \u2014 no decisions file with \`## D-NNN\` entries; skipped, not flagged.`
|
|
3275
|
+
`${counters.unresolvableRefs} decision reference(s) could not be resolved \u2014 no decisions file with \`## D-NNN\` entries; skipped, not flagged.`
|
|
3026
3276
|
);
|
|
3027
3277
|
}
|
|
3028
3278
|
if (excluded.length) {
|
|
@@ -3037,7 +3287,7 @@ var instructionTruthLens = {
|
|
|
3037
3287
|
);
|
|
3038
3288
|
}
|
|
3039
3289
|
disclosures.push(
|
|
3040
|
-
`Checked ${files.length} instruction file(s); commands resolved against root + ${facts.packages.length} workspace manifest(s) plus installed binaries; paths matched against root and package roots. Heuristics: workspace-filtered commands skipped (${
|
|
3290
|
+
`Checked ${files.length} instruction file(s); commands resolved against root + ${facts.packages.length} workspace manifest(s) plus installed binaries; paths matched against root and package roots. Heuristics: workspace-filtered commands skipped (${counters.filteredSkipped}); tokens without a recognized extension treated as prose (a dir claim needs a trailing slash); gitignored claims unverifiable; create-this and stand-in path claims skipped; absolute/globbed/placeholder tokens skipped; doc mentions inside \`~/\` home paths skipped; framework-pattern staleness not checked.`
|
|
3041
3291
|
);
|
|
3042
3292
|
return {
|
|
3043
3293
|
lens: LENS_ID4,
|
|
@@ -3470,7 +3720,7 @@ async function loadFleetManifest(manifestPath) {
|
|
|
3470
3720
|
init_config();
|
|
3471
3721
|
init_facts();
|
|
3472
3722
|
init_util();
|
|
3473
|
-
var
|
|
3723
|
+
var pExecFile3 = promisify(execFile);
|
|
3474
3724
|
var FLEET_LENS = "fleet-manifest";
|
|
3475
3725
|
var FLEET_JSON_SCHEMA = "fleet-experimental-0.2";
|
|
3476
3726
|
var CORP_WALL_ARTIFACTS = ["PROJECT_CONTEXT.md", "DECISIONS.md"];
|
|
@@ -3525,7 +3775,7 @@ function stateBudgetsFor(entry) {
|
|
|
3525
3775
|
async function gitGrepFiles(root, needle, mode = "fixed") {
|
|
3526
3776
|
try {
|
|
3527
3777
|
const matchFlag = mode === "regex" ? "-E" : "-F";
|
|
3528
|
-
const { stdout } = await
|
|
3778
|
+
const { stdout } = await pExecFile3("git", ["grep", "-I", "-l", matchFlag, "-e", needle], {
|
|
3529
3779
|
cwd: root,
|
|
3530
3780
|
timeout: 8e3
|
|
3531
3781
|
});
|
|
@@ -4075,8 +4325,273 @@ async function sweepFleet(manifest, opts = {}) {
|
|
|
4075
4325
|
};
|
|
4076
4326
|
}
|
|
4077
4327
|
|
|
4328
|
+
// src/engine/premise.ts
|
|
4329
|
+
init_facts();
|
|
4330
|
+
init_scan();
|
|
4331
|
+
init_util();
|
|
4332
|
+
var PREMISE_LENS = "premise";
|
|
4333
|
+
var PREMISE_BRIEF_FILE = path2.join(ETYMD_DIR, "premise-brief.md");
|
|
4334
|
+
var TASK_LABEL = "task";
|
|
4335
|
+
var MAX_PATH_FINDINGS = 15;
|
|
4336
|
+
var CODE_SPAN_RE = /(```[\s\S]*?```|`[^`\n]+`)/g;
|
|
4337
|
+
var RUN_RE = /\b(?:pnpm|yarn|npm|bun)\s+run\s+(?!-)([A-Za-z0-9:._-]+)|\bnpm\s+(?:test|start)\b/g;
|
|
4338
|
+
var BARE_PM_RE = /\b(?:pnpm|yarn|bun)\s+(?!run\b)(?!-)[A-Za-z0-9:._-]+/g;
|
|
4339
|
+
var STOP_WORDS = new Set(
|
|
4340
|
+
"a an the this that these those it its them all any both each every some your our my their and or but to in on at of for with from by as is are was were be will would can could should may might must do does did not no so if then than when where which who what how also again now first once twice after before via into over only just too very".split(" ")
|
|
4341
|
+
);
|
|
4342
|
+
var WRAP_RE = /^([([{"']*)(.*?)([.,;:!?)\]}"']*)$/;
|
|
4343
|
+
var TRAIL_RE = /[.,;:!?)\]}"']*$/;
|
|
4344
|
+
var HOSTNAME_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.([a-z]{2,})$/i;
|
|
4345
|
+
function promoteBareTokens(text, ctx) {
|
|
4346
|
+
const skips = {
|
|
4347
|
+
bareInvocations: 0,
|
|
4348
|
+
proseScripts: 0,
|
|
4349
|
+
hostnameLike: 0,
|
|
4350
|
+
unrootedDirs: 0
|
|
4351
|
+
};
|
|
4352
|
+
const promoted = text.split(CODE_SPAN_RE).map((segment, i) => i % 2 === 1 ? segment : promoteProse(segment, ctx, skips)).join("");
|
|
4353
|
+
return { text: promoted, skips };
|
|
4354
|
+
}
|
|
4355
|
+
function promoteProse(segment, ctx, skips) {
|
|
4356
|
+
skips.bareInvocations += segment.match(BARE_PM_RE)?.length ?? 0;
|
|
4357
|
+
const withCommands = segment.replace(RUN_RE, (m, script) => {
|
|
4358
|
+
const trail = TRAIL_RE.exec(m)?.[0] ?? "";
|
|
4359
|
+
const core = trail ? m.slice(0, -trail.length) : m;
|
|
4360
|
+
if (script !== void 0) {
|
|
4361
|
+
const name = script.replace(TRAIL_RE, "");
|
|
4362
|
+
if (name.length < 2 || STOP_WORDS.has(name.toLowerCase())) {
|
|
4363
|
+
skips.proseScripts += 1;
|
|
4364
|
+
return m;
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
return `\`${core}\`${trail}`;
|
|
4368
|
+
});
|
|
4369
|
+
return withCommands.replace(/[^\s`]+/g, (token) => {
|
|
4370
|
+
const m = WRAP_RE.exec(token);
|
|
4371
|
+
if (!m) return token;
|
|
4372
|
+
const [, lead = "", core = "", trail = ""] = m;
|
|
4373
|
+
if (!core || core.includes("://") || !PATH_TOKEN_RE.test(core)) return token;
|
|
4374
|
+
const first = core.slice(0, core.indexOf("/"));
|
|
4375
|
+
const hostSuffix = HOSTNAME_RE.exec(first)?.[1]?.toLowerCase();
|
|
4376
|
+
if (hostSuffix && !KNOWN_EXTENSIONS.has(hostSuffix)) {
|
|
4377
|
+
skips.hostnameLike += 1;
|
|
4378
|
+
return token;
|
|
4379
|
+
}
|
|
4380
|
+
if (core.endsWith("/")) {
|
|
4381
|
+
if (!ctx.rootedDirs.has(first)) {
|
|
4382
|
+
skips.unrootedDirs += 1;
|
|
4383
|
+
return token;
|
|
4384
|
+
}
|
|
4385
|
+
return `${lead}\`${core}\`${trail}`;
|
|
4386
|
+
}
|
|
4387
|
+
const ext = core.toLowerCase().match(/\.([a-z0-9]{1,8})$/)?.[1];
|
|
4388
|
+
if (!ext || !KNOWN_EXTENSIONS.has(ext)) return token;
|
|
4389
|
+
return `${lead}\`${core}\`${trail}`;
|
|
4390
|
+
});
|
|
4391
|
+
}
|
|
4392
|
+
async function listRootedDirs(env) {
|
|
4393
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
4394
|
+
for (const base of env.bases) {
|
|
4395
|
+
for (const sub of ["", "src", "scripts"]) {
|
|
4396
|
+
const entries = await promises.readdir(path2.join(base, sub), { withFileTypes: true }).catch(() => []);
|
|
4397
|
+
for (const entry of entries) if (entry.isDirectory()) dirs.add(entry.name);
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
return dirs;
|
|
4401
|
+
}
|
|
4402
|
+
async function runPremise(opts) {
|
|
4403
|
+
const root = opts.root;
|
|
4404
|
+
const task = opts.task.trim();
|
|
4405
|
+
const facts = await scanProject(root);
|
|
4406
|
+
const env = await buildTruthEnv(root, facts);
|
|
4407
|
+
const counters = emptyCounters();
|
|
4408
|
+
const promoted = promoteBareTokens(task, { rootedDirs: await listRootedDirs(env) });
|
|
4409
|
+
const text = { path: TASK_LABEL, text: promoted.text };
|
|
4410
|
+
const findings = [];
|
|
4411
|
+
const disclosures = [];
|
|
4412
|
+
const entities = [];
|
|
4413
|
+
const claims = await checkTextClaims(
|
|
4414
|
+
env,
|
|
4415
|
+
text,
|
|
4416
|
+
{
|
|
4417
|
+
lensId: PREMISE_LENS,
|
|
4418
|
+
subject: "The task",
|
|
4419
|
+
missingPathTier: "risk",
|
|
4420
|
+
maxPathFindings: MAX_PATH_FINDINGS,
|
|
4421
|
+
whyCommand: "The task is built on a command that does not exist \u2014 an agent will run it and fail, or quietly substitute something else and report success.",
|
|
4422
|
+
actionCommand: "Fix the task before handing it over: name the real script, or restore it.",
|
|
4423
|
+
whyPath: "The task is ABOUT this path. Acting on it means editing, testing, or reasoning about something that is not there \u2014 the wrong problem, solved precisely.",
|
|
4424
|
+
actionPath: "Find where the behaviour actually lives and rewrite the task to name it."
|
|
4425
|
+
},
|
|
4426
|
+
counters
|
|
4427
|
+
);
|
|
4428
|
+
findings.push(...claims.findings);
|
|
4429
|
+
disclosures.push(...claims.disclosures);
|
|
4430
|
+
entities.push(...claims.examined);
|
|
4431
|
+
const docs = await checkDocRefs(env, text, PREMISE_LENS, counters, "The task");
|
|
4432
|
+
findings.push(...docs.findings);
|
|
4433
|
+
entities.push(...docs.examined);
|
|
4434
|
+
const ledger = await loadDecisionLedger(root, facts);
|
|
4435
|
+
const decisions = checkDecisionRefs(
|
|
4436
|
+
text,
|
|
4437
|
+
ledger,
|
|
4438
|
+
{
|
|
4439
|
+
lensId: PREMISE_LENS,
|
|
4440
|
+
subject: "The task",
|
|
4441
|
+
why: "The task presupposes a ruling that was never recorded; an agent will act on a decision nobody made.",
|
|
4442
|
+
action: "Point the task at the entry that exists \u2014 or record the missing decision first."
|
|
4443
|
+
},
|
|
4444
|
+
counters
|
|
4445
|
+
);
|
|
4446
|
+
findings.push(...decisions.findings);
|
|
4447
|
+
entities.push(...decisions.examined);
|
|
4448
|
+
if (!entities.length) {
|
|
4449
|
+
disclosures.push(
|
|
4450
|
+
"The task names no path with a directory, no package script, no well-known doc, and no decision id \u2014 nothing etymd can check by reading the repo. Everything it presupposes is in the brief."
|
|
4451
|
+
);
|
|
4452
|
+
}
|
|
4453
|
+
disclosures.push(
|
|
4454
|
+
"In prose, a path needs a directory and a recognized extension (a directory claim needs a trailing slash and a first segment that exists here), and a script needs the `run` form (`npm run X`, `pnpm run X` \u2026) or the `npm test` / `npm start` shorthands; anything backticked is read exactly as an instruction file would be. A bare file name (a lone `config.ts`) is prose."
|
|
4455
|
+
);
|
|
4456
|
+
const { skips } = promoted;
|
|
4457
|
+
if (skips.bareInvocations) {
|
|
4458
|
+
disclosures.push(
|
|
4459
|
+
`${skips.bareInvocations} bare \`pnpm X\` / \`yarn X\` / \`bun X\` mention(s) in prose were not read as scripts \u2014 in a sentence that shape is a phrase as often as an invocation; write \`\u2026 run X\` or backtick it to have it checked.`
|
|
4460
|
+
);
|
|
4461
|
+
}
|
|
4462
|
+
if (skips.proseScripts) {
|
|
4463
|
+
disclosures.push(
|
|
4464
|
+
`${skips.proseScripts} \`\u2026 run X\` mention(s) put a function word or a one-letter stand-in where a script name would be; read as prose, skipped.`
|
|
4465
|
+
);
|
|
4466
|
+
}
|
|
4467
|
+
if (skips.hostnameLike) {
|
|
4468
|
+
disclosures.push(
|
|
4469
|
+
`${skips.hostnameLike} slash token(s) start with a host name (e.g. \`github.com/\u2026\`) \u2014 a URL, not a repo path; skipped.`
|
|
4470
|
+
);
|
|
4471
|
+
}
|
|
4472
|
+
if (skips.unrootedDirs) {
|
|
4473
|
+
disclosures.push(
|
|
4474
|
+
`${skips.unrootedDirs} slash-joined phrase(s) ending in \`/\` start with no directory that exists here (e.g. \`input/output/\`) \u2014 read as prose; skipped, not flagged. Backtick one to have it checked.`
|
|
4475
|
+
);
|
|
4476
|
+
}
|
|
4477
|
+
if (counters.unverifiableCommands) {
|
|
4478
|
+
disclosures.push(
|
|
4479
|
+
`node_modules is not installed \u2014 ${counters.unverifiableCommands} command(s) matching no package script could not be checked against installed binaries; skipped, not flagged.`
|
|
4480
|
+
);
|
|
4481
|
+
}
|
|
4482
|
+
if (counters.binaryResolved) {
|
|
4483
|
+
disclosures.push(
|
|
4484
|
+
`${counters.binaryResolved} command(s) resolve to installed binaries (node_modules/.bin), not package scripts \u2014 treated as true.`
|
|
4485
|
+
);
|
|
4486
|
+
}
|
|
4487
|
+
if (counters.gitignoredSkipped) {
|
|
4488
|
+
disclosures.push(
|
|
4489
|
+
`${counters.gitignoredSkipped} missing path(s) are gitignored (machine-local) \u2014 existence is not verifiable from the repo; skipped, not flagged.`
|
|
4490
|
+
);
|
|
4491
|
+
}
|
|
4492
|
+
if (counters.prospectiveSkipped) {
|
|
4493
|
+
disclosures.push(
|
|
4494
|
+
`${counters.prospectiveSkipped} path(s) sit in create-this prose (the task says to create them) \u2014 forward-looking, not missing; skipped.`
|
|
4495
|
+
);
|
|
4496
|
+
}
|
|
4497
|
+
if (counters.placeholderSkipped) {
|
|
4498
|
+
disclosures.push(
|
|
4499
|
+
`${counters.placeholderSkipped} path(s) are naming stand-ins (e.g. \`my-feature\`) rather than real references; skipped.`
|
|
4500
|
+
);
|
|
4501
|
+
}
|
|
4502
|
+
if (counters.filteredSkipped) {
|
|
4503
|
+
disclosures.push(
|
|
4504
|
+
`${counters.filteredSkipped} workspace-filtered command(s) (\`--filter\`, \`-C\` \u2026) were not resolved; skipped.`
|
|
4505
|
+
);
|
|
4506
|
+
}
|
|
4507
|
+
if (counters.tildeSkipped) {
|
|
4508
|
+
disclosures.push(
|
|
4509
|
+
`${counters.tildeSkipped} well-known doc mention(s) sit inside \`~/\` home paths (e.g. \`~/.claude/CLAUDE.md\`) \u2014 machine-global files, not this repo's; skipped, not flagged.`
|
|
4510
|
+
);
|
|
4511
|
+
}
|
|
4512
|
+
if (counters.qualifiedRefsSkipped) {
|
|
4513
|
+
disclosures.push(
|
|
4514
|
+
`${counters.qualifiedRefsSkipped} decision reference(s) name another record \u2014 not this repo's; skipped.`
|
|
4515
|
+
);
|
|
4516
|
+
}
|
|
4517
|
+
if (counters.unresolvableRefs) {
|
|
4518
|
+
disclosures.push(
|
|
4519
|
+
`${counters.unresolvableRefs} decision reference(s) could not be resolved \u2014 no decisions file with \`## D-NNN\` entries; skipped, not flagged.`
|
|
4520
|
+
);
|
|
4521
|
+
}
|
|
4522
|
+
disclosures.push(
|
|
4523
|
+
"What the task PRESUPPOSES \u2014 that the named things are the ones meant, that the mechanism it assumes actually runs, that the state it assumes holds \u2014 cannot be checked by reading files. The brief hands those to the agent."
|
|
4524
|
+
);
|
|
4525
|
+
disclosures.push("Task findings are not remembered between runs \u2014 no ledger is written.");
|
|
4526
|
+
const ranked = rankFindings(findings);
|
|
4527
|
+
const brief = renderBrief(task, ranked, entities);
|
|
4528
|
+
let briefPath = null;
|
|
4529
|
+
if (opts.writeBrief !== false && await pathExists(path2.join(root, ETYMD_DIR))) {
|
|
4530
|
+
await promises.writeFile(path2.join(root, PREMISE_BRIEF_FILE), brief, "utf8");
|
|
4531
|
+
briefPath = PREMISE_BRIEF_FILE;
|
|
4532
|
+
}
|
|
4533
|
+
return {
|
|
4534
|
+
schema: "premise/1",
|
|
4535
|
+
task,
|
|
4536
|
+
source: opts.source ?? "argument",
|
|
4537
|
+
name: facts.name,
|
|
4538
|
+
findings: ranked,
|
|
4539
|
+
disclosures,
|
|
4540
|
+
entities,
|
|
4541
|
+
brief,
|
|
4542
|
+
briefPath
|
|
4543
|
+
};
|
|
4544
|
+
}
|
|
4545
|
+
function mark(exists) {
|
|
4546
|
+
return exists === null ? "?" : exists ? "\u2713" : "\u2717";
|
|
4547
|
+
}
|
|
4548
|
+
function kindLabel(kind) {
|
|
4549
|
+
return kind === "script" ? "script " : kind === "decision" ? "decision " : "";
|
|
4550
|
+
}
|
|
4551
|
+
function renderBrief(task, findings, entities) {
|
|
4552
|
+
const oneLine = task.replace(/\s+/g, " ").trim();
|
|
4553
|
+
const title = oneLine.length > 100 ? `${oneLine.slice(0, 97)}\u2026` : oneLine;
|
|
4554
|
+
const checked = entities.length ? entities.map(
|
|
4555
|
+
(e) => `- ${mark(e.exists)} ${kindLabel(e.kind)}\`${e.value}\`${e.exists === null ? " \u2014 could not be checked" : e.exists ? " \u2014 exists" : " \u2014 missing (see the finding)"}`
|
|
4556
|
+
).join("\n") : "- (nothing the task names could be checked by reading the repo)";
|
|
4557
|
+
const failed = findings.filter((f) => f.tier === "risk");
|
|
4558
|
+
const failedLine = failed.length ? `
|
|
4559
|
+
**${failed.length} premise(s) already failed** \u2014 the task names things that do not exist. Resolve those before anything below.
|
|
4560
|
+
` : "";
|
|
4561
|
+
return `# Premise brief \u2014 ${title}
|
|
4562
|
+
|
|
4563
|
+
The task, verbatim:
|
|
4564
|
+
|
|
4565
|
+
> ${task.replace(/\n/g, "\n> ")}
|
|
4566
|
+
|
|
4567
|
+
etymd checked what the task names against the repo. What it could NOT check is listed under
|
|
4568
|
+
"only you can verify"; check those before acting, and if one fails, say so first \u2014 everything
|
|
4569
|
+
else is downstream of it.
|
|
4570
|
+
${failedLine}
|
|
4571
|
+
## What the task names, and whether it exists
|
|
4572
|
+
|
|
4573
|
+
${checked}
|
|
4574
|
+
|
|
4575
|
+
## Premises only you can verify
|
|
4576
|
+
|
|
4577
|
+
1. **The named things are the ones MEANT.** A file that exists can still be the wrong file, and a
|
|
4578
|
+
script that exists can still not be the one the task has in mind. Confirm each is where the
|
|
4579
|
+
behaviour actually lives before touching it.
|
|
4580
|
+
2. **The mechanism the task assumes is working.** A task shaped like "make X faster / less flaky /
|
|
4581
|
+
fresh after Y" assumes X and Y already do what it says. Run them once. If the assumed
|
|
4582
|
+
mechanism does not run, the task is downstream of a defect it does not mention.
|
|
4583
|
+
3. **The state the task assumes holds.** Read it; do not trust the task's description of it.
|
|
4584
|
+
|
|
4585
|
+
## If a premise fails
|
|
4586
|
+
|
|
4587
|
+
Report it first, in one line: what the task presupposes, and what you found instead. Do not
|
|
4588
|
+
answer the original task as if the premise held \u2014 a wrong problem solved precisely is still the
|
|
4589
|
+
wrong problem.
|
|
4590
|
+
`;
|
|
4591
|
+
}
|
|
4592
|
+
|
|
4078
4593
|
// src/index.ts
|
|
4079
4594
|
init_version();
|
|
4080
4595
|
init_version2();
|
|
4081
4596
|
|
|
4082
|
-
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 };
|
|
4597
|
+
export { CONFIG_FILE, DECISIONS_FORMAT_MARKER, DEFAULT_CONFIG, EXTRACTION_THRESHOLD, FLEET_JSON_SCHEMA, FLEET_LENS, LENSES, NAME, PACK_VERSION, PREMISE_BRIEF_FILE, PREMISE_LENS, TOTAL_BUDGET_WORDS, VERSION, applyFiles, baselineCarriesMachinePath, baselinePath, buildGateInventory, buildTruthEnv, cacheFactsPath, checkDecisionRefs, checkDocRefs, checkManifest, checkTextClaims, collectWallFindings, configPath, contextEconomyLens, corpPersistenceRoot, deriveProfile, emptyCounters, expandTilde, extractCommandClaims, extractPathClaims, gateIntegrityLens, instructionTruthLens, isAlwaysAppliedCursorRule, listInstructionFiles, loadDecisionLedger, loadFleetManifest, measureContext, planWorkflow, promoteBareTokens, rankFindings, readBaseline, readCachedFacts, readConfig, readLedger, reconcileLedger, renderBrief, runAudit, runPremise, scanProject, stateFreshnessLens, sweepFleet, withoutMachinePath, writeBaseline, writeCachedFacts, writeLedger };
|