@curiousnerd/keel 0.1.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/LICENSE +21 -0
- package/README.md +250 -0
- package/data/capability-buckets.json +15 -0
- package/dist/analyze/docDrift.d.ts +9 -0
- package/dist/analyze/docDrift.js +116 -0
- package/dist/analyze/docDrift.js.map +1 -0
- package/dist/analyze/drift.d.ts +4 -0
- package/dist/analyze/drift.js +134 -0
- package/dist/analyze/drift.js.map +1 -0
- package/dist/analyze/duplication.d.ts +7 -0
- package/dist/analyze/duplication.js +46 -0
- package/dist/analyze/duplication.js.map +1 -0
- package/dist/analyze/index.d.ts +10 -0
- package/dist/analyze/index.js +28 -0
- package/dist/analyze/index.js.map +1 -0
- package/dist/analyze/libConflicts.d.ts +9 -0
- package/dist/analyze/libConflicts.js +36 -0
- package/dist/analyze/libConflicts.js.map +1 -0
- package/dist/analyze/nearDup.d.ts +11 -0
- package/dist/analyze/nearDup.js +67 -0
- package/dist/analyze/nearDup.js.map +1 -0
- package/dist/analyze/score.d.ts +6 -0
- package/dist/analyze/score.js +39 -0
- package/dist/analyze/score.js.map +1 -0
- package/dist/analyze/shared.d.ts +19 -0
- package/dist/analyze/shared.js +53 -0
- package/dist/analyze/shared.js.map +1 -0
- package/dist/cache/hashCache.d.ts +19 -0
- package/dist/cache/hashCache.js +49 -0
- package/dist/cache/hashCache.js.map +1 -0
- package/dist/claims/parseBlock.d.ts +4 -0
- package/dist/claims/parseBlock.js +66 -0
- package/dist/claims/parseBlock.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +136 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.js +37 -0
- package/dist/config.js.map +1 -0
- package/dist/extract/imports.d.ts +12 -0
- package/dist/extract/imports.js +74 -0
- package/dist/extract/imports.js.map +1 -0
- package/dist/extract/index.d.ts +24 -0
- package/dist/extract/index.js +117 -0
- package/dist/extract/index.js.map +1 -0
- package/dist/extract/language.d.ts +3 -0
- package/dist/extract/language.js +13 -0
- package/dist/extract/language.js.map +1 -0
- package/dist/extract/naming.d.ts +11 -0
- package/dist/extract/naming.js +57 -0
- package/dist/extract/naming.js.map +1 -0
- package/dist/extract/packageJson.d.ts +3 -0
- package/dist/extract/packageJson.js +43 -0
- package/dist/extract/packageJson.js.map +1 -0
- package/dist/extract/python.d.ts +11 -0
- package/dist/extract/python.js +244 -0
- package/dist/extract/python.js.map +1 -0
- package/dist/extract/scan.d.ts +12 -0
- package/dist/extract/scan.js +16 -0
- package/dist/extract/scan.js.map +1 -0
- package/dist/extract/symbols.d.ts +9 -0
- package/dist/extract/symbols.js +120 -0
- package/dist/extract/symbols.js.map +1 -0
- package/dist/extract/walk.d.ts +10 -0
- package/dist/extract/walk.js +115 -0
- package/dist/extract/walk.js.map +1 -0
- package/dist/llm/cache.d.ts +17 -0
- package/dist/llm/cache.js +50 -0
- package/dist/llm/cache.js.map +1 -0
- package/dist/llm/claimsFromDocs.d.ts +16 -0
- package/dist/llm/claimsFromDocs.js +95 -0
- package/dist/llm/claimsFromDocs.js.map +1 -0
- package/dist/llm/explain.d.ts +10 -0
- package/dist/llm/explain.js +63 -0
- package/dist/llm/explain.js.map +1 -0
- package/dist/llm/improve.d.ts +9 -0
- package/dist/llm/improve.js +37 -0
- package/dist/llm/improve.js.map +1 -0
- package/dist/llm/provider.d.ts +24 -0
- package/dist/llm/provider.js +210 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +43 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.js +173 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/report/json.d.ts +3 -0
- package/dist/report/json.js +5 -0
- package/dist/report/json.js.map +1 -0
- package/dist/report/markdown.d.ts +9 -0
- package/dist/report/markdown.js +97 -0
- package/dist/report/markdown.js.map +1 -0
- package/dist/report/text.d.ts +11 -0
- package/dist/report/text.js +76 -0
- package/dist/report/text.js.map +1 -0
- package/dist/suppress.d.ts +22 -0
- package/dist/suppress.js +80 -0
- package/dist/suppress.js.map +1 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/util/fingerprint.d.ts +12 -0
- package/dist/util/fingerprint.js +60 -0
- package/dist/util/fingerprint.js.map +1 -0
- package/dist/util/hash.d.ts +4 -0
- package/dist/util/hash.js +15 -0
- package/dist/util/hash.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { isPathSuppressed, locationFile } from "../suppress.js";
|
|
2
|
+
import { detectDrift } from "./drift.js";
|
|
3
|
+
import { detectDocDrift } from "./docDrift.js";
|
|
4
|
+
import { detectLibConflicts } from "./libConflicts.js";
|
|
5
|
+
import { detectDuplication } from "./duplication.js";
|
|
6
|
+
import { detectNearDuplication } from "./nearDup.js";
|
|
7
|
+
import { scoreFindings } from "./score.js";
|
|
8
|
+
import { loadCapabilityBuckets } from "./shared.js";
|
|
9
|
+
const NO_SUPPRESSIONS = { buckets: new Set(), paths: [] };
|
|
10
|
+
/** Run every v0 engine against the Facts + Claims, applying suppressions. */
|
|
11
|
+
export function analyze(facts, claims, config, suppressions = NO_SUPPRESSIONS) {
|
|
12
|
+
const buckets = loadCapabilityBuckets();
|
|
13
|
+
const ignoreBuckets = new Set([...config.libConflicts.ignoreBuckets, ...suppressions.buckets]);
|
|
14
|
+
let findings = [
|
|
15
|
+
...detectDrift(claims, facts, buckets),
|
|
16
|
+
...detectDocDrift(facts, config),
|
|
17
|
+
...detectLibConflicts(facts, ignoreBuckets, buckets),
|
|
18
|
+
...detectDuplication(facts),
|
|
19
|
+
];
|
|
20
|
+
if (config.duplication.near.enabled) {
|
|
21
|
+
findings.push(...detectNearDuplication(facts, config.duplication.near.threshold));
|
|
22
|
+
}
|
|
23
|
+
// `.keelignore` path rules drop any located finding in a matching file.
|
|
24
|
+
findings = findings.filter((f) => !(f.location && isPathSuppressed(suppressions, locationFile(f.location))));
|
|
25
|
+
const { score, breakdown } = scoreFindings(findings);
|
|
26
|
+
return { findings, score, breakdown };
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/analyze/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAqB,MAAM,gBAAgB,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAQpD,MAAM,eAAe,GAAiB,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;AAExE,6EAA6E;AAC7E,MAAM,UAAU,OAAO,CACrB,KAAY,EACZ,MAAc,EACd,MAAkB,EAClB,eAA6B,eAAe;IAE5C,MAAM,OAAO,GAAG,qBAAqB,EAAE,CAAC;IACxC,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IAE/F,IAAI,QAAQ,GAAc;QACxB,GAAG,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC;QACtC,GAAG,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC;QAChC,GAAG,kBAAkB,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,CAAC;QACpD,GAAG,iBAAiB,CAAC,KAAK,CAAC;KAC5B,CAAC;IACF,IAAI,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QACpC,QAAQ,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IACpF,CAAC;IAED,wEAAwE;IACxE,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,gBAAgB,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAE7G,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACrD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AACxC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Facts, Finding } from "../types.js";
|
|
2
|
+
import { type CapabilityBuckets } from "./shared.js";
|
|
3
|
+
/**
|
|
4
|
+
* Flag capability buckets where 2+ competing packages are actually imported.
|
|
5
|
+
* Evaluated per language, so a JS http client and a Python one are never
|
|
6
|
+
* reported as one conflict. Reports per-package file counts so an intentional
|
|
7
|
+
* migration is obvious.
|
|
8
|
+
*/
|
|
9
|
+
export declare function detectLibConflicts(facts: Facts, ignoreBuckets: Set<string>, buckets?: CapabilityBuckets): Finding[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { filesByPackage, languagesIn, loadCapabilityBuckets } from "./shared.js";
|
|
2
|
+
const CONFLICT_PENALTY = 6;
|
|
3
|
+
/**
|
|
4
|
+
* Flag capability buckets where 2+ competing packages are actually imported.
|
|
5
|
+
* Evaluated per language, so a JS http client and a Python one are never
|
|
6
|
+
* reported as one conflict. Reports per-package file counts so an intentional
|
|
7
|
+
* migration is obvious.
|
|
8
|
+
*/
|
|
9
|
+
export function detectLibConflicts(facts, ignoreBuckets, buckets = loadCapabilityBuckets()) {
|
|
10
|
+
const findings = [];
|
|
11
|
+
for (const language of languagesIn(facts)) {
|
|
12
|
+
const byPkg = filesByPackage(facts, language);
|
|
13
|
+
for (const [bucket, members] of Object.entries(buckets)) {
|
|
14
|
+
if (ignoreBuckets.has(bucket))
|
|
15
|
+
continue;
|
|
16
|
+
const present = members
|
|
17
|
+
.map((pkg) => ({ pkg, files: byPkg.get(pkg) ?? [] }))
|
|
18
|
+
.filter((entry) => entry.files.length > 0)
|
|
19
|
+
.sort((a, b) => b.files.length - a.files.length);
|
|
20
|
+
if (present.length < 2)
|
|
21
|
+
continue;
|
|
22
|
+
const summary = present
|
|
23
|
+
.map((e) => `${e.pkg} (${e.files.length} file${e.files.length === 1 ? "" : "s"})`)
|
|
24
|
+
.join(", ");
|
|
25
|
+
findings.push({
|
|
26
|
+
kind: "conflict",
|
|
27
|
+
confidence: "high",
|
|
28
|
+
title: `${bucket}: ${summary}`,
|
|
29
|
+
detail: `Multiple ${bucket} libraries are in use. Pick one, or suppress with libConflicts.ignoreBuckets: ["${bucket}"].`,
|
|
30
|
+
penalty: CONFLICT_PENALTY,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return findings;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=libConflicts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"libConflicts.js","sourceRoot":"","sources":["../../src/analyze/libConflicts.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,qBAAqB,EAA0B,MAAM,aAAa,CAAC;AAEzG,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAE3B;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAY,EACZ,aAA0B,EAC1B,UAA6B,qBAAqB,EAAE;IAEpD,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,KAAK,MAAM,QAAQ,IAAI,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAE9C,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YACxD,IAAI,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,SAAS;YAExC,MAAM,OAAO,GAAG,OAAO;iBACpB,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;iBACpD,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;iBACzC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAEnD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;gBAAE,SAAS;YAEjC,MAAM,OAAO,GAAG,OAAO;iBACpB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;iBACjF,IAAI,CAAC,IAAI,CAAC,CAAC;YACd,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,UAAU;gBAChB,UAAU,EAAE,MAAM;gBAClB,KAAK,EAAE,GAAG,MAAM,KAAK,OAAO,EAAE;gBAC9B,MAAM,EAAE,YAAY,MAAM,mFAAmF,MAAM,KAAK;gBACxH,OAAO,EAAE,gBAAgB;aAC1B,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Facts, Finding } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Detect near-duplicate functions: pairs whose normalized bodies are at least
|
|
4
|
+
* `threshold` similar (Jaccard over winnowing fingerprints) but not identical
|
|
5
|
+
* (exact clones are handled by the duplication engine).
|
|
6
|
+
*
|
|
7
|
+
* Findings are produced by greedy matching on descending similarity, so each
|
|
8
|
+
* function appears in at most one near-dup finding — keeping output readable
|
|
9
|
+
* even when several functions cluster together.
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectNearDuplication(facts: Facts, threshold: number): Finding[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jaccard } from "../util/fingerprint.js";
|
|
2
|
+
import { buildFunctionIgnoreCheck } from "./shared.js";
|
|
3
|
+
/** Gentle penalty — near-dups are advisory, not high-confidence. */
|
|
4
|
+
const NEAR_PENALTY = 2;
|
|
5
|
+
const key = (fn) => `${fn.filePath}:${fn.startLine}`;
|
|
6
|
+
const label = (fn) => fn.name ?? "(anonymous)";
|
|
7
|
+
/**
|
|
8
|
+
* Detect near-duplicate functions: pairs whose normalized bodies are at least
|
|
9
|
+
* `threshold` similar (Jaccard over winnowing fingerprints) but not identical
|
|
10
|
+
* (exact clones are handled by the duplication engine).
|
|
11
|
+
*
|
|
12
|
+
* Findings are produced by greedy matching on descending similarity, so each
|
|
13
|
+
* function appears in at most one near-dup finding — keeping output readable
|
|
14
|
+
* even when several functions cluster together.
|
|
15
|
+
*/
|
|
16
|
+
export function detectNearDuplication(facts, threshold) {
|
|
17
|
+
const isSuppressed = buildFunctionIgnoreCheck(facts);
|
|
18
|
+
const candidates = [];
|
|
19
|
+
for (const file of facts.files) {
|
|
20
|
+
for (const fn of file.functions) {
|
|
21
|
+
// Named functions only — anonymous callbacks are noise (see duplication.ts).
|
|
22
|
+
if (fn.bodyHash === null || fn.name === null || fn.fingerprints.length === 0 || isSuppressed(fn))
|
|
23
|
+
continue;
|
|
24
|
+
candidates.push({ fn, fp: fn.fingerprints });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Compare shorter fingerprint sets first so the size-ratio prune is tight.
|
|
28
|
+
candidates.sort((a, b) => a.fp.length - b.fp.length);
|
|
29
|
+
const pairs = [];
|
|
30
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
31
|
+
const a = candidates[i];
|
|
32
|
+
for (let j = i + 1; j < candidates.length; j++) {
|
|
33
|
+
const b = candidates[j];
|
|
34
|
+
// Jaccard <= min/max of set sizes; once that bound drops below the
|
|
35
|
+
// threshold, every later (longer) b can only be worse — stop early.
|
|
36
|
+
if (a.fp.length / b.fp.length < threshold)
|
|
37
|
+
break;
|
|
38
|
+
if (a.fn.bodyHash === b.fn.bodyHash)
|
|
39
|
+
continue; // identical -> exact engine
|
|
40
|
+
const sim = jaccard(a.fp, b.fp);
|
|
41
|
+
if (sim >= threshold)
|
|
42
|
+
pairs.push({ a: a.fn, b: b.fn, sim });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
pairs.sort((x, y) => y.sim - x.sim);
|
|
46
|
+
const used = new Set();
|
|
47
|
+
const findings = [];
|
|
48
|
+
for (const { a, b, sim } of pairs) {
|
|
49
|
+
const ka = key(a);
|
|
50
|
+
const kb = key(b);
|
|
51
|
+
if (used.has(ka) || used.has(kb))
|
|
52
|
+
continue;
|
|
53
|
+
used.add(ka);
|
|
54
|
+
used.add(kb);
|
|
55
|
+
const pct = Math.round(sim * 100);
|
|
56
|
+
findings.push({
|
|
57
|
+
kind: "dup-near",
|
|
58
|
+
confidence: "low",
|
|
59
|
+
title: `${label(a)}() ~${pct}% similar to ${label(b)}()`,
|
|
60
|
+
detail: `${label(a)} @ ${ka}\n ${label(b)} @ ${kb}`,
|
|
61
|
+
location: ka,
|
|
62
|
+
penalty: NEAR_PENALTY,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return findings;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=nearDup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nearDup.js","sourceRoot":"","sources":["../../src/analyze/nearDup.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-category penalty ceilings. Each category's penalty grows with the number
|
|
3
|
+
* of issues but saturates toward its cap, so one noisy category can't tank the
|
|
4
|
+
* whole score — while all three together can still drive it to 0. The ceilings
|
|
5
|
+
* sum to 100.
|
|
6
|
+
*/
|
|
7
|
+
const CATEGORY_CAP = { drift: 35, conflict: 25, duplication: 40 };
|
|
8
|
+
/**
|
|
9
|
+
* Saturating curve. Slope is ~1 near the origin (a handful of issues costs
|
|
10
|
+
* about its raw penalty) and eases toward `cap` as the raw penalty grows, so a
|
|
11
|
+
* repo with 30 duplicates scores poorly but not identically to one with 300.
|
|
12
|
+
* penalty = cap * (1 - e^(-raw / cap))
|
|
13
|
+
*/
|
|
14
|
+
function saturate(raw, cap) {
|
|
15
|
+
if (raw <= 0)
|
|
16
|
+
return 0;
|
|
17
|
+
return Math.round(cap * (1 - Math.exp(-raw / cap)));
|
|
18
|
+
}
|
|
19
|
+
/** Roll findings into per-category penalties and the 0-100 Coherence Score. */
|
|
20
|
+
export function scoreFindings(findings) {
|
|
21
|
+
const raw = { drift: 0, conflict: 0, duplication: 0 };
|
|
22
|
+
for (const f of findings) {
|
|
23
|
+
if (f.kind === "drift")
|
|
24
|
+
raw.drift += f.penalty;
|
|
25
|
+
else if (f.kind === "conflict")
|
|
26
|
+
raw.conflict += f.penalty;
|
|
27
|
+
else
|
|
28
|
+
raw.duplication += f.penalty; // dup + dup-near
|
|
29
|
+
}
|
|
30
|
+
const breakdown = {
|
|
31
|
+
drift: saturate(raw.drift, CATEGORY_CAP.drift),
|
|
32
|
+
conflict: saturate(raw.conflict, CATEGORY_CAP.conflict),
|
|
33
|
+
duplication: saturate(raw.duplication, CATEGORY_CAP.duplication),
|
|
34
|
+
};
|
|
35
|
+
const total = breakdown.drift + breakdown.conflict + breakdown.duplication;
|
|
36
|
+
const score = Math.max(0, Math.min(100, 100 - total));
|
|
37
|
+
return { score, breakdown };
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=score.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score.js","sourceRoot":"","sources":["../../src/analyze/score.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,YAAY,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAW,CAAC;AAE3E;;;;;GAKG;AACH,SAAS,QAAQ,CAAC,GAAW,EAAE,GAAW;IACxC,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACvB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;AACtD,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,aAAa,CAAC,QAAmB;IAC/C,MAAM,GAAG,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IACtD,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO;YAAE,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,OAAO,CAAC;aAC1C,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU;YAAE,GAAG,CAAC,QAAQ,IAAI,CAAC,CAAC,OAAO,CAAC;;YACrD,GAAG,CAAC,WAAW,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,iBAAiB;IACtD,CAAC;IAED,MAAM,SAAS,GAAmB;QAChC,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC;QAC9C,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,YAAY,CAAC,QAAQ,CAAC;QACvD,WAAW,EAAE,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,YAAY,CAAC,WAAW,CAAC;KACjE,CAAC;IACF,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,GAAG,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,WAAW,CAAC;IAC3E,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC;IACtD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Facts, FunctionFact, Language } from "../types.js";
|
|
2
|
+
export type CapabilityBuckets = Record<string, string[]>;
|
|
3
|
+
/** Load the curated capability buckets shipped in data/. */
|
|
4
|
+
export declare function loadCapabilityBuckets(): CapabilityBuckets;
|
|
5
|
+
/**
|
|
6
|
+
* Map each externally-imported package -> sorted list of files importing it.
|
|
7
|
+
* Built from *actual imports*, not package.json, so unused/transitive deps
|
|
8
|
+
* never appear. Pass `language` to restrict to files of one language (so e.g.
|
|
9
|
+
* a JS http client and a Python one are never mistaken for the same conflict).
|
|
10
|
+
*/
|
|
11
|
+
export declare function filesByPackage(facts: Facts, language?: Language): Map<string, string[]>;
|
|
12
|
+
/** The distinct languages present across the scanned files. */
|
|
13
|
+
export declare function languagesIn(facts: Facts): Language[];
|
|
14
|
+
/**
|
|
15
|
+
* Build a predicate that reports whether a function is suppressed by an inline
|
|
16
|
+
* `// keel-ignore` marker on its declaration line or the line directly above.
|
|
17
|
+
* Shared by the exact and near duplication engines.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildFunctionIgnoreCheck(facts: Facts): (fn: FunctionFact) => boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/** Load the curated capability buckets shipped in data/. */
|
|
5
|
+
export function loadCapabilityBuckets() {
|
|
6
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const path = join(here, "..", "..", "data", "capability-buckets.json");
|
|
8
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Map each externally-imported package -> sorted list of files importing it.
|
|
12
|
+
* Built from *actual imports*, not package.json, so unused/transitive deps
|
|
13
|
+
* never appear. Pass `language` to restrict to files of one language (so e.g.
|
|
14
|
+
* a JS http client and a Python one are never mistaken for the same conflict).
|
|
15
|
+
*/
|
|
16
|
+
export function filesByPackage(facts, language) {
|
|
17
|
+
const sets = new Map();
|
|
18
|
+
for (const file of facts.files) {
|
|
19
|
+
if (language && file.language !== language)
|
|
20
|
+
continue;
|
|
21
|
+
for (const imp of file.imports) {
|
|
22
|
+
if (!imp.external || !imp.packageName)
|
|
23
|
+
continue;
|
|
24
|
+
let set = sets.get(imp.packageName);
|
|
25
|
+
if (!set)
|
|
26
|
+
sets.set(imp.packageName, (set = new Set()));
|
|
27
|
+
set.add(file.path);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const out = new Map();
|
|
31
|
+
for (const [pkg, files] of sets)
|
|
32
|
+
out.set(pkg, [...files].sort());
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** The distinct languages present across the scanned files. */
|
|
36
|
+
export function languagesIn(facts) {
|
|
37
|
+
return [...new Set(facts.files.map((f) => f.language))];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build a predicate that reports whether a function is suppressed by an inline
|
|
41
|
+
* `// keel-ignore` marker on its declaration line or the line directly above.
|
|
42
|
+
* Shared by the exact and near duplication engines.
|
|
43
|
+
*/
|
|
44
|
+
export function buildFunctionIgnoreCheck(facts) {
|
|
45
|
+
const byPath = new Map();
|
|
46
|
+
for (const file of facts.files)
|
|
47
|
+
byPath.set(file.path, new Set(file.ignoreLines));
|
|
48
|
+
return (fn) => {
|
|
49
|
+
const lines = byPath.get(fn.filePath);
|
|
50
|
+
return lines !== undefined && (lines.has(fn.startLine) || lines.has(fn.startLine - 1));
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/analyze/shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKzC,4DAA4D;AAC5D,MAAM,UAAU,qBAAqB;IACnC,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,yBAAyB,CAAC,CAAC;IACvE,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAsB,CAAC;AACrE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAAY,EAAE,QAAmB;IAC9D,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC5C,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,QAAQ,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ;YAAE,SAAS;QACrD,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAC/B,IAAI,CAAC,GAAG,CAAC,QAAQ,IAAI,CAAC,GAAG,CAAC,WAAW;gBAAE,SAAS;YAChD,IAAI,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;YACpC,IAAI,CAAC,GAAG;gBAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;YACvD,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,GAAG,EAAoB,CAAC;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI;QAAE,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACjE,OAAO,GAAG,CAAC;AACb,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,WAAW,CAAC,KAAY;IACtC,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAY;IACnD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK;QAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IACjF,OAAO,CAAC,EAAE,EAAE,EAAE;QACZ,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;QACtC,OAAO,KAAK,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC;IACzF,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FileFacts } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* File-hash keyed cache of extracted FileFacts, persisted at `.keel/cache.json`.
|
|
4
|
+
* Re-runs on an iterating codebase only re-parse files whose content changed.
|
|
5
|
+
*/
|
|
6
|
+
export declare class HashCache {
|
|
7
|
+
private readonly path;
|
|
8
|
+
private data;
|
|
9
|
+
/** Entries written this run; everything else is pruned on save. */
|
|
10
|
+
private readonly live;
|
|
11
|
+
constructor(root: string);
|
|
12
|
+
private load;
|
|
13
|
+
/** Return cached facts iff the content hash matches; otherwise undefined. */
|
|
14
|
+
get(path: string, hash: string): FileFacts | undefined;
|
|
15
|
+
/** Record facts for this run (also marks the entry live so it survives pruning). */
|
|
16
|
+
set(path: string, hash: string, facts: FileFacts): void;
|
|
17
|
+
/** Persist only entries touched this run, pruning deleted files. */
|
|
18
|
+
save(): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
/** Bump when the FileFacts shape changes, to invalidate stale caches. */
|
|
4
|
+
const CACHE_VERSION = 5;
|
|
5
|
+
/**
|
|
6
|
+
* File-hash keyed cache of extracted FileFacts, persisted at `.keel/cache.json`.
|
|
7
|
+
* Re-runs on an iterating codebase only re-parse files whose content changed.
|
|
8
|
+
*/
|
|
9
|
+
export class HashCache {
|
|
10
|
+
path;
|
|
11
|
+
data;
|
|
12
|
+
/** Entries written this run; everything else is pruned on save. */
|
|
13
|
+
live = new Map();
|
|
14
|
+
constructor(root) {
|
|
15
|
+
this.path = join(root, ".keel", "cache.json");
|
|
16
|
+
this.data = this.load();
|
|
17
|
+
}
|
|
18
|
+
load() {
|
|
19
|
+
if (!existsSync(this.path))
|
|
20
|
+
return { version: CACHE_VERSION, files: {} };
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(readFileSync(this.path, "utf8"));
|
|
23
|
+
if (parsed.version !== CACHE_VERSION)
|
|
24
|
+
return { version: CACHE_VERSION, files: {} };
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { version: CACHE_VERSION, files: {} };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Return cached facts iff the content hash matches; otherwise undefined. */
|
|
32
|
+
get(path, hash) {
|
|
33
|
+
const entry = this.data.files[path];
|
|
34
|
+
return entry && entry.hash === hash ? entry.facts : undefined;
|
|
35
|
+
}
|
|
36
|
+
/** Record facts for this run (also marks the entry live so it survives pruning). */
|
|
37
|
+
set(path, hash, facts) {
|
|
38
|
+
this.live.set(path, { hash, facts });
|
|
39
|
+
}
|
|
40
|
+
/** Persist only entries touched this run, pruning deleted files. */
|
|
41
|
+
save() {
|
|
42
|
+
const files = {};
|
|
43
|
+
for (const [path, entry] of this.live)
|
|
44
|
+
files[path] = entry;
|
|
45
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
46
|
+
writeFileSync(this.path, JSON.stringify({ version: CACHE_VERSION, files }));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=hashCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hashCache.js","sourceRoot":"","sources":["../../src/cache/hashCache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAG1C,yEAAyE;AACzE,MAAM,aAAa,GAAG,CAAC,CAAC;AAOxB;;;GAGG;AACH,MAAM,OAAO,SAAS;IACH,IAAI,CAAS;IACtB,IAAI,CAAa;IACzB,mEAAmE;IAClD,IAAI,GAAG,IAAI,GAAG,EAA8C,CAAC;IAE9E,YAAY,IAAY;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC1B,CAAC;IAEO,IAAI;QACV,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QACzE,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAe,CAAC;YACzE,IAAI,MAAM,CAAC,OAAO,KAAK,aAAa;gBAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;YACnF,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,GAAG,CAAC,IAAY,EAAE,IAAY;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpC,OAAO,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAChE,CAAC;IAED,oFAAoF;IACpF,GAAG,CAAC,IAAY,EAAE,IAAY,EAAE,KAAgB;QAC9C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACvC,CAAC;IAED,oEAAoE;IACpE,IAAI;QACF,MAAM,KAAK,GAAwB,EAAE,CAAC;QACtC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QAC3D,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IAC9E,CAAC;CACF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { lineHasIgnore } from "../suppress.js";
|
|
4
|
+
function sectionOf(heading) {
|
|
5
|
+
const h = heading.toLowerCase();
|
|
6
|
+
if (h.includes("constraint"))
|
|
7
|
+
return "constraints";
|
|
8
|
+
if (h.includes("stack") || h.includes("tech"))
|
|
9
|
+
return "stack";
|
|
10
|
+
return "other";
|
|
11
|
+
}
|
|
12
|
+
/** Normalize a stack key: "Package manager" -> "package manager". */
|
|
13
|
+
function normalizeKey(raw) {
|
|
14
|
+
return raw.trim().toLowerCase().replace(/\s+/g, " ");
|
|
15
|
+
}
|
|
16
|
+
/** Parse the structured `## Stack` / `## Constraints` blocks from one file. */
|
|
17
|
+
function parseFile(content, source) {
|
|
18
|
+
const stack = [];
|
|
19
|
+
const constraints = [];
|
|
20
|
+
let section = "other";
|
|
21
|
+
const lines = content.split(/\r?\n/);
|
|
22
|
+
for (let i = 0; i < lines.length; i++) {
|
|
23
|
+
const line = lines[i];
|
|
24
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(line);
|
|
25
|
+
if (heading) {
|
|
26
|
+
section = sectionOf(heading[1]);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const bullet = /^\s*[-*]\s+(.*)$/.exec(line);
|
|
30
|
+
if (!bullet)
|
|
31
|
+
continue;
|
|
32
|
+
// Inline suppression: a marker on this line or the line above skips the claim.
|
|
33
|
+
if (lineHasIgnore(line) || lineHasIgnore(lines[i - 1]))
|
|
34
|
+
continue;
|
|
35
|
+
const text = bullet[1].trim();
|
|
36
|
+
if (section === "stack") {
|
|
37
|
+
const kv = /^([^:]+):\s*(.+)$/.exec(text);
|
|
38
|
+
if (kv) {
|
|
39
|
+
stack.push({ key: normalizeKey(kv[1]), value: kv[2].trim(), source, line: i + 1 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (section === "constraints") {
|
|
43
|
+
constraints.push({ key: "constraint", value: text.replace(/`/g, ""), source, line: i + 1 });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { stack, constraints };
|
|
47
|
+
}
|
|
48
|
+
/** Load and parse claims from the configured context files in `root`. */
|
|
49
|
+
export function parseClaims(root, config) {
|
|
50
|
+
const merged = { stack: [], constraints: [] };
|
|
51
|
+
for (const name of config.claims.sources) {
|
|
52
|
+
const path = join(root, name);
|
|
53
|
+
if (!existsSync(path))
|
|
54
|
+
continue;
|
|
55
|
+
try {
|
|
56
|
+
const parsed = parseFile(readFileSync(path, "utf8"), name);
|
|
57
|
+
merged.stack.push(...parsed.stack);
|
|
58
|
+
merged.constraints.push(...parsed.constraints);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// unreadable context file — skip
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return merged;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=parseBlock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseBlock.js","sourceRoot":"","sources":["../../src/claims/parseBlock.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import { loadFacts } from "./extract/scan.js";
|
|
7
|
+
import { parseClaims } from "./claims/parseBlock.js";
|
|
8
|
+
import { analyze } from "./analyze/index.js";
|
|
9
|
+
import { loadKeelignore } from "./suppress.js";
|
|
10
|
+
import { renderText, sortFindings, DEFAULT_FINDING_LIMIT } from "./report/text.js";
|
|
11
|
+
import { renderJson } from "./report/json.js";
|
|
12
|
+
import { renderMarkdown, improvementSummary } from "./report/markdown.js";
|
|
13
|
+
import { suggestImprovements } from "./llm/improve.js";
|
|
14
|
+
import { writeFileSync } from "node:fs";
|
|
15
|
+
import { resolveProvider } from "./llm/provider.js";
|
|
16
|
+
import { LLMCache } from "./llm/cache.js";
|
|
17
|
+
import { explainFindings } from "./llm/explain.js";
|
|
18
|
+
import { extractClaimsFromDocs, mergeInferredClaims } from "./llm/claimsFromDocs.js";
|
|
19
|
+
import { startMcpServer } from "./mcp/server.js";
|
|
20
|
+
const program = new Command();
|
|
21
|
+
program
|
|
22
|
+
.name("keel")
|
|
23
|
+
.description("A local-first verifier that keeps an AI coding agent honest.")
|
|
24
|
+
.version("0.0.0");
|
|
25
|
+
program
|
|
26
|
+
.command("check", { isDefault: true })
|
|
27
|
+
.description("Scan a repo and report drift, library conflicts, and duplication.")
|
|
28
|
+
.argument("[path]", "path to the repository to scan", ".")
|
|
29
|
+
.option("--json", "print the report as JSON")
|
|
30
|
+
.option("--facts", "print raw extracted Facts as JSON (debug)")
|
|
31
|
+
.option("-v, --verbose", "show finding details")
|
|
32
|
+
.option("--limit <n>", "max findings to print in text mode (default 25)", parseInt)
|
|
33
|
+
.option("--no-cache", "do not read or write the .keel cache (read-only run)")
|
|
34
|
+
.option("--no-gitignore", "scan files even if they are listed in .gitignore")
|
|
35
|
+
.option("--no-python", "skip Python files (don't load the Python parser)")
|
|
36
|
+
.option("--llm", "add plain-language explanations via your claude/codex CLI or an API key")
|
|
37
|
+
.option("--output-md [file]", "write a full Markdown report to a file (default keel-report.md)")
|
|
38
|
+
.option("--fail-under <score>", "exit non-zero if the Coherence Score is below this", parseFloat)
|
|
39
|
+
.action(async (path, opts) => {
|
|
40
|
+
const root = resolve(path);
|
|
41
|
+
const config = loadConfig(root);
|
|
42
|
+
const respectGitignore = opts.gitignore === false ? false : config.scan.respectGitignore;
|
|
43
|
+
// LLM setup (once) — off by default; powers both Tier-2 doc-claim extraction
|
|
44
|
+
// and plain-language explanations. Resolved before analyze so inferred claims
|
|
45
|
+
// can flow through the deterministic drift engine.
|
|
46
|
+
const llmEnabled = opts.llm === true || config.llm.enabled;
|
|
47
|
+
const provider = llmEnabled ? await resolveProvider(config) : undefined;
|
|
48
|
+
const llmCache = llmEnabled ? new LLMCache(root, opts.cache !== false) : undefined;
|
|
49
|
+
const llmLabel = provider?.name;
|
|
50
|
+
const t0 = process.hrtime.bigint();
|
|
51
|
+
const { facts, reused, parsed, skipped, unsupported } = await loadFacts(root, config, {
|
|
52
|
+
persistCache: opts.cache !== false,
|
|
53
|
+
respectGitignore,
|
|
54
|
+
pythonEnabled: opts.python !== false,
|
|
55
|
+
});
|
|
56
|
+
let claims = parseClaims(root, config);
|
|
57
|
+
const suppressions = loadKeelignore(root);
|
|
58
|
+
let durationMs = Number(process.hrtime.bigint() - t0) / 1e6;
|
|
59
|
+
// Tier-2 doc drift: extend claims with ones the LLM infers from prose (untimed).
|
|
60
|
+
if (provider && llmCache) {
|
|
61
|
+
const inferred = await extractClaimsFromDocs(root, config, provider, llmCache);
|
|
62
|
+
claims = { ...claims, stack: mergeInferredClaims(claims.stack, inferred) };
|
|
63
|
+
}
|
|
64
|
+
const analyzeStart = process.hrtime.bigint();
|
|
65
|
+
const { findings, score, breakdown } = analyze(facts, claims, config, suppressions);
|
|
66
|
+
durationMs += Number(process.hrtime.bigint() - analyzeStart) / 1e6;
|
|
67
|
+
if (opts.facts) {
|
|
68
|
+
process.stdout.write(JSON.stringify(facts, null, 2) + "\n");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const report = {
|
|
72
|
+
score,
|
|
73
|
+
breakdown,
|
|
74
|
+
findings,
|
|
75
|
+
stats: { filesScanned: facts.files.length, durationMs, costUsd: 0 },
|
|
76
|
+
};
|
|
77
|
+
// LLM extras (off by default): per-finding explanations + how-to-improve plan.
|
|
78
|
+
if (provider && llmCache && findings.length > 0) {
|
|
79
|
+
const limit = opts.limit && opts.limit > 0 ? opts.limit : DEFAULT_FINDING_LIMIT;
|
|
80
|
+
await explainFindings(sortFindings(findings).slice(0, limit), provider, llmCache);
|
|
81
|
+
}
|
|
82
|
+
let improvement = improvementSummary(report);
|
|
83
|
+
if (provider && llmCache) {
|
|
84
|
+
const plan = await suggestImprovements(report, provider, llmCache);
|
|
85
|
+
if (plan)
|
|
86
|
+
improvement = plan;
|
|
87
|
+
}
|
|
88
|
+
llmCache?.save();
|
|
89
|
+
// Optional full Markdown report — written to the cwd, never into the scanned repo.
|
|
90
|
+
let reportFile;
|
|
91
|
+
if (opts.outputMd !== undefined) {
|
|
92
|
+
reportFile = typeof opts.outputMd === "string" ? opts.outputMd : "keel-report.md";
|
|
93
|
+
writeFileSync(resolve(reportFile), renderMarkdown(report, improvement));
|
|
94
|
+
}
|
|
95
|
+
if (opts.json) {
|
|
96
|
+
process.stdout.write(renderJson(report) + "\n");
|
|
97
|
+
if (reportFile)
|
|
98
|
+
process.stderr.write(`report written to ${reportFile}\n`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
process.stdout.write(renderText(report, { verbose: opts.verbose, limit: opts.limit }) + "\n");
|
|
102
|
+
if (unsupported > 0) {
|
|
103
|
+
process.stdout.write(pc.yellow(` ⚠ ${unsupported} Python file${unsupported === 1 ? "" : "s"} not analyzed`) +
|
|
104
|
+
pc.dim(" (Python parsing is off — the score reflects only the files keel read)\n"));
|
|
105
|
+
}
|
|
106
|
+
if (llmLabel !== undefined) {
|
|
107
|
+
const note = llmLabel === "off" ? "no provider found — install claude/codex or set an API key" : llmLabel;
|
|
108
|
+
process.stdout.write(pc.dim(` llm: ${note}\n`));
|
|
109
|
+
}
|
|
110
|
+
const skippedNote = skipped > 0 ? `, skipped ${skipped} generated` : "";
|
|
111
|
+
process.stdout.write(pc.dim(` (parsed ${parsed}, cached ${reused}${skippedNote})\n\n`));
|
|
112
|
+
// When the LLM is on, show how to raise the score (the deterministic
|
|
113
|
+
// summary is always in the Markdown report; keep the terminal opt-in).
|
|
114
|
+
if (llmLabel && llmLabel !== "off" && findings.length > 0) {
|
|
115
|
+
const plain = improvement.replace(/\*\*/g, "").split("\n").map((l) => ` ${l}`).join("\n");
|
|
116
|
+
process.stdout.write(` ${pc.bold("How to improve")}\n${pc.dim(plain)}\n\n`);
|
|
117
|
+
}
|
|
118
|
+
if (reportFile)
|
|
119
|
+
process.stdout.write(pc.dim(` report written to ${reportFile}\n\n`));
|
|
120
|
+
}
|
|
121
|
+
if (opts.failUnder !== undefined && score < opts.failUnder) {
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
program
|
|
126
|
+
.command("mcp")
|
|
127
|
+
.description("Run keel as an MCP server (stdio) so an agent can query it before writing code")
|
|
128
|
+
.argument("[path]", "repository root to serve", ".")
|
|
129
|
+
.action(async (path) => {
|
|
130
|
+
await startMcpServer(resolve(path));
|
|
131
|
+
});
|
|
132
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
133
|
+
process.stderr.write(pc.red(`keel: ${err instanceof Error ? err.message : String(err)}\n`));
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|