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/CHANGELOG.md +14 -0
- package/README.md +30 -19
- package/dist/{approve-QDOH2E7V.js → approve-MA4Z3TBT.js} +2 -2
- package/dist/{audit-42OHF7ON.js → audit-YSALDC2L.js} +5 -4
- package/dist/{brief-ZUQK3CFL.js → brief-DPZYSAMC.js} +2 -2
- package/dist/{chunk-LT7JMVV3.js → chunk-2RNQ6OLV.js} +43 -730
- package/dist/{chunk-PAMWS665.js → chunk-CEO3BXQB.js} +2 -1
- package/dist/{chunk-623W6KVC.js → chunk-DBWDMIYO.js} +1 -1
- package/dist/chunk-IWG77WV3.js +758 -0
- package/dist/{chunk-HWC6PKJM.js → chunk-Y6RZRED3.js} +1 -1
- package/dist/cli.js +35 -14
- package/dist/{doctor-CE5SD4JW.js → doctor-Y3DWDEBT.js} +5 -4
- package/dist/{fleet-F3XDEDCV.js → fleet-4FYZ3GBK.js} +5 -4
- package/dist/{gates-F4YPKWJR.js → gates-IBQ7HCAN.js} +2 -2
- package/dist/index.d.ts +164 -1
- package/dist/index.js +498 -166
- package/dist/{init-AHA7VRRI.js → init-H57H2JBE.js} +2 -2
- package/dist/premise-VMC3UUIB.js +333 -0
- package/dist/scan-W4US23MU.js +5 -0
- package/dist/{scan-TVX3FKK4.js → scan-XBOHDXAH.js} +2 -2
- package/package.json +2 -2
- package/dist/scan-QRTH6HAP.js +0 -5
|
@@ -1,32 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { stateFreshnessLens, rankFindings, listInstructionFiles, buildTruthEnv, emptyCounters, packageManagerUsage, checkDocRefs, listStateDocuments, loadDecisionLedger, checkDecisionRefs, checkTextClaims } from './chunk-IWG77WV3.js';
|
|
2
3
|
import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-3E2IPCRY.js';
|
|
3
4
|
import { measureContext, contextFileLabel } from './chunk-HI7NWPRA.js';
|
|
4
|
-
import { scanProject
|
|
5
|
+
import { scanProject } from './chunk-Y6RZRED3.js';
|
|
5
6
|
import { DEFAULT_CONFIG, ETYMD_DIR, writeCachedFacts, readBaseline, readConfig, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE, CONFIG_FILE } from './chunk-P6ATKV2R.js';
|
|
6
7
|
import { PACK_VERSION } from './chunk-YQZDYDAK.js';
|
|
7
|
-
import { pathExists,
|
|
8
|
+
import { pathExists, readText, readJson, isCiEnvironment, isExecutable } from './chunk-4VPBP6K6.js';
|
|
8
9
|
import path from 'path';
|
|
9
10
|
import YAML from 'yaml';
|
|
10
11
|
import { execFile } from 'child_process';
|
|
11
12
|
import { promisify } from 'util';
|
|
12
|
-
import { promises } from 'fs';
|
|
13
|
-
|
|
14
|
-
// src/engine/finding.ts
|
|
15
|
-
var TIER_ORDER = { risk: 0, gap: 1, polish: 2 };
|
|
16
|
-
var EFFORT_ORDER = { S: 0, M: 1, L: 2 };
|
|
17
|
-
function parseFailOnTier(value) {
|
|
18
|
-
if (value === "risk" || value === "gap" || value === "polish") return value;
|
|
19
|
-
throw new Error(`--fail-on must be risk|gap|polish, got \`${value}\``);
|
|
20
|
-
}
|
|
21
|
-
function meetsFailOn(findings, failOn) {
|
|
22
|
-
const threshold = TIER_ORDER[failOn];
|
|
23
|
-
return findings.some((f) => TIER_ORDER[f.tier] <= threshold);
|
|
24
|
-
}
|
|
25
|
-
function rankFindings(findings) {
|
|
26
|
-
return [...findings].sort(
|
|
27
|
-
(a, b) => TIER_ORDER[a.tier] - TIER_ORDER[b.tier] || EFFORT_ORDER[a.effort] - EFFORT_ORDER[b.effort]
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
13
|
|
|
31
14
|
// src/lenses/context-economy.ts
|
|
32
15
|
var LENS_ID = "context-economy";
|
|
@@ -760,549 +743,12 @@ var gateIntegrityLens = {
|
|
|
760
743
|
};
|
|
761
744
|
}
|
|
762
745
|
};
|
|
763
|
-
var LENS_ID3 = "state-freshness";
|
|
764
|
-
var DECISIONS_FORMAT_MARKER = "<!-- decisions-format: 1 -->";
|
|
765
|
-
var KNOWN_FORMAT_VERSION = 1;
|
|
766
|
-
var MARKER_RE = /<!--\s*decisions-format:\s*(\d+)([^>]*?)-->/;
|
|
767
|
-
var FIELD_NAME_RE = /^[A-Za-z0-9 _-]+$/;
|
|
768
|
-
var BUILT_IN_FIELDS = /* @__PURE__ */ new Set(["scope"]);
|
|
769
|
-
var MS_PER_DAY = 864e5;
|
|
770
|
-
function parseDecisionsFormat(text) {
|
|
771
|
-
const m = MARKER_RE.exec(text);
|
|
772
|
-
if (!m) return null;
|
|
773
|
-
const problems = [];
|
|
774
|
-
const fields = [];
|
|
775
|
-
const offset = m.index ?? 0;
|
|
776
|
-
const version = Number(m[1]);
|
|
777
|
-
if (version !== KNOWN_FORMAT_VERSION) {
|
|
778
|
-
problems.push(
|
|
779
|
-
`declares decisions-format version ${version}; this etymd understands version ${KNOWN_FORMAT_VERSION} \u2014 checked as version ${KNOWN_FORMAT_VERSION}.`
|
|
780
|
-
);
|
|
781
|
-
}
|
|
782
|
-
const attrs = (m[2] ?? "").trim();
|
|
783
|
-
if (!attrs) return { fields, offset, problems };
|
|
784
|
-
const declared = /^fields=(.*)$/.exec(attrs);
|
|
785
|
-
if (!declared) {
|
|
786
|
-
problems.push(`marker attribute \`${attrs}\` is not understood \u2014 ignored (only \`fields=\`).`);
|
|
787
|
-
return { fields, offset, problems };
|
|
788
|
-
}
|
|
789
|
-
const seen = new Set(BUILT_IN_FIELDS);
|
|
790
|
-
for (const raw of declared[1].split(",")) {
|
|
791
|
-
const name = raw.trim();
|
|
792
|
-
if (!name) continue;
|
|
793
|
-
if (!FIELD_NAME_RE.test(name)) {
|
|
794
|
-
problems.push(
|
|
795
|
-
`declared field \`${name}\` is not a usable field name (letters, digits, spaces, \`-\`, \`_\`) \u2014 not checked.`
|
|
796
|
-
);
|
|
797
|
-
continue;
|
|
798
|
-
}
|
|
799
|
-
const key = name.toLowerCase();
|
|
800
|
-
if (seen.has(key)) continue;
|
|
801
|
-
seen.add(key);
|
|
802
|
-
fields.push(name);
|
|
803
|
-
}
|
|
804
|
-
if (fields.length === 0 && problems.length === 0) {
|
|
805
|
-
problems.push("marker declares `fields=` with no field names \u2014 no extra fields checked.");
|
|
806
|
-
}
|
|
807
|
-
return { fields, offset, problems };
|
|
808
|
-
}
|
|
809
|
-
function hasField(block, name) {
|
|
810
|
-
return new RegExp(`${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s*]*:`).test(block);
|
|
811
|
-
}
|
|
812
|
-
function parseDecisionEntries(text) {
|
|
813
|
-
const headings = [...text.matchAll(/^## .*$/gm)];
|
|
814
|
-
const entries = [];
|
|
815
|
-
for (let i = 0; i < headings.length; i++) {
|
|
816
|
-
const h = headings[i];
|
|
817
|
-
const m = /^## (D-(\d+))\b/.exec(h[0]);
|
|
818
|
-
if (!m) continue;
|
|
819
|
-
const offset = h.index ?? 0;
|
|
820
|
-
const start = offset + h[0].length;
|
|
821
|
-
const end = i + 1 < headings.length ? headings[i + 1].index : void 0;
|
|
822
|
-
entries.push({ id: m[1], num: Number(m[2]), block: text.slice(start, end), offset });
|
|
823
|
-
}
|
|
824
|
-
return entries;
|
|
825
|
-
}
|
|
826
|
-
function checkIdSequence(file, entries) {
|
|
827
|
-
const findings = [];
|
|
828
|
-
const nextFree = Math.max(0, ...entries.map((e) => e.num)) + 1;
|
|
829
|
-
const seen = /* @__PURE__ */ new Map();
|
|
830
|
-
const duplicated = /* @__PURE__ */ new Set();
|
|
831
|
-
let prev;
|
|
832
|
-
for (const entry of entries) {
|
|
833
|
-
if (seen.has(entry.num)) {
|
|
834
|
-
if (!duplicated.has(entry.num)) {
|
|
835
|
-
duplicated.add(entry.num);
|
|
836
|
-
findings.push({
|
|
837
|
-
id: `${LENS_ID3}/duplicate-id:${file}:${entry.id}`,
|
|
838
|
-
lens: LENS_ID3,
|
|
839
|
-
tier: "gap",
|
|
840
|
-
claim: `${file} carries more than one ${entry.id} entry`,
|
|
841
|
-
evidence: [`${file}: ${entry.id} appears twice`],
|
|
842
|
-
why: "Two decisions under one id cannot be cited, superseded, or dismissed unambiguously \u2014 append races collide exactly here.",
|
|
843
|
-
action: `Rename the later entry to D-${String(nextFree).padStart(3, "0")} (the next free id).`,
|
|
844
|
-
effort: "S",
|
|
845
|
-
confidence: "high"
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
} else {
|
|
849
|
-
seen.set(entry.num, entry);
|
|
850
|
-
if (prev && entry.num < prev.num) {
|
|
851
|
-
findings.push({
|
|
852
|
-
id: `${LENS_ID3}/id-order:${file}:${entry.id}`,
|
|
853
|
-
lens: LENS_ID3,
|
|
854
|
-
tier: "gap",
|
|
855
|
-
claim: `${file} lists ${entry.id} after ${prev.id} \u2014 ids out of append order`,
|
|
856
|
-
evidence: [`${file}: ${prev.id} precedes ${entry.id}`],
|
|
857
|
-
why: "An append-only record reads in id order; out-of-order ids make the newest decision hard to find and the next id hard to pick.",
|
|
858
|
-
action: `Rename the out-of-order entry into sequence (next free id: D-${String(nextFree).padStart(3, "0")}).`,
|
|
859
|
-
effort: "S",
|
|
860
|
-
confidence: "high"
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
|
-
prev = entry;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
return findings;
|
|
867
|
-
}
|
|
868
|
-
function checkFormatFields(file, entries, today, declaredFields, markerOffset) {
|
|
869
|
-
const findings = [];
|
|
870
|
-
for (const entry of entries) {
|
|
871
|
-
const bound = entry.offset >= markerOffset;
|
|
872
|
-
for (const field of bound ? declaredFields : []) {
|
|
873
|
-
if (hasField(entry.block, field)) continue;
|
|
874
|
-
findings.push({
|
|
875
|
-
id: `${LENS_ID3}/field-missing:${file}:${entry.id}:${field}`,
|
|
876
|
-
lens: LENS_ID3,
|
|
877
|
-
tier: "gap",
|
|
878
|
-
claim: `${file} ${entry.id} has no ${field}: field`,
|
|
879
|
-
evidence: [`${file}: ${entry.id}`, `${file} marker declares required field \`${field}\``],
|
|
880
|
-
why: "The file declares this field required on every entry after the marker; whatever reads the record for it finds nothing here.",
|
|
881
|
-
action: `Add a ${field}: line to ${entry.id}.`,
|
|
882
|
-
effort: "S",
|
|
883
|
-
confidence: "high"
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
if (bound && !/Scope[\s*]*:/.test(entry.block)) {
|
|
887
|
-
findings.push({
|
|
888
|
-
id: `${LENS_ID3}/scope-missing:${file}:${entry.id}`,
|
|
889
|
-
lens: LENS_ID3,
|
|
890
|
-
tier: "gap",
|
|
891
|
-
claim: `${file} ${entry.id} has no Scope: field`,
|
|
892
|
-
evidence: [`${file}: ${entry.id}`],
|
|
893
|
-
why: "A decision without a scope binds nobody \u2014 a reader cannot tell whether it covers the project they are working in.",
|
|
894
|
-
action: "Add a Scope: line naming what the decision binds.",
|
|
895
|
-
effort: "S",
|
|
896
|
-
confidence: "high"
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
const revisit = /Revisit[\s*]*:[\s*]*(\d{4}-\d{2}-\d{2})/.exec(entry.block);
|
|
900
|
-
if (revisit && revisit[1] < today) {
|
|
901
|
-
findings.push({
|
|
902
|
-
id: `${LENS_ID3}/revisit-due:${file}:${entry.id}`,
|
|
903
|
-
lens: LENS_ID3,
|
|
904
|
-
tier: "gap",
|
|
905
|
-
claim: `${file} ${entry.id} was due for revisit on ${revisit[1]}`,
|
|
906
|
-
evidence: [`${file}: ${entry.id} Revisit: ${revisit[1]}`],
|
|
907
|
-
why: "Review debt is due \u2014 a Revisit date is a promise to re-evaluate, and a past one silently hardens into policy.",
|
|
908
|
-
action: "Re-evaluate the decision: supersede it or move the Revisit date.",
|
|
909
|
-
effort: "S",
|
|
910
|
-
confidence: "high"
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
return findings;
|
|
915
|
-
}
|
|
916
|
-
var stateFreshnessLens = {
|
|
917
|
-
id: LENS_ID3,
|
|
918
|
-
version: "1",
|
|
919
|
-
title: "State freshness",
|
|
920
|
-
kind: "truth",
|
|
921
|
-
async run(ctx) {
|
|
922
|
-
const budgets = ctx.config?.config.state ?? DEFAULT_CONFIG.state;
|
|
923
|
-
const findings = [];
|
|
924
|
-
const disclosures = [...ctx.config?.problems ?? []];
|
|
925
|
-
const outOfScope = [];
|
|
926
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
927
|
-
const stateArtifacts = ctx.facts.artifacts.filter((a) => a.kind === "state" && a.exists);
|
|
928
|
-
const decisionArtifacts = ctx.facts.artifacts.filter((a) => a.kind === "decisions" && a.exists);
|
|
929
|
-
if (stateArtifacts.length === 0 && decisionArtifacts.length === 0) {
|
|
930
|
-
disclosures.push("No state or decisions artifacts detected \u2014 nothing to check.");
|
|
931
|
-
}
|
|
932
|
-
const freshness = ctx.facts.freshness;
|
|
933
|
-
if (!freshness) {
|
|
934
|
-
disclosures.push("Scan carried no freshness facts \u2014 staleness unchecked.");
|
|
935
|
-
} else {
|
|
936
|
-
for (const u of freshness.unverifiable) {
|
|
937
|
-
disclosures.push(`Freshness of ${u.path} is unverifiable (${u.reason}) \u2014 not flagged.`);
|
|
938
|
-
}
|
|
939
|
-
for (const a of stateArtifacts) {
|
|
940
|
-
const fact = freshness.artifacts.find((f) => f.artifactId === a.id);
|
|
941
|
-
if (!fact || !freshness.repoLastCommit) continue;
|
|
942
|
-
if (fact.dirty) {
|
|
943
|
-
disclosures.push(
|
|
944
|
-
`${a.path} has uncommitted changes \u2014 modified since its last commit; treated fresh-now, not flagged.`
|
|
945
|
-
);
|
|
946
|
-
continue;
|
|
947
|
-
}
|
|
948
|
-
if (!fact.commitsSince) continue;
|
|
949
|
-
const gapDays = Math.floor(
|
|
950
|
-
(Date.parse(freshness.repoLastCommit) - Date.parse(fact.lastCommit)) / MS_PER_DAY
|
|
951
|
-
);
|
|
952
|
-
if (gapDays <= budgets.staleAfterDays) continue;
|
|
953
|
-
const escalated = gapDays > budgets.staleAfterDays * 3;
|
|
954
|
-
findings.push({
|
|
955
|
-
id: `${LENS_ID3}/stale-state:${a.path}`,
|
|
956
|
-
lens: LENS_ID3,
|
|
957
|
-
tier: escalated ? "risk" : "gap",
|
|
958
|
-
claim: `${a.path} trails the repo by ${gapDays} days of commit traffic \u2014 it says "now" but the repo moved on`,
|
|
959
|
-
evidence: [
|
|
960
|
-
`${a.path} last commit: ${fact.lastCommit}`,
|
|
961
|
-
`repo last commit: ${freshness.repoLastCommit}`
|
|
962
|
-
],
|
|
963
|
-
why: escalated ? `Over three times the ${budgets.staleAfterDays}-day threshold while commits kept landing \u2014 every session starts from a picture of the project that is no longer true.` : `A state doc more than ${budgets.staleAfterDays} days behind continued commit traffic misleads every session that loads it.`,
|
|
964
|
-
action: "Refresh the state doc (or record why it is still current).",
|
|
965
|
-
effort: "S",
|
|
966
|
-
confidence: "high"
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
for (const a of stateArtifacts) {
|
|
971
|
-
const text = await readText(path.join(ctx.root, a.path));
|
|
972
|
-
if (text === null) {
|
|
973
|
-
disclosures.push(`${a.path} could not be read \u2014 unexamined, not clean.`);
|
|
974
|
-
outOfScope.push(a.path);
|
|
975
|
-
continue;
|
|
976
|
-
}
|
|
977
|
-
if (text.length > budgets.maxChars) {
|
|
978
|
-
findings.push({
|
|
979
|
-
id: `${LENS_ID3}/state-over-budget:${a.path}`,
|
|
980
|
-
lens: LENS_ID3,
|
|
981
|
-
tier: "gap",
|
|
982
|
-
claim: `${a.path} is ${text.length} chars \u2014 over the ${budgets.maxChars}-char state budget`,
|
|
983
|
-
evidence: [`${a.path}: ${text.length} chars`],
|
|
984
|
-
why: "Session-injection hooks truncate state around 10,000 chars \u2014 an over-budget state doc gets cut mid-sentence, and every session pays its full weight before the task begins.",
|
|
985
|
-
action: "Trim to budget; move overflow into decisions/docs and keep a pointer.",
|
|
986
|
-
effort: "M",
|
|
987
|
-
confidence: "high"
|
|
988
|
-
});
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
for (const a of decisionArtifacts) {
|
|
992
|
-
const text = await readText(path.join(ctx.root, a.path));
|
|
993
|
-
if (text === null) {
|
|
994
|
-
disclosures.push(
|
|
995
|
-
`${a.path} recognized as a decisions convention (directory) \u2014 age-exempt; per-entry format checks apply only to marker-carrying decisions files.`
|
|
996
|
-
);
|
|
997
|
-
continue;
|
|
998
|
-
}
|
|
999
|
-
const entries = parseDecisionEntries(text);
|
|
1000
|
-
findings.push(...checkIdSequence(a.path, entries));
|
|
1001
|
-
const format = parseDecisionsFormat(text);
|
|
1002
|
-
if (!format) {
|
|
1003
|
-
disclosures.push(
|
|
1004
|
-
`${a.path} carries no \`${DECISIONS_FORMAT_MARKER}\` marker \u2014 format checks skipped (forward-only, never retroactive); id-sequence checks still ran.`
|
|
1005
|
-
);
|
|
1006
|
-
outOfScope.push(a.path);
|
|
1007
|
-
continue;
|
|
1008
|
-
}
|
|
1009
|
-
for (const problem of format.problems) disclosures.push(`${a.path}: ${problem}`);
|
|
1010
|
-
const exempt = entries.filter((e) => e.offset < format.offset);
|
|
1011
|
-
if (format.fields.length > 0) {
|
|
1012
|
-
disclosures.push(
|
|
1013
|
-
`${a.path} declares required entry fields: ${format.fields.join(", ")} \u2014 checked on every entry at or after the marker (etymd attaches no meaning to the names).`
|
|
1014
|
-
);
|
|
1015
|
-
}
|
|
1016
|
-
if (exempt.length > 0) {
|
|
1017
|
-
disclosures.push(
|
|
1018
|
-
`${a.path}: ${exempt.length} entr${exempt.length === 1 ? "y" : "ies"} precede the format marker (${exempt[0]?.id}\u2026${exempt[exempt.length - 1]?.id}) \u2014 field presence not checked there (forward-only from the marker's position).`
|
|
1019
|
-
);
|
|
1020
|
-
}
|
|
1021
|
-
findings.push(...checkFormatFields(a.path, entries, today, format.fields, format.offset));
|
|
1022
|
-
}
|
|
1023
|
-
disclosures.push(
|
|
1024
|
-
`Thresholds: staleAfterDays ${budgets.staleAfterDays} (3x escalates to risk), state budget ${budgets.maxChars} chars (${budgets.staleAfterDays === DEFAULT_CONFIG.state.staleAfterDays && budgets.maxChars === DEFAULT_CONFIG.state.maxChars ? `defaults \u2014 override under \`state\` in ${CONFIG_FILE}` : `set in ${CONFIG_FILE}`}). Decisions artifacts are exempt from age \u2014 old decisions are history, not defects.`
|
|
1025
|
-
);
|
|
1026
|
-
return {
|
|
1027
|
-
lens: LENS_ID3,
|
|
1028
|
-
version: "1",
|
|
1029
|
-
title: "State freshness",
|
|
1030
|
-
kind: "truth",
|
|
1031
|
-
status: "ran",
|
|
1032
|
-
disclosures,
|
|
1033
|
-
findings,
|
|
1034
|
-
...outOfScope.length ? { outOfScope } : {}
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
1037
|
-
};
|
|
1038
|
-
async function listInstructionFiles(root, facts, scope) {
|
|
1039
|
-
const files = [];
|
|
1040
|
-
const add = async (rel) => {
|
|
1041
|
-
const text = await readText(path.join(root, rel));
|
|
1042
|
-
if (text !== null) files.push({ path: normalizeRelPath(rel), text });
|
|
1043
|
-
};
|
|
1044
|
-
const singleFileArtifacts = [
|
|
1045
|
-
"agents",
|
|
1046
|
-
"claude",
|
|
1047
|
-
"gemini",
|
|
1048
|
-
"copilot",
|
|
1049
|
-
"cursorrules",
|
|
1050
|
-
"cline",
|
|
1051
|
-
"windsurf"
|
|
1052
|
-
];
|
|
1053
|
-
for (const id of singleFileArtifacts) {
|
|
1054
|
-
const artifact = facts.artifacts.find((a) => a.id === id);
|
|
1055
|
-
if (artifact?.exists) await add(artifact.path);
|
|
1056
|
-
}
|
|
1057
|
-
const rulesDir = path.join(root, ".cursor", "rules");
|
|
1058
|
-
if (await isDirectory(rulesDir)) {
|
|
1059
|
-
try {
|
|
1060
|
-
for (const entry of await promises.readdir(rulesDir)) {
|
|
1061
|
-
if (entry.endsWith(".md") || entry.endsWith(".mdc"))
|
|
1062
|
-
await add(path.join(".cursor/rules", entry));
|
|
1063
|
-
}
|
|
1064
|
-
} catch {
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
const skillsDir = path.join(root, ".claude", "skills");
|
|
1068
|
-
if (await isDirectory(skillsDir)) {
|
|
1069
|
-
try {
|
|
1070
|
-
for (const entry of await promises.readdir(skillsDir)) {
|
|
1071
|
-
const skill = path.join(".claude/skills", entry, "SKILL.md");
|
|
1072
|
-
await add(skill);
|
|
1073
|
-
}
|
|
1074
|
-
} catch {
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
const detected = new Set(files.map((f) => f.path));
|
|
1078
|
-
const included = [];
|
|
1079
|
-
for (const rel of await expandFileGlobs(root, scope?.include ?? [])) {
|
|
1080
|
-
if (detected.has(rel)) continue;
|
|
1081
|
-
const before = files.length;
|
|
1082
|
-
await add(rel);
|
|
1083
|
-
if (files.length > before) included.push(rel);
|
|
1084
|
-
}
|
|
1085
|
-
const exclude = scope?.exclude ?? [];
|
|
1086
|
-
if (!exclude.length) return { files, excluded: [], included };
|
|
1087
|
-
const kept = [];
|
|
1088
|
-
const excluded = [];
|
|
1089
|
-
for (const file of files) {
|
|
1090
|
-
if (matchesAnyGlob(file.path, exclude)) excluded.push(file.path);
|
|
1091
|
-
else kept.push(file);
|
|
1092
|
-
}
|
|
1093
|
-
return { files: kept, excluded, included };
|
|
1094
|
-
}
|
|
1095
|
-
async function listStateDocuments(root, facts) {
|
|
1096
|
-
const docs = [];
|
|
1097
|
-
for (const artifact of facts.artifacts) {
|
|
1098
|
-
if (artifact.kind !== "state" || !artifact.exists) continue;
|
|
1099
|
-
const text = await readText(path.join(root, artifact.path));
|
|
1100
|
-
if (text !== null) docs.push({ path: normalizeRelPath(artifact.path), text });
|
|
1101
|
-
}
|
|
1102
|
-
return docs;
|
|
1103
|
-
}
|
|
1104
|
-
function extractCodeTokens(text) {
|
|
1105
|
-
const tokens = [];
|
|
1106
|
-
for (const m of text.matchAll(/`([^`\n]+)`/g)) tokens.push(m[1].trim());
|
|
1107
|
-
for (const block of text.matchAll(/```[a-z]*\n([\s\S]*?)```/g)) {
|
|
1108
|
-
for (const line of block[1].split("\n")) {
|
|
1109
|
-
const trimmed = line.trim();
|
|
1110
|
-
if (trimmed && !trimmed.startsWith("#")) tokens.push(trimmed);
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
return tokens;
|
|
1114
|
-
}
|
|
1115
|
-
var PM_BUILTINS = /* @__PURE__ */ new Set([
|
|
1116
|
-
"install",
|
|
1117
|
-
"i",
|
|
1118
|
-
"add",
|
|
1119
|
-
"remove",
|
|
1120
|
-
"rm",
|
|
1121
|
-
"up",
|
|
1122
|
-
"update",
|
|
1123
|
-
"upgrade",
|
|
1124
|
-
"dlx",
|
|
1125
|
-
"exec",
|
|
1126
|
-
"create",
|
|
1127
|
-
"init",
|
|
1128
|
-
"link",
|
|
1129
|
-
"unlink",
|
|
1130
|
-
"publish",
|
|
1131
|
-
"pack",
|
|
1132
|
-
"audit",
|
|
1133
|
-
"outdated",
|
|
1134
|
-
"why",
|
|
1135
|
-
"list",
|
|
1136
|
-
"ls",
|
|
1137
|
-
"view",
|
|
1138
|
-
"info",
|
|
1139
|
-
"config",
|
|
1140
|
-
"store",
|
|
1141
|
-
"import",
|
|
1142
|
-
"rebuild",
|
|
1143
|
-
"prune",
|
|
1144
|
-
"setup",
|
|
1145
|
-
"env",
|
|
1146
|
-
"bin",
|
|
1147
|
-
"root",
|
|
1148
|
-
"licenses",
|
|
1149
|
-
"patch",
|
|
1150
|
-
"approve-builds",
|
|
1151
|
-
"workspaces",
|
|
1152
|
-
"workspace",
|
|
1153
|
-
"cache",
|
|
1154
|
-
"version",
|
|
1155
|
-
"help"
|
|
1156
|
-
]);
|
|
1157
|
-
function extractCommandClaims(text) {
|
|
1158
|
-
const scripts = /* @__PURE__ */ new Map();
|
|
1159
|
-
let filteredSkipped = 0;
|
|
1160
|
-
for (const token of extractCodeTokens(text)) {
|
|
1161
|
-
for (const m of token.matchAll(
|
|
1162
|
-
/(?:^|&&\s*|\|\|\s*|;\s*|\|\s*|\$\s+|\(\s*)(pnpm|yarn|npm|bun)\s+(?:(run)\s+)?(-{0,2}[A-Za-z0-9:._@/[\]-]+)/g
|
|
1163
|
-
)) {
|
|
1164
|
-
const pm = m[1];
|
|
1165
|
-
const ranExplicit = Boolean(m[2]);
|
|
1166
|
-
const arg = m[3];
|
|
1167
|
-
if (arg.startsWith("-")) {
|
|
1168
|
-
filteredSkipped += 1;
|
|
1169
|
-
continue;
|
|
1170
|
-
}
|
|
1171
|
-
if (pm === "npm" && !ranExplicit && arg !== "test" && arg !== "start") continue;
|
|
1172
|
-
if ((pm === "bun" || pm === "yarn" || pm === "pnpm") && !ranExplicit && PM_BUILTINS.has(arg))
|
|
1173
|
-
continue;
|
|
1174
|
-
if (ranExplicit && PM_BUILTINS.has(arg)) continue;
|
|
1175
|
-
scripts.set(arg, token);
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
return { scripts, filteredSkipped };
|
|
1179
|
-
}
|
|
1180
|
-
var PATH_TOKEN_RE = /^[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.$-]+)+\/?$/;
|
|
1181
|
-
var KNOWN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1182
|
-
..."ts tsx cts mts js jsx cjs mjs json jsonc json5 md mdx mdc yml yaml toml ini cfg conf env sh bash zsh fish ps1 bat cmd css scss sass less html htm xml svg sql prisma graphql gql proto py rb rs go java kt kts swift c h cc cpp hpp cs php vue svelte astro txt log lock csv tsv png jpg jpeg gif webp ico avif woff woff2 ttf otf wasm map pem key crt tf tfvars example sample local snap ejs hbs pug".split(" ")
|
|
1183
|
-
]);
|
|
1184
|
-
var CREATION_CONTEXT_RE = /\b(?:creat(?:e|es|ed|ing)|generat(?:e|es|ed|ing)|scaffold(?:s|ed|ing)?|quarantin(?:e|es|ed|ing)|(?:writ(?:e|es|ten|ing)|output(?:s|ted)?|emit(?:s|ted|ting)?|sav(?:e|es|ed|ing)|mov(?:e|es|ed|ing)|copy|copi(?:es|ed))\s+(?:it\s+|them\s+)?(?:to|into)|new\s+(?:file|directory|folder)|will\s+(?:be\s+)?(?:created|generated|written)|add(?:s|ed|ing)?\s+(?:a|the)\s+new)\b/i;
|
|
1185
|
-
var PLACEHOLDER_SEGMENTS = /* @__PURE__ */ new Set(["placeholder", "foo", "bar", "baz", "qux"]);
|
|
1186
|
-
var PLACEHOLDER_PREFIX_RE = /^(?:my|your)-/i;
|
|
1187
|
-
function isPlaceholderClaim(token) {
|
|
1188
|
-
return token.split("/").some((seg) => PLACEHOLDER_PREFIX_RE.test(seg) || PLACEHOLDER_SEGMENTS.has(seg.toLowerCase()));
|
|
1189
|
-
}
|
|
1190
|
-
function claimContext(text, index) {
|
|
1191
|
-
const start = text.lastIndexOf("\n", index) + 1;
|
|
1192
|
-
const endRaw = text.indexOf("\n", index);
|
|
1193
|
-
const end = endRaw === -1 ? text.length : endRaw;
|
|
1194
|
-
const line = text.slice(start, end);
|
|
1195
|
-
if (!/^\s*(?:[-*+]|\d+[.)]|\|)/.test(line)) return line;
|
|
1196
|
-
let cursor = start;
|
|
1197
|
-
while (cursor > 0) {
|
|
1198
|
-
const prevEnd = cursor - 1;
|
|
1199
|
-
const prevStart = text.lastIndexOf("\n", prevEnd - 1) + 1;
|
|
1200
|
-
const prev = text.slice(prevStart, prevEnd);
|
|
1201
|
-
cursor = prevStart;
|
|
1202
|
-
if (!prev.trim()) continue;
|
|
1203
|
-
if (/^\s*(?:[-*+]|\d+[.)]|\|)/.test(prev)) continue;
|
|
1204
|
-
return `${prev}
|
|
1205
|
-
${line}`;
|
|
1206
|
-
}
|
|
1207
|
-
return line;
|
|
1208
|
-
}
|
|
1209
|
-
function extractPathClaims(text) {
|
|
1210
|
-
const prospectiveOnly = /* @__PURE__ */ new Map();
|
|
1211
|
-
const placeholder = /* @__PURE__ */ new Set();
|
|
1212
|
-
for (const m of text.matchAll(/`([^`\n]+)`/g)) {
|
|
1213
|
-
const token = m[1].trim();
|
|
1214
|
-
if (token.includes(" ") || token.length > 120) continue;
|
|
1215
|
-
if (token.startsWith("/") || token.startsWith("~") || token.startsWith("@") || token.startsWith("$"))
|
|
1216
|
-
continue;
|
|
1217
|
-
if (token.includes("://") || token.startsWith("www.")) continue;
|
|
1218
|
-
if (/[*?{}<>|]/.test(token)) continue;
|
|
1219
|
-
if (token.includes("@")) continue;
|
|
1220
|
-
if (!PATH_TOKEN_RE.test(token)) continue;
|
|
1221
|
-
if (token.split("/").some((seg) => seg.startsWith("$"))) continue;
|
|
1222
|
-
const isDirClaim = token.endsWith("/");
|
|
1223
|
-
const ext = token.toLowerCase().match(/\.([a-z0-9]{1,8})$/)?.[1];
|
|
1224
|
-
if (!isDirClaim && !(ext && KNOWN_EXTENSIONS.has(ext))) continue;
|
|
1225
|
-
const claim = token.replace(/\/$/, "");
|
|
1226
|
-
if (isPlaceholderClaim(claim)) {
|
|
1227
|
-
placeholder.add(claim);
|
|
1228
|
-
continue;
|
|
1229
|
-
}
|
|
1230
|
-
const prospective2 = CREATION_CONTEXT_RE.test(claimContext(text, m.index ?? 0));
|
|
1231
|
-
prospectiveOnly.set(claim, (prospectiveOnly.get(claim) ?? true) && prospective2);
|
|
1232
|
-
}
|
|
1233
|
-
const paths = [];
|
|
1234
|
-
const prospective = [];
|
|
1235
|
-
for (const [claim, only] of prospectiveOnly) {
|
|
1236
|
-
if (only) prospective.push(claim);
|
|
1237
|
-
else paths.push(claim);
|
|
1238
|
-
}
|
|
1239
|
-
return { paths, prospective, placeholder: [...placeholder] };
|
|
1240
|
-
}
|
|
1241
|
-
var LOCAL_REF_LEADINS = new Set(
|
|
1242
|
-
"decision decisions entry entries ruling rulings record records ledger id ids item items see per in of on at by to as is was are were the a an and or but not with under over from via vs than after before since between through against latest newest earliest only also still now supersedes superseded superseding amends amended extends extended cites cited citing adds added adding wrote written writes locked locks closed closes opened opens resolves resolved reopened recorded number numbers".split(" ")
|
|
1243
|
-
);
|
|
1244
|
-
function extractDecisionRefs(text) {
|
|
1245
|
-
const byNum = /* @__PURE__ */ new Map();
|
|
1246
|
-
for (const m of text.matchAll(/\bD-(\d{1,4})\b/g)) {
|
|
1247
|
-
const index = m.index ?? 0;
|
|
1248
|
-
if (index > 0 && /[-/_.]/.test(text[index - 1])) continue;
|
|
1249
|
-
const before = text.slice(Math.max(0, index - 48), index);
|
|
1250
|
-
const lead = /([A-Za-z][A-Za-z0-9'’-]*)[ \t]+$/.exec(before)?.[1];
|
|
1251
|
-
const local = !lead || LOCAL_REF_LEADINS.has(lead.toLowerCase());
|
|
1252
|
-
const num = Number(m[1]);
|
|
1253
|
-
const seen = byNum.get(num);
|
|
1254
|
-
if (!seen) byNum.set(num, { asWritten: m[0], local });
|
|
1255
|
-
else seen.local = seen.local || local;
|
|
1256
|
-
}
|
|
1257
|
-
const refs = /* @__PURE__ */ new Map();
|
|
1258
|
-
let qualifiedSkipped = 0;
|
|
1259
|
-
for (const [num, ref] of byNum) {
|
|
1260
|
-
if (ref.local) refs.set(num, ref.asWritten);
|
|
1261
|
-
else qualifiedSkipped += 1;
|
|
1262
|
-
}
|
|
1263
|
-
return { refs, qualifiedSkipped };
|
|
1264
|
-
}
|
|
1265
|
-
function packageManagerUsage(text) {
|
|
1266
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1267
|
-
for (const token of extractCodeTokens(text)) {
|
|
1268
|
-
for (const m of token.matchAll(/\b(pnpm|yarn|npm|bun)\s+(?:run\s+)?[A-Za-z-]/g)) {
|
|
1269
|
-
const pm = m[1];
|
|
1270
|
-
counts.set(pm, (counts.get(pm) ?? 0) + 1);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
return counts;
|
|
1274
|
-
}
|
|
1275
|
-
var KNOWN_DOC_REFS = [
|
|
1276
|
-
"AGENTS.md",
|
|
1277
|
-
"CLAUDE.md",
|
|
1278
|
-
"PROJECT_CONTEXT.md",
|
|
1279
|
-
"DECISIONS.md",
|
|
1280
|
-
"GEMINI.md"
|
|
1281
|
-
];
|
|
1282
|
-
var PATH_TOKEN_CHARS = /[A-Za-z0-9_.$~/-]/;
|
|
1283
|
-
function extractDocRefs(text) {
|
|
1284
|
-
const refs = [];
|
|
1285
|
-
let tildeSkipped = 0;
|
|
1286
|
-
for (const name of KNOWN_DOC_REFS) {
|
|
1287
|
-
let claimed = false;
|
|
1288
|
-
let at = text.indexOf(name);
|
|
1289
|
-
while (at !== -1) {
|
|
1290
|
-
let head = at;
|
|
1291
|
-
while (head > 0 && PATH_TOKEN_CHARS.test(text[head - 1])) head -= 1;
|
|
1292
|
-
if (text[head] === "~") tildeSkipped += 1;
|
|
1293
|
-
else claimed = true;
|
|
1294
|
-
at = text.indexOf(name, at + name.length);
|
|
1295
|
-
}
|
|
1296
|
-
if (claimed) refs.push(name);
|
|
1297
|
-
}
|
|
1298
|
-
return { refs, tildeSkipped };
|
|
1299
|
-
}
|
|
1300
746
|
|
|
1301
747
|
// src/lenses/instruction-truth/lens.ts
|
|
1302
|
-
var
|
|
748
|
+
var LENS_ID3 = "instruction-truth";
|
|
1303
749
|
var MAX_PATH_FINDINGS_PER_FILE = 15;
|
|
1304
750
|
function finding2(partial) {
|
|
1305
|
-
return { lens:
|
|
751
|
+
return { lens: LENS_ID3, ...partial };
|
|
1306
752
|
}
|
|
1307
753
|
function compareCommands(baseline, fresh) {
|
|
1308
754
|
const out = [];
|
|
@@ -1311,7 +757,7 @@ function compareCommands(baseline, fresh) {
|
|
|
1311
757
|
if (before && !(before in fresh.commands.raw)) {
|
|
1312
758
|
out.push(
|
|
1313
759
|
finding2({
|
|
1314
|
-
id: `${
|
|
760
|
+
id: `${LENS_ID3}/command-gone-${role}`,
|
|
1315
761
|
tier: "risk",
|
|
1316
762
|
claim: `Documented ${role} command \`${before}\` no longer exists in package.json`,
|
|
1317
763
|
evidence: ["package.json"],
|
|
@@ -1333,7 +779,7 @@ function compareArtifacts(baseline, fresh) {
|
|
|
1333
779
|
if (a.exists && now && !now.exists) {
|
|
1334
780
|
out.push(
|
|
1335
781
|
finding2({
|
|
1336
|
-
id: `${
|
|
782
|
+
id: `${LENS_ID3}/artifact-gone-${a.id}`,
|
|
1337
783
|
tier: "gap",
|
|
1338
784
|
claim: `${a.label} was present at baseline but is now missing`,
|
|
1339
785
|
evidence: [a.path],
|
|
@@ -1351,7 +797,7 @@ function compareLayout(baseline, fresh) {
|
|
|
1351
797
|
const now = new Set(fresh.tree.dirs.map((d) => d.name));
|
|
1352
798
|
return baseline.tree.dirs.filter((d) => !now.has(d.name)).map(
|
|
1353
799
|
(d) => finding2({
|
|
1354
|
-
id: `${
|
|
800
|
+
id: `${LENS_ID3}/dir-gone-${d.name}`,
|
|
1355
801
|
tier: "gap",
|
|
1356
802
|
claim: `Top-level \`${d.name}/\` from the baseline no longer exists \u2014 the repo map may be stale`,
|
|
1357
803
|
evidence: [`${d.name}/`],
|
|
@@ -1363,7 +809,7 @@ function compareLayout(baseline, fresh) {
|
|
|
1363
809
|
);
|
|
1364
810
|
}
|
|
1365
811
|
var instructionTruthLens = {
|
|
1366
|
-
id:
|
|
812
|
+
id: LENS_ID3,
|
|
1367
813
|
version: "1",
|
|
1368
814
|
title: "Instruction truth",
|
|
1369
815
|
kind: "truth",
|
|
@@ -1381,7 +827,7 @@ var instructionTruthLens = {
|
|
|
1381
827
|
if (!files.length) {
|
|
1382
828
|
findings.push(
|
|
1383
829
|
finding2({
|
|
1384
|
-
id: `${
|
|
830
|
+
id: `${LENS_ID3}/no-contract`,
|
|
1385
831
|
tier: "gap",
|
|
1386
832
|
claim: "No agent instruction files exist (AGENTS.md or equivalents)",
|
|
1387
833
|
evidence: ["AGENTS.md (missing)"],
|
|
@@ -1392,99 +838,17 @@ var instructionTruthLens = {
|
|
|
1392
838
|
})
|
|
1393
839
|
);
|
|
1394
840
|
}
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
}
|
|
1402
|
-
const pathResolves = async (claim) => {
|
|
1403
|
-
const bases = [root, ...facts.packages.map((p) => path.join(root, p.dir))];
|
|
1404
|
-
for (const base of bases) {
|
|
1405
|
-
if (await pathExists(path.join(base, claim))) return true;
|
|
1406
|
-
if (await pathExists(path.join(base, "src", claim))) return true;
|
|
1407
|
-
if (await pathExists(path.join(base, "scripts", claim))) return true;
|
|
1408
|
-
}
|
|
1409
|
-
return false;
|
|
1410
|
-
};
|
|
1411
|
-
const binResolves = async (name) => {
|
|
1412
|
-
const bases = [root, ...facts.packages.map((p) => path.join(root, p.dir))];
|
|
1413
|
-
for (const base of bases) {
|
|
1414
|
-
if (await pathExists(path.join(base, "node_modules", ".bin", name))) return true;
|
|
1415
|
-
}
|
|
1416
|
-
return false;
|
|
841
|
+
const env = await buildTruthEnv(root, facts);
|
|
842
|
+
const counters = emptyCounters();
|
|
843
|
+
const claimOpts = {
|
|
844
|
+
lensId: LENS_ID3,
|
|
845
|
+
missingPathTier: "gap",
|
|
846
|
+
maxPathFindings: MAX_PATH_FINDINGS_PER_FILE
|
|
1417
847
|
};
|
|
1418
|
-
const nodeModulesInstalled = await pathExists(path.join(root, "node_modules"));
|
|
1419
|
-
const manifestExists = await pathExists(path.join(root, "package.json")) || facts.packages.length > 0;
|
|
1420
|
-
let totalFilteredSkipped = 0;
|
|
1421
|
-
let totalTildeSkipped = 0;
|
|
1422
|
-
let binaryResolved = 0;
|
|
1423
|
-
let unverifiableCommands = 0;
|
|
1424
|
-
let gitignoredSkipped = 0;
|
|
1425
|
-
let prospectiveSkipped = 0;
|
|
1426
|
-
let placeholderSkipped = 0;
|
|
1427
848
|
const auditClaims = async (file) => {
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
if (knownScripts.has(script)) continue;
|
|
1432
|
-
if (await binResolves(script)) {
|
|
1433
|
-
binaryResolved += 1;
|
|
1434
|
-
continue;
|
|
1435
|
-
}
|
|
1436
|
-
if (!nodeModulesInstalled && manifestExists) {
|
|
1437
|
-
unverifiableCommands += 1;
|
|
1438
|
-
continue;
|
|
1439
|
-
}
|
|
1440
|
-
findings.push(
|
|
1441
|
-
finding2({
|
|
1442
|
-
id: `${LENS_ID4}/stale-command:${file.path}:${script}`,
|
|
1443
|
-
tier: "risk",
|
|
1444
|
-
claim: `${file.path} tells agents to run \`${script}\` \u2014 no such script exists`,
|
|
1445
|
-
evidence: [`${file.path}: \`${raw}\``, "package.json scripts (root + workspaces)"],
|
|
1446
|
-
why: "An agent following this instruction runs a command that fails \u2014 or silently skips the check it was meant to run.",
|
|
1447
|
-
action: "Update the instruction to the current script name (or restore the script).",
|
|
1448
|
-
effort: "S",
|
|
1449
|
-
confidence: "high"
|
|
1450
|
-
})
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
const { paths, prospective, placeholder } = extractPathClaims(file.text);
|
|
1454
|
-
prospectiveSkipped += prospective.length;
|
|
1455
|
-
placeholderSkipped += placeholder.length;
|
|
1456
|
-
const missing = [];
|
|
1457
|
-
for (const claim of paths) {
|
|
1458
|
-
if (!await pathResolves(claim)) missing.push(claim);
|
|
1459
|
-
}
|
|
1460
|
-
const ignoredOut = missing.length ? await git(root, ["check-ignore", ...missing]) : null;
|
|
1461
|
-
const gitignored = new Set((ignoredOut ?? "").split("\n").filter(Boolean));
|
|
1462
|
-
let pathFindings = 0;
|
|
1463
|
-
for (const claim of missing) {
|
|
1464
|
-
if (gitignored.has(claim)) {
|
|
1465
|
-
gitignoredSkipped += 1;
|
|
1466
|
-
continue;
|
|
1467
|
-
}
|
|
1468
|
-
if (pathFindings >= MAX_PATH_FINDINGS_PER_FILE) {
|
|
1469
|
-
disclosures.push(
|
|
1470
|
-
`${file.path}: more than ${MAX_PATH_FINDINGS_PER_FILE} missing-path claims \u2014 truncated.`
|
|
1471
|
-
);
|
|
1472
|
-
break;
|
|
1473
|
-
}
|
|
1474
|
-
pathFindings += 1;
|
|
1475
|
-
findings.push(
|
|
1476
|
-
finding2({
|
|
1477
|
-
id: `${LENS_ID4}/stale-path:${file.path}:${claim}`,
|
|
1478
|
-
tier: "gap",
|
|
1479
|
-
claim: `${file.path} references \`${claim}\` \u2014 it does not exist in the repo`,
|
|
1480
|
-
evidence: [file.path, `missing: ${claim}`],
|
|
1481
|
-
why: "Agents navigate by these references; a dead path wastes a lookup and erodes trust in the rest of the file.",
|
|
1482
|
-
action: "Fix or remove the reference.",
|
|
1483
|
-
effort: "S",
|
|
1484
|
-
confidence: "medium"
|
|
1485
|
-
})
|
|
1486
|
-
);
|
|
1487
|
-
}
|
|
849
|
+
const result = await checkTextClaims(env, file, claimOpts, counters);
|
|
850
|
+
findings.push(...result.findings);
|
|
851
|
+
disclosures.push(...result.disclosures);
|
|
1488
852
|
};
|
|
1489
853
|
for (const file of files) {
|
|
1490
854
|
await auditClaims(file);
|
|
@@ -1495,7 +859,7 @@ var instructionTruthLens = {
|
|
|
1495
859
|
if (pm === facts.packageManager || count < 2 || count <= own) continue;
|
|
1496
860
|
findings.push(
|
|
1497
861
|
finding2({
|
|
1498
|
-
id: `${
|
|
862
|
+
id: `${LENS_ID3}/pm-conflict:${file.path}`,
|
|
1499
863
|
tier: "gap",
|
|
1500
864
|
claim: `${file.path} instructs \`${pm}\` (${count}\xD7) but the repo uses ${facts.packageManager}`,
|
|
1501
865
|
evidence: [file.path, `lockfile \u2192 ${facts.packageManager}`],
|
|
@@ -1508,66 +872,15 @@ var instructionTruthLens = {
|
|
|
1508
872
|
break;
|
|
1509
873
|
}
|
|
1510
874
|
}
|
|
1511
|
-
|
|
1512
|
-
totalTildeSkipped += tildeSkipped;
|
|
1513
|
-
for (const ref of docRefs) {
|
|
1514
|
-
if (await pathExists(path.join(root, ref))) continue;
|
|
1515
|
-
findings.push(
|
|
1516
|
-
finding2({
|
|
1517
|
-
id: `${LENS_ID4}/dangling-ref:${file.path}:${ref}`,
|
|
1518
|
-
tier: "gap",
|
|
1519
|
-
claim: `${file.path} references ${ref} \u2014 no such file exists`,
|
|
1520
|
-
evidence: [file.path, `missing: ${ref}`],
|
|
1521
|
-
why: "The pointer chain agents follow breaks at a file they can never read.",
|
|
1522
|
-
action: `Create ${ref} or remove the reference.`,
|
|
1523
|
-
effort: "S",
|
|
1524
|
-
confidence: "high"
|
|
1525
|
-
})
|
|
1526
|
-
);
|
|
1527
|
-
}
|
|
875
|
+
findings.push(...(await checkDocRefs(env, file, LENS_ID3, counters)).findings);
|
|
1528
876
|
}
|
|
1529
877
|
const auditedPaths = new Set(files.map((f) => f.path));
|
|
1530
878
|
const stateDocs = await listStateDocuments(root, facts);
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
let ledgerIds = null;
|
|
1534
|
-
const ledgerSources = [];
|
|
1535
|
-
if (stateDocs.length) {
|
|
1536
|
-
for (const artifact of facts.artifacts) {
|
|
1537
|
-
if (artifact.kind !== "decisions" || !artifact.exists) continue;
|
|
1538
|
-
const text = await readText(path.join(root, artifact.path));
|
|
1539
|
-
if (text === null) continue;
|
|
1540
|
-
const entries = parseDecisionEntries(text);
|
|
1541
|
-
if (!entries.length) continue;
|
|
1542
|
-
ledgerIds ??= /* @__PURE__ */ new Set();
|
|
1543
|
-
for (const entry of entries) ledgerIds.add(entry.num);
|
|
1544
|
-
ledgerSources.push(artifact.path);
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
879
|
+
const ledger = stateDocs.length ? await loadDecisionLedger(root, facts) : { ids: null, sources: [] };
|
|
880
|
+
const ledgerSources = ledger.sources;
|
|
1547
881
|
for (const doc of stateDocs) {
|
|
1548
882
|
if (!auditedPaths.has(doc.path)) await auditClaims(doc);
|
|
1549
|
-
|
|
1550
|
-
qualifiedRefsSkipped += qualifiedSkipped;
|
|
1551
|
-
if (!refs.size) continue;
|
|
1552
|
-
if (!ledgerIds) {
|
|
1553
|
-
unresolvableRefs += refs.size;
|
|
1554
|
-
continue;
|
|
1555
|
-
}
|
|
1556
|
-
for (const [num, asWritten] of refs) {
|
|
1557
|
-
if (ledgerIds.has(num)) continue;
|
|
1558
|
-
findings.push(
|
|
1559
|
-
finding2({
|
|
1560
|
-
id: `${LENS_ID4}/dead-decision-ref:${doc.path}:${asWritten}`,
|
|
1561
|
-
tier: "gap",
|
|
1562
|
-
claim: `${doc.path} cites ${asWritten} \u2014 no such entry exists in ${ledgerSources.join(", ")}`,
|
|
1563
|
-
evidence: [doc.path, `${ledgerSources.join(", ")}: no ${asWritten} entry`],
|
|
1564
|
-
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.",
|
|
1565
|
-
action: "Fix the reference \u2014 or record the missing decision.",
|
|
1566
|
-
effort: "S",
|
|
1567
|
-
confidence: "medium"
|
|
1568
|
-
})
|
|
1569
|
-
);
|
|
1570
|
-
}
|
|
883
|
+
findings.push(...checkDecisionRefs(doc, ledger, { lensId: LENS_ID3 }, counters).findings);
|
|
1571
884
|
}
|
|
1572
885
|
if (ctx.baseline) {
|
|
1573
886
|
findings.push(
|
|
@@ -1590,34 +903,34 @@ var instructionTruthLens = {
|
|
|
1590
903
|
"No committed baseline (.etymd/baseline.json) \u2014 drift over time is not measurable; run `etymd init` to approve one."
|
|
1591
904
|
);
|
|
1592
905
|
}
|
|
1593
|
-
if (binaryResolved) {
|
|
906
|
+
if (counters.binaryResolved) {
|
|
1594
907
|
disclosures.push(
|
|
1595
|
-
`${binaryResolved} command claim(s) are installed binaries (node_modules/.bin), not package scripts \u2014 treated as true.`
|
|
908
|
+
`${counters.binaryResolved} command claim(s) are installed binaries (node_modules/.bin), not package scripts \u2014 treated as true.`
|
|
1596
909
|
);
|
|
1597
910
|
}
|
|
1598
|
-
if (unverifiableCommands) {
|
|
911
|
+
if (counters.unverifiableCommands) {
|
|
1599
912
|
disclosures.push(
|
|
1600
|
-
`node_modules is not installed \u2014 ${unverifiableCommands} command claim(s) matching no package script could not be checked against installed binaries; skipped, not flagged.`
|
|
913
|
+
`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.`
|
|
1601
914
|
);
|
|
1602
915
|
}
|
|
1603
|
-
if (gitignoredSkipped) {
|
|
916
|
+
if (counters.gitignoredSkipped) {
|
|
1604
917
|
disclosures.push(
|
|
1605
|
-
`${gitignoredSkipped} missing path claim(s) are gitignored (machine-local, e.g. .env) \u2014 existence is not verifiable from the repo; skipped, not flagged.`
|
|
918
|
+
`${counters.gitignoredSkipped} missing path claim(s) are gitignored (machine-local, e.g. .env) \u2014 existence is not verifiable from the repo; skipped, not flagged.`
|
|
1606
919
|
);
|
|
1607
920
|
}
|
|
1608
|
-
if (prospectiveSkipped) {
|
|
921
|
+
if (counters.prospectiveSkipped) {
|
|
1609
922
|
disclosures.push(
|
|
1610
|
-
`${prospectiveSkipped} path claim(s) sit in create-this prose (the file instructs generating them) \u2014 forward-looking, not stale; skipped, not flagged.`
|
|
923
|
+
`${counters.prospectiveSkipped} path claim(s) sit in create-this prose (the file instructs generating them) \u2014 forward-looking, not stale; skipped, not flagged.`
|
|
1611
924
|
);
|
|
1612
925
|
}
|
|
1613
|
-
if (placeholderSkipped) {
|
|
926
|
+
if (counters.placeholderSkipped) {
|
|
1614
927
|
disclosures.push(
|
|
1615
|
-
`${placeholderSkipped} path claim(s) are naming stand-ins (e.g. \`my-custom-skill\`) rather than real references; skipped, not flagged.`
|
|
928
|
+
`${counters.placeholderSkipped} path claim(s) are naming stand-ins (e.g. \`my-custom-skill\`) rather than real references; skipped, not flagged.`
|
|
1616
929
|
);
|
|
1617
930
|
}
|
|
1618
|
-
if (
|
|
931
|
+
if (counters.tildeSkipped) {
|
|
1619
932
|
disclosures.push(
|
|
1620
|
-
`${
|
|
933
|
+
`${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.`
|
|
1621
934
|
);
|
|
1622
935
|
}
|
|
1623
936
|
if (stateDocs.length) {
|
|
@@ -1625,14 +938,14 @@ var instructionTruthLens = {
|
|
|
1625
938
|
`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"}.`
|
|
1626
939
|
);
|
|
1627
940
|
}
|
|
1628
|
-
if (qualifiedRefsSkipped) {
|
|
941
|
+
if (counters.qualifiedRefsSkipped) {
|
|
1629
942
|
disclosures.push(
|
|
1630
|
-
`${qualifiedRefsSkipped} decision reference(s) name another record (e.g. a fleet-level ledger) \u2014 not claims about this repo's decisions; skipped, not flagged.`
|
|
943
|
+
`${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.`
|
|
1631
944
|
);
|
|
1632
945
|
}
|
|
1633
|
-
if (unresolvableRefs) {
|
|
946
|
+
if (counters.unresolvableRefs) {
|
|
1634
947
|
disclosures.push(
|
|
1635
|
-
`${unresolvableRefs} decision reference(s) could not be resolved \u2014 no decisions file with \`## D-NNN\` entries; skipped, not flagged.`
|
|
948
|
+
`${counters.unresolvableRefs} decision reference(s) could not be resolved \u2014 no decisions file with \`## D-NNN\` entries; skipped, not flagged.`
|
|
1636
949
|
);
|
|
1637
950
|
}
|
|
1638
951
|
if (excluded.length) {
|
|
@@ -1647,10 +960,10 @@ var instructionTruthLens = {
|
|
|
1647
960
|
);
|
|
1648
961
|
}
|
|
1649
962
|
disclosures.push(
|
|
1650
|
-
`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 (${
|
|
963
|
+
`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.`
|
|
1651
964
|
);
|
|
1652
965
|
return {
|
|
1653
|
-
lens:
|
|
966
|
+
lens: LENS_ID3,
|
|
1654
967
|
version: "1",
|
|
1655
968
|
title: "Instruction truth",
|
|
1656
969
|
kind: "truth",
|
|
@@ -1724,4 +1037,4 @@ async function runAudit(root, opts = {}) {
|
|
|
1724
1037
|
};
|
|
1725
1038
|
}
|
|
1726
1039
|
|
|
1727
|
-
export {
|
|
1040
|
+
export { runAudit };
|