etymd 0.14.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/dist/index.js CHANGED
@@ -30,8 +30,8 @@ var init_package = __esm({
30
30
  "package.json"() {
31
31
  package_default = {
32
32
  name: "etymd",
33
- version: "0.14.0",
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.",
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",
@@ -2324,6 +2324,8 @@ var gateIntegrityLens = {
2324
2324
  init_version();
2325
2325
  init_config();
2326
2326
  init_facts();
2327
+
2328
+ // src/lenses/instruction-truth/checks.ts
2327
2329
  init_util();
2328
2330
 
2329
2331
  // src/lenses/state-freshness.ts
@@ -2871,6 +2873,204 @@ function extractDocRefs(text) {
2871
2873
  return { refs, tildeSkipped };
2872
2874
  }
2873
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 };
3072
+ }
3073
+
2874
3074
  // src/lenses/instruction-truth/lens.ts
2875
3075
  var LENS_ID4 = "instruction-truth";
2876
3076
  var MAX_PATH_FINDINGS_PER_FILE = 15;
@@ -2965,99 +3165,17 @@ var instructionTruthLens = {
2965
3165
  })
2966
3166
  );
2967
3167
  }
2968
- const knownScripts = new Set(Object.keys(facts.commands.raw));
2969
- for (const pkg of facts.packages) {
2970
- const pkgJson = await readJson(
2971
- path2.join(root, pkg.dir, "package.json")
2972
- );
2973
- for (const key of Object.keys(pkgJson?.scripts ?? {})) knownScripts.add(key);
2974
- }
2975
- const pathResolves = async (claim) => {
2976
- const bases = [root, ...facts.packages.map((p) => path2.join(root, p.dir))];
2977
- for (const base of bases) {
2978
- if (await pathExists(path2.join(base, claim))) return true;
2979
- if (await pathExists(path2.join(base, "src", claim))) return true;
2980
- if (await pathExists(path2.join(base, "scripts", claim))) return true;
2981
- }
2982
- 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
2983
3174
  };
2984
- const binResolves = async (name) => {
2985
- const bases = [root, ...facts.packages.map((p) => path2.join(root, p.dir))];
2986
- for (const base of bases) {
2987
- if (await pathExists(path2.join(base, "node_modules", ".bin", name))) return true;
2988
- }
2989
- return false;
2990
- };
2991
- const nodeModulesInstalled = await pathExists(path2.join(root, "node_modules"));
2992
- const manifestExists = await pathExists(path2.join(root, "package.json")) || facts.packages.length > 0;
2993
- let totalFilteredSkipped = 0;
2994
- let totalTildeSkipped = 0;
2995
- let binaryResolved = 0;
2996
- let unverifiableCommands = 0;
2997
- let gitignoredSkipped = 0;
2998
- let prospectiveSkipped = 0;
2999
- let placeholderSkipped = 0;
3000
3175
  const auditClaims = async (file) => {
3001
- const { scripts: claimed, filteredSkipped } = extractCommandClaims(file.text);
3002
- totalFilteredSkipped += filteredSkipped;
3003
- for (const [script, raw] of claimed) {
3004
- if (knownScripts.has(script)) continue;
3005
- if (await binResolves(script)) {
3006
- binaryResolved += 1;
3007
- continue;
3008
- }
3009
- if (!nodeModulesInstalled && manifestExists) {
3010
- unverifiableCommands += 1;
3011
- continue;
3012
- }
3013
- findings.push(
3014
- finding2({
3015
- id: `${LENS_ID4}/stale-command:${file.path}:${script}`,
3016
- tier: "risk",
3017
- claim: `${file.path} tells agents to run \`${script}\` \u2014 no such script exists`,
3018
- evidence: [`${file.path}: \`${raw}\``, "package.json scripts (root + workspaces)"],
3019
- why: "An agent following this instruction runs a command that fails \u2014 or silently skips the check it was meant to run.",
3020
- action: "Update the instruction to the current script name (or restore the script).",
3021
- effort: "S",
3022
- confidence: "high"
3023
- })
3024
- );
3025
- }
3026
- const { paths, prospective, placeholder } = extractPathClaims(file.text);
3027
- prospectiveSkipped += prospective.length;
3028
- placeholderSkipped += placeholder.length;
3029
- const missing = [];
3030
- for (const claim of paths) {
3031
- if (!await pathResolves(claim)) missing.push(claim);
3032
- }
3033
- const ignoredOut = missing.length ? await git(root, ["check-ignore", ...missing]) : null;
3034
- const gitignored = new Set((ignoredOut ?? "").split("\n").filter(Boolean));
3035
- let pathFindings = 0;
3036
- for (const claim of missing) {
3037
- if (gitignored.has(claim)) {
3038
- gitignoredSkipped += 1;
3039
- continue;
3040
- }
3041
- if (pathFindings >= MAX_PATH_FINDINGS_PER_FILE) {
3042
- disclosures.push(
3043
- `${file.path}: more than ${MAX_PATH_FINDINGS_PER_FILE} missing-path claims \u2014 truncated.`
3044
- );
3045
- break;
3046
- }
3047
- pathFindings += 1;
3048
- findings.push(
3049
- finding2({
3050
- id: `${LENS_ID4}/stale-path:${file.path}:${claim}`,
3051
- tier: "gap",
3052
- claim: `${file.path} references \`${claim}\` \u2014 it does not exist in the repo`,
3053
- evidence: [file.path, `missing: ${claim}`],
3054
- why: "Agents navigate by these references; a dead path wastes a lookup and erodes trust in the rest of the file.",
3055
- action: "Fix or remove the reference.",
3056
- effort: "S",
3057
- confidence: "medium"
3058
- })
3059
- );
3060
- }
3176
+ const result = await checkTextClaims(env, file, claimOpts, counters);
3177
+ findings.push(...result.findings);
3178
+ disclosures.push(...result.disclosures);
3061
3179
  };
3062
3180
  for (const file of files) {
3063
3181
  await auditClaims(file);
@@ -3081,66 +3199,15 @@ var instructionTruthLens = {
3081
3199
  break;
3082
3200
  }
3083
3201
  }
3084
- const { refs: docRefs, tildeSkipped } = extractDocRefs(file.text);
3085
- totalTildeSkipped += tildeSkipped;
3086
- for (const ref of docRefs) {
3087
- if (await pathExists(path2.join(root, ref))) continue;
3088
- findings.push(
3089
- finding2({
3090
- id: `${LENS_ID4}/dangling-ref:${file.path}:${ref}`,
3091
- tier: "gap",
3092
- claim: `${file.path} references ${ref} \u2014 no such file exists`,
3093
- evidence: [file.path, `missing: ${ref}`],
3094
- why: "The pointer chain agents follow breaks at a file they can never read.",
3095
- action: `Create ${ref} or remove the reference.`,
3096
- effort: "S",
3097
- confidence: "high"
3098
- })
3099
- );
3100
- }
3202
+ findings.push(...(await checkDocRefs(env, file, LENS_ID4, counters)).findings);
3101
3203
  }
3102
3204
  const auditedPaths = new Set(files.map((f) => f.path));
3103
3205
  const stateDocs = await listStateDocuments(root, facts);
3104
- let qualifiedRefsSkipped = 0;
3105
- let unresolvableRefs = 0;
3106
- let ledgerIds = null;
3107
- const ledgerSources = [];
3108
- if (stateDocs.length) {
3109
- for (const artifact of facts.artifacts) {
3110
- if (artifact.kind !== "decisions" || !artifact.exists) continue;
3111
- const text = await readText(path2.join(root, artifact.path));
3112
- if (text === null) continue;
3113
- const entries = parseDecisionEntries(text);
3114
- if (!entries.length) continue;
3115
- ledgerIds ??= /* @__PURE__ */ new Set();
3116
- for (const entry of entries) ledgerIds.add(entry.num);
3117
- ledgerSources.push(artifact.path);
3118
- }
3119
- }
3206
+ const ledger = stateDocs.length ? await loadDecisionLedger(root, facts) : { ids: null, sources: [] };
3207
+ const ledgerSources = ledger.sources;
3120
3208
  for (const doc of stateDocs) {
3121
3209
  if (!auditedPaths.has(doc.path)) await auditClaims(doc);
3122
- const { refs, qualifiedSkipped } = extractDecisionRefs(doc.text);
3123
- qualifiedRefsSkipped += qualifiedSkipped;
3124
- if (!refs.size) continue;
3125
- if (!ledgerIds) {
3126
- unresolvableRefs += refs.size;
3127
- continue;
3128
- }
3129
- for (const [num, asWritten] of refs) {
3130
- if (ledgerIds.has(num)) continue;
3131
- findings.push(
3132
- finding2({
3133
- id: `${LENS_ID4}/dead-decision-ref:${doc.path}:${asWritten}`,
3134
- tier: "gap",
3135
- claim: `${doc.path} cites ${asWritten} \u2014 no such entry exists in ${ledgerSources.join(", ")}`,
3136
- evidence: [doc.path, `${ledgerSources.join(", ")}: no ${asWritten} entry`],
3137
- 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.",
3138
- action: "Fix the reference \u2014 or record the missing decision.",
3139
- effort: "S",
3140
- confidence: "medium"
3141
- })
3142
- );
3143
- }
3210
+ findings.push(...checkDecisionRefs(doc, ledger, { lensId: LENS_ID4 }, counters).findings);
3144
3211
  }
3145
3212
  if (ctx.baseline) {
3146
3213
  findings.push(
@@ -3163,34 +3230,34 @@ var instructionTruthLens = {
3163
3230
  "No committed baseline (.etymd/baseline.json) \u2014 drift over time is not measurable; run `etymd init` to approve one."
3164
3231
  );
3165
3232
  }
3166
- if (binaryResolved) {
3233
+ if (counters.binaryResolved) {
3167
3234
  disclosures.push(
3168
- `${binaryResolved} command claim(s) are installed binaries (node_modules/.bin), not package scripts \u2014 treated as true.`
3235
+ `${counters.binaryResolved} command claim(s) are installed binaries (node_modules/.bin), not package scripts \u2014 treated as true.`
3169
3236
  );
3170
3237
  }
3171
- if (unverifiableCommands) {
3238
+ if (counters.unverifiableCommands) {
3172
3239
  disclosures.push(
3173
- `node_modules is not installed \u2014 ${unverifiableCommands} command claim(s) matching no package script could not be checked against installed binaries; skipped, not flagged.`
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.`
3174
3241
  );
3175
3242
  }
3176
- if (gitignoredSkipped) {
3243
+ if (counters.gitignoredSkipped) {
3177
3244
  disclosures.push(
3178
- `${gitignoredSkipped} missing path claim(s) are gitignored (machine-local, e.g. .env) \u2014 existence is not verifiable from the repo; skipped, not flagged.`
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.`
3179
3246
  );
3180
3247
  }
3181
- if (prospectiveSkipped) {
3248
+ if (counters.prospectiveSkipped) {
3182
3249
  disclosures.push(
3183
- `${prospectiveSkipped} path claim(s) sit in create-this prose (the file instructs generating them) \u2014 forward-looking, not stale; skipped, not flagged.`
3250
+ `${counters.prospectiveSkipped} path claim(s) sit in create-this prose (the file instructs generating them) \u2014 forward-looking, not stale; skipped, not flagged.`
3184
3251
  );
3185
3252
  }
3186
- if (placeholderSkipped) {
3253
+ if (counters.placeholderSkipped) {
3187
3254
  disclosures.push(
3188
- `${placeholderSkipped} path claim(s) are naming stand-ins (e.g. \`my-custom-skill\`) rather than real references; skipped, not flagged.`
3255
+ `${counters.placeholderSkipped} path claim(s) are naming stand-ins (e.g. \`my-custom-skill\`) rather than real references; skipped, not flagged.`
3189
3256
  );
3190
3257
  }
3191
- if (totalTildeSkipped) {
3258
+ if (counters.tildeSkipped) {
3192
3259
  disclosures.push(
3193
- `${totalTildeSkipped} 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.`
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.`
3194
3261
  );
3195
3262
  }
3196
3263
  if (stateDocs.length) {
@@ -3198,14 +3265,14 @@ var instructionTruthLens = {
3198
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"}.`
3199
3266
  );
3200
3267
  }
3201
- if (qualifiedRefsSkipped) {
3268
+ if (counters.qualifiedRefsSkipped) {
3202
3269
  disclosures.push(
3203
- `${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.`
3204
3271
  );
3205
3272
  }
3206
- if (unresolvableRefs) {
3273
+ if (counters.unresolvableRefs) {
3207
3274
  disclosures.push(
3208
- `${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.`
3209
3276
  );
3210
3277
  }
3211
3278
  if (excluded.length) {
@@ -3220,7 +3287,7 @@ var instructionTruthLens = {
3220
3287
  );
3221
3288
  }
3222
3289
  disclosures.push(
3223
- `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 (${totalFilteredSkipped}); 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.`
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.`
3224
3291
  );
3225
3292
  return {
3226
3293
  lens: LENS_ID4,
@@ -4258,8 +4325,273 @@ async function sweepFleet(manifest, opts = {}) {
4258
4325
  };
4259
4326
  }
4260
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
+
4261
4593
  // src/index.ts
4262
4594
  init_version();
4263
4595
  init_version2();
4264
4596
 
4265
- 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 };