circle-ir-ai 2.22.0 → 2.27.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +390 -0
  2. package/dist/agents/mastra/workflow.d.ts.map +1 -1
  3. package/dist/agents/mastra/workflow.js +41 -1
  4. package/dist/agents/mastra/workflow.js.map +1 -1
  5. package/dist/cache/backend.d.ts +52 -0
  6. package/dist/cache/backend.d.ts.map +1 -0
  7. package/dist/cache/backend.js +19 -0
  8. package/dist/cache/backend.js.map +1 -0
  9. package/dist/cache/backends/index.d.ts +32 -0
  10. package/dist/cache/backends/index.d.ts.map +1 -0
  11. package/dist/cache/backends/index.js +76 -0
  12. package/dist/cache/backends/index.js.map +1 -0
  13. package/dist/cache/backends/local-fs.d.ts +31 -0
  14. package/dist/cache/backends/local-fs.d.ts.map +1 -0
  15. package/dist/cache/backends/local-fs.js +101 -0
  16. package/dist/cache/backends/local-fs.js.map +1 -0
  17. package/dist/cache/backends/memory.d.ts +19 -0
  18. package/dist/cache/backends/memory.d.ts.map +1 -0
  19. package/dist/cache/backends/memory.js +30 -0
  20. package/dist/cache/backends/memory.js.map +1 -0
  21. package/dist/cache/backends/r2.d.ts +51 -0
  22. package/dist/cache/backends/r2.d.ts.map +1 -0
  23. package/dist/cache/backends/r2.js +116 -0
  24. package/dist/cache/backends/r2.js.map +1 -0
  25. package/dist/cache/classification-cache.d.ts +23 -9
  26. package/dist/cache/classification-cache.d.ts.map +1 -1
  27. package/dist/cache/classification-cache.js +37 -64
  28. package/dist/cache/classification-cache.js.map +1 -1
  29. package/dist/cache/discovery-cache.d.ts +71 -0
  30. package/dist/cache/discovery-cache.d.ts.map +1 -0
  31. package/dist/cache/discovery-cache.js +95 -0
  32. package/dist/cache/discovery-cache.js.map +1 -0
  33. package/dist/cache/index.d.ts +4 -0
  34. package/dist/cache/index.d.ts.map +1 -1
  35. package/dist/cache/index.js +2 -0
  36. package/dist/cache/index.js.map +1 -1
  37. package/dist/cache/tree-cache.d.ts +111 -0
  38. package/dist/cache/tree-cache.d.ts.map +1 -0
  39. package/dist/cache/tree-cache.js +171 -0
  40. package/dist/cache/tree-cache.js.map +1 -0
  41. package/dist/cache/verify-cache.d.ts +60 -0
  42. package/dist/cache/verify-cache.d.ts.map +1 -0
  43. package/dist/cache/verify-cache.js +103 -0
  44. package/dist/cache/verify-cache.js.map +1 -0
  45. package/dist/cluster/cluster.d.ts +174 -0
  46. package/dist/cluster/cluster.d.ts.map +1 -0
  47. package/dist/cluster/cluster.js +392 -0
  48. package/dist/cluster/cluster.js.map +1 -0
  49. package/dist/cluster/index.d.ts +11 -0
  50. package/dist/cluster/index.d.ts.map +1 -0
  51. package/dist/cluster/index.js +10 -0
  52. package/dist/cluster/index.js.map +1 -0
  53. package/dist/index.d.ts +1 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +4 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/llm/batch-classifier.js +2 -2
  58. package/dist/llm/batch-classifier.js.map +1 -1
  59. package/dist/llm/call-logger.d.ts +36 -0
  60. package/dist/llm/call-logger.d.ts.map +1 -1
  61. package/dist/llm/call-logger.js +36 -0
  62. package/dist/llm/call-logger.js.map +1 -1
  63. package/dist/llm/discovery.d.ts +1 -1
  64. package/dist/llm/discovery.d.ts.map +1 -1
  65. package/dist/llm/discovery.js +39 -0
  66. package/dist/llm/discovery.js.map +1 -1
  67. package/dist/llm/verification.d.ts +1 -1
  68. package/dist/llm/verification.d.ts.map +1 -1
  69. package/dist/llm/verification.js +37 -0
  70. package/dist/llm/verification.js.map +1 -1
  71. package/package.json +2 -1
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Tree Cache
3
+ *
4
+ * Persistent on-disk cache for `runPatternMatch` output (cognium-ai#153,
5
+ * P3 of epic #155). Pattern-match output is a pure function of file
6
+ * content + engine version, so caching it eliminates the parse +
7
+ * AST-walk + SAST-rule firing on warm runs.
8
+ *
9
+ * Empirically (v2.20.6 gson re-verify, 2026-06-29) the scan stage ate
10
+ * 179s of 358s end-to-end — ~half the wall-clock is re-discovering
11
+ * structure circle-ir already knows. Caching the pattern-match seam
12
+ * is the smallest empirical win in the cache stack (no LLM-token
13
+ * reduction, only CPU + wall-clock), but it stacks cleanly on the
14
+ * #145 static pre-filter — a cached tree means the pre-filter has
15
+ * more to chew on, which reduces enrichment calls downstream.
16
+ *
17
+ * **Scope:** caches `PatternMatchResult` only, NOT full `CircleIR`.
18
+ * The full graph (cross-file CFG, project-level type hierarchy) is
19
+ * too entangled with project context to cache safely without a full
20
+ * `CircleIR.serialize()` upstream — that's a separate D3 design
21
+ * question, file separately if pursued.
22
+ *
23
+ * **Key composition:**
24
+ * content = SHA-256(sourceCode) + '\0' +
25
+ * circleIrVersion + '\0' +
26
+ * circleIrAiVersion + '\0' +
27
+ * language + '\0' +
28
+ * ruleSetFingerprint
29
+ * classifier = 'tree-pattern-match'
30
+ *
31
+ * Any of: file edit, circle-ir bump, circle-ir-ai bump, language
32
+ * change, or rule-set change → cache miss.
33
+ *
34
+ * **Storage:** `ClassificationCache<PatternMatchResult>` under
35
+ * `.cognium/cache/tree/`, 7-day TTL (longer than discovery cache
36
+ * because the input is structural and stable — pattern-match output
37
+ * doesn't drift between scans the way an LLM call would).
38
+ *
39
+ * **Env toggles:**
40
+ * LLM_TREE_CACHE=0 → disable persistent tree cache.
41
+ *
42
+ * **Versioning note:** unlike discovery-cache / verify-cache, this
43
+ * cache is NOT keyed on the LLM model fingerprint (pattern-match is
44
+ * static — circle-ir doesn't call any LLM). It IS keyed on the
45
+ * circle-ir and circle-ir-ai package versions because either bump
46
+ * can change the workflow-level transforms applied to the result.
47
+ */
48
+ import * as crypto from 'crypto';
49
+ import * as fs from 'fs';
50
+ import * as path from 'path';
51
+ import { createRequire } from 'module';
52
+ import { ClassificationCache } from './classification-cache.js';
53
+ const TREE_CACHE_SUBDIR = path.join('.cognium', 'cache', 'tree');
54
+ const TREE_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days — structural, stable
55
+ /**
56
+ * Module-scoped singleton — distinct from the verify/discovery
57
+ * singletons so the tree cache (which doesn't depend on the LLM model)
58
+ * doesn't get invalidated when the operator swaps discovery model.
59
+ */
60
+ let _instance = null;
61
+ /**
62
+ * Read package versions once at module load (best-effort). Wrapped in a
63
+ * try/catch so unit tests that don't ship a package.json next to the
64
+ * source can still construct the cache; in that case the version
65
+ * component degrades to 'unknown' and the cache still works (just isn't
66
+ * cross-version-safe).
67
+ */
68
+ function safeReadVersion(spec, baseUrl) {
69
+ try {
70
+ const require = createRequire(baseUrl);
71
+ return require(spec).version || 'unknown';
72
+ }
73
+ catch {
74
+ return 'unknown';
75
+ }
76
+ }
77
+ const _circleIrVersion = safeReadVersion('circle-ir/package.json', import.meta.url);
78
+ const _circleIrAiVersion = (() => {
79
+ // The package's own version is harder to resolve via require, so read
80
+ // it directly from disk by walking up from this file.
81
+ try {
82
+ // Compiled output sits at dist/cache/tree-cache.js → ../../package.json
83
+ const candidates = [
84
+ path.resolve(new URL(import.meta.url).pathname, '../../../package.json'),
85
+ path.resolve(new URL(import.meta.url).pathname, '../../package.json'),
86
+ ];
87
+ for (const p of candidates) {
88
+ if (fs.existsSync(p)) {
89
+ const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
90
+ if (pkg.name === 'circle-ir-ai' && pkg.version) {
91
+ return pkg.version;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ catch {
97
+ /* fallthrough */
98
+ }
99
+ return 'unknown';
100
+ })();
101
+ /**
102
+ * Get the tree-cache singleton. When `LLM_TREE_CACHE=0` returns a
103
+ * disabled cache that no-ops on get/set (callers don't need to branch).
104
+ */
105
+ export function getTreeCache() {
106
+ const enabled = process.env.LLM_TREE_CACHE !== '0';
107
+ if (!_instance) {
108
+ _instance = new ClassificationCache({
109
+ cacheDir: path.join(process.cwd(), TREE_CACHE_SUBDIR),
110
+ maxAge: TREE_CACHE_TTL_MS,
111
+ enabled,
112
+ });
113
+ }
114
+ return _instance;
115
+ }
116
+ /** Reset the module-scoped singleton — test-only. */
117
+ export function resetTreeCache() {
118
+ _instance = null;
119
+ }
120
+ /**
121
+ * Compute the `(content, classifier)` tuple for the underlying
122
+ * ClassificationCache.
123
+ *
124
+ * Components:
125
+ * - source code SHA-256 — file edit invalidates only that file's entry
126
+ * - circleIrVersion — bumping circle-ir invalidates all entries
127
+ * - circleIrAiVersion — bumping circle-ir-ai (which may change
128
+ * workflow-level transforms applied to pattern-match output)
129
+ * invalidates all entries
130
+ * - language — same source, different language tag must miss (the
131
+ * analyzer's behavior depends on the language argument)
132
+ * - ruleSetFingerprint — caller-supplied digest of the active rule
133
+ * set (disabledPasses + projectProfile). Empty string is fine for
134
+ * the default rule set.
135
+ */
136
+ export function computeTreeCacheKey(input) {
137
+ const sourceSha = crypto.createHash('sha256').update(input.sourceCode).digest('hex');
138
+ const ruleSet = input.ruleSetFingerprint ?? '';
139
+ return {
140
+ content: `${sourceSha}\0${_circleIrVersion}\0${_circleIrAiVersion}\0${input.language}\0${ruleSet}`,
141
+ classifier: 'tree-pattern-match',
142
+ };
143
+ }
144
+ /**
145
+ * Compute a stable fingerprint over an arbitrary rule-set descriptor
146
+ * (disabledPasses array + projectProfile object). Result is suitable to
147
+ * pass as `ruleSetFingerprint` to `computeTreeCacheKey`. Order-stable
148
+ * for arrays so re-ordering `--disable a,b` vs `--disable b,a` still
149
+ * hits the cache.
150
+ */
151
+ export function computeRuleSetFingerprint(input) {
152
+ const passes = [...(input.disabledPasses ?? [])].sort().join(',');
153
+ // projectProfile JSON-stringifies; key order in objects matters only
154
+ // if the producer is non-deterministic — `@cognium/project-profile-detect`
155
+ // emits stable keys, so this is fine. If a future profile producer is
156
+ // less stable we can switch to a sorted-keys serializer.
157
+ const profile = input.projectProfile === undefined
158
+ ? ''
159
+ : JSON.stringify(input.projectProfile);
160
+ if (!passes && !profile)
161
+ return '';
162
+ return crypto.createHash('sha256').update(`${passes}\0${profile}`).digest('hex');
163
+ }
164
+ /** Test-only: expose the captured engine versions for assertions. */
165
+ export function getTreeCacheVersions() {
166
+ return {
167
+ circleIrVersion: _circleIrVersion,
168
+ circleIrAiVersion: _circleIrAiVersion,
169
+ };
170
+ }
171
+ //# sourceMappingURL=tree-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree-cache.js","sourceRoot":"","sources":["../../src/cache/tree-cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Verify Cache
3
+ *
4
+ * Persistent on-disk cache for LLM verification batch results
5
+ * (cognium-ai#152, P1 of epic #155). Sits in front of the in-memory
6
+ * `_verifierDedupCache` from verification.ts (#138 step 3) so that
7
+ * identical `(file SHA, pair-set)` requests survive across processes,
8
+ * CI runs, and developer iterations — not just within one process.
9
+ *
10
+ * Built on top of `ClassificationCache<BatchVerificationResult>` (which
11
+ * already carries the cognium-ai#151 model-fingerprint key dimension),
12
+ * with verify-specific key construction:
13
+ *
14
+ * content = SHA-256(code) + '\\0' + pairsFingerprint
15
+ * classifier = 'verify-batch'
16
+ * fingerprint = getPhaseModelFingerprint(config) // includes verify model
17
+ *
18
+ * Cache directory: `.cognium/cache/verify/` (separate from the
19
+ * discovery-phase `.cognium/cache/` so a `rm -rf .cognium/cache/verify/`
20
+ * only nukes verifier entries).
21
+ *
22
+ * Invalidation matrix (acceptance criteria #152):
23
+ * - Edit one file in a project → only that file's entries invalidate
24
+ * (file SHA-256 is part of the cache content key)
25
+ * - Add a new sink to the engine → existing entries still hit if the
26
+ * new sink doesn't change the pair-set for that file
27
+ * - Change verification model → entire cache invalidates
28
+ * (model fingerprint is part of the singleton key)
29
+ *
30
+ * Env toggles:
31
+ * LLM_VERIFY_CACHE=0 → disable persistent verify cache (in-memory
32
+ * dedup from #138 still applies)
33
+ */
34
+ import { ClassificationCache } from './classification-cache.js';
35
+ import { type LLMConfig } from '../llm/config.js';
36
+ import type { BatchVerificationInput, BatchVerificationResult } from '../llm/verification.js';
37
+ /**
38
+ * Get the verify-phase persistent cache, keyed by per-phase model
39
+ * fingerprint. When `LLM_VERIFY_CACHE=0` returns a disabled cache that
40
+ * no-ops on get/set (callers don't need to branch).
41
+ */
42
+ export declare function getVerifyCache(config?: LLMConfig): ClassificationCache<BatchVerificationResult>;
43
+ /** Reset the module-scoped singleton — test-only. */
44
+ export declare function resetVerifyCache(): void;
45
+ /**
46
+ * Compute the `(content, classifier)` tuple for the underlying
47
+ * ClassificationCache. The fingerprint dimension is folded in by the
48
+ * cache itself (see classification-cache.ts:computeKey).
49
+ *
50
+ * Components:
51
+ * - file SHA-256 — file edit invalidates only that file's entry
52
+ * - pairs fingerprint — sort-invariant over the source/sink set
53
+ * - classifier 'verify-batch' — namespace for future
54
+ * 'verify-pair' / 'verify-flow' tiers without collision
55
+ */
56
+ export declare function computeVerifyCacheKey(input: BatchVerificationInput): {
57
+ content: string;
58
+ classifier: string;
59
+ };
60
+ //# sourceMappingURL=verify-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-cache.d.ts","sourceRoot":"","sources":["../../src/cache/verify-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAIH,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAiD,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACjG,OAAO,KAAK,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAa9F;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,MAAM,GAAE,SAAiC,GACxC,mBAAmB,CAAC,uBAAuB,CAAC,CAc9C;AAED,qDAAqD;AACrD,wBAAgB,gBAAgB,IAAI,IAAI,CAGvC;AAiBD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,sBAAsB,GAC5B;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAOzC"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Verify Cache
3
+ *
4
+ * Persistent on-disk cache for LLM verification batch results
5
+ * (cognium-ai#152, P1 of epic #155). Sits in front of the in-memory
6
+ * `_verifierDedupCache` from verification.ts (#138 step 3) so that
7
+ * identical `(file SHA, pair-set)` requests survive across processes,
8
+ * CI runs, and developer iterations — not just within one process.
9
+ *
10
+ * Built on top of `ClassificationCache<BatchVerificationResult>` (which
11
+ * already carries the cognium-ai#151 model-fingerprint key dimension),
12
+ * with verify-specific key construction:
13
+ *
14
+ * content = SHA-256(code) + '\\0' + pairsFingerprint
15
+ * classifier = 'verify-batch'
16
+ * fingerprint = getPhaseModelFingerprint(config) // includes verify model
17
+ *
18
+ * Cache directory: `.cognium/cache/verify/` (separate from the
19
+ * discovery-phase `.cognium/cache/` so a `rm -rf .cognium/cache/verify/`
20
+ * only nukes verifier entries).
21
+ *
22
+ * Invalidation matrix (acceptance criteria #152):
23
+ * - Edit one file in a project → only that file's entries invalidate
24
+ * (file SHA-256 is part of the cache content key)
25
+ * - Add a new sink to the engine → existing entries still hit if the
26
+ * new sink doesn't change the pair-set for that file
27
+ * - Change verification model → entire cache invalidates
28
+ * (model fingerprint is part of the singleton key)
29
+ *
30
+ * Env toggles:
31
+ * LLM_VERIFY_CACHE=0 → disable persistent verify cache (in-memory
32
+ * dedup from #138 still applies)
33
+ */
34
+ import * as crypto from 'crypto';
35
+ import * as path from 'path';
36
+ import { ClassificationCache } from './classification-cache.js';
37
+ import { getDefaultLLMConfig, getPhaseModelFingerprint } from '../llm/config.js';
38
+ const VERIFY_CACHE_SUBDIR = path.join('.cognium', 'cache', 'verify');
39
+ /**
40
+ * Module-scoped singleton — distinct from the discovery-phase singleton
41
+ * in `classification-cache.ts` so the two phases can carry different
42
+ * model fingerprints (e.g. cheap discovery + strong verifier) without
43
+ * thrashing each other's cache instance.
44
+ */
45
+ let _instance = null;
46
+ let _instanceFingerprint = '';
47
+ /**
48
+ * Get the verify-phase persistent cache, keyed by per-phase model
49
+ * fingerprint. When `LLM_VERIFY_CACHE=0` returns a disabled cache that
50
+ * no-ops on get/set (callers don't need to branch).
51
+ */
52
+ export function getVerifyCache(config = getDefaultLLMConfig()) {
53
+ const enabled = process.env.LLM_VERIFY_CACHE !== '0';
54
+ const fingerprint = getPhaseModelFingerprint(config);
55
+ if (!_instance || _instanceFingerprint !== fingerprint || _instance.getModelFingerprint() !== fingerprint) {
56
+ _instance = new ClassificationCache({
57
+ cacheDir: path.join(process.cwd(), VERIFY_CACHE_SUBDIR),
58
+ modelFingerprint: fingerprint,
59
+ enabled,
60
+ });
61
+ _instanceFingerprint = fingerprint;
62
+ }
63
+ return _instance;
64
+ }
65
+ /** Reset the module-scoped singleton — test-only. */
66
+ export function resetVerifyCache() {
67
+ _instance = null;
68
+ _instanceFingerprint = '';
69
+ }
70
+ /**
71
+ * Stable sort over the source/sink pair-set; never mutates the caller's
72
+ * arrays. Mirrors `_serializeVerifierPairs` in verification.ts so
73
+ * persistent + in-memory dedup agree on what counts as "the same call".
74
+ */
75
+ function serializeVerifyPairs(input) {
76
+ const sources = [...input.sources]
77
+ .map((s) => `${s.line}|${s.type}|${s.variable ?? ''}`)
78
+ .sort();
79
+ const sinks = [...input.sinks]
80
+ .map((s) => `${s.line}|${s.type}|${s.cwe}|${s.method ?? ''}`)
81
+ .sort();
82
+ return JSON.stringify({ sources, sinks });
83
+ }
84
+ /**
85
+ * Compute the `(content, classifier)` tuple for the underlying
86
+ * ClassificationCache. The fingerprint dimension is folded in by the
87
+ * cache itself (see classification-cache.ts:computeKey).
88
+ *
89
+ * Components:
90
+ * - file SHA-256 — file edit invalidates only that file's entry
91
+ * - pairs fingerprint — sort-invariant over the source/sink set
92
+ * - classifier 'verify-batch' — namespace for future
93
+ * 'verify-pair' / 'verify-flow' tiers without collision
94
+ */
95
+ export function computeVerifyCacheKey(input) {
96
+ const fileSha = crypto.createHash('sha256').update(input.code).digest('hex');
97
+ const pairsFp = crypto.createHash('sha256').update(serializeVerifyPairs(input)).digest('hex');
98
+ return {
99
+ content: `${fileSha}\0${pairsFp}`,
100
+ classifier: 'verify-batch',
101
+ };
102
+ }
103
+ //# sourceMappingURL=verify-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-cache.js","sourceRoot":"","sources":["../../src/cache/verify-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,wBAAwB,EAAkB,MAAM,kBAAkB,CAAC;AAGjG,MAAM,mBAAmB,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;AAErE;;;;;GAKG;AACH,IAAI,SAAS,GAAwD,IAAI,CAAC;AAC1E,IAAI,oBAAoB,GAAG,EAAE,CAAC;AAE9B;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,SAAoB,mBAAmB,EAAE;IAEzC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,GAAG,CAAC;IACrD,MAAM,WAAW,GAAG,wBAAwB,CAAC,MAAM,CAAC,CAAC;IAErD,IAAI,CAAC,SAAS,IAAI,oBAAoB,KAAK,WAAW,IAAI,SAAS,CAAC,mBAAmB,EAAE,KAAK,WAAW,EAAE,CAAC;QAC1G,SAAS,GAAG,IAAI,mBAAmB,CAA0B;YAC3D,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,mBAAmB,CAAC;YACvD,gBAAgB,EAAE,WAAW;YAC7B,OAAO;SACR,CAAC,CAAC;QACH,oBAAoB,GAAG,WAAW,CAAC;IACrC,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,gBAAgB;IAC9B,SAAS,GAAG,IAAI,CAAC;IACjB,oBAAoB,GAAG,EAAE,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,SAAS,oBAAoB,CAAC,KAA6B;IACzD,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC;SAC/B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;SACrD,IAAI,EAAE,CAAC;IACV,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;SAC3B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;SAC5D,IAAI,EAAE,CAAC;IACV,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAA6B;IAE7B,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7E,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9F,OAAO;QACL,OAAO,EAAE,GAAG,OAAO,KAAK,OAAO,EAAE;QACjC,UAAU,EAAE,cAAc;KAC3B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Cluster Layer — Foundation
3
+ *
4
+ * Top-of-pipeline file clustering (cognium-ai#154, P4 of epic #155).
5
+ * Groups files in a repo by their static structural fingerprint (CK
6
+ * metrics + import/annotation set) so downstream passes can apply
7
+ * per-cluster policy bundles ("library bootstrap" vs "controller
8
+ * surface" vs "data-access shim") instead of one-size-fits-all
9
+ * discovery/verification.
10
+ *
11
+ * This file ships the **foundation only**:
12
+ * - `extractClusterVector(circleIr)` — pull CK vector from analyze() output
13
+ * - `clusterFiles(inputs)` — deterministic k-medoids, returns
14
+ * `Map<filePath, clusterId>` per acceptance #1
15
+ * - distance functions (Euclidean on normalized CK + Jaccard on
16
+ * imports/annotations) + the k-medoids loop itself
17
+ * - `ClusterPolicy` type so downstream wiring can land in a
18
+ * follow-up without re-declaring shape
19
+ *
20
+ * **Explicitly out of scope here** (tracked as follow-up tickets):
21
+ * - `meta.json.files[*].clusterId` field — needs scan-emit schema
22
+ * migration (Acceptance #3)
23
+ * - top-100 HIGH/CRIT reduction validation (Acceptance #4)
24
+ * - CWE-Bench-Java regression check (Acceptance #5)
25
+ * - `--cluster-policy <profile>` CLI flag in cognium-ai
26
+ * (Acceptance #6)
27
+ * - LLM cluster labeling pass (optional future per the issue)
28
+ *
29
+ * The split is deliberate: P1-P3 of epic #155 were single-shot
30
+ * cache wins; P4's policy bundles + the two benchmark validation
31
+ * gates need empirical tuning that can't complete in a single
32
+ * release cycle. Foundation-first lets cognium-ai start consuming
33
+ * `clusterFiles()` as an opaque grouping signal while the policy
34
+ * resolver is iterated separately.
35
+ *
36
+ * **Determinism:** k-medoids is seeded from a digest of (sorted
37
+ * file paths || vector signature) so re-runs on the same input
38
+ * produce the same cluster assignments. Tie-breaking on equal
39
+ * distances falls back to lexicographic path order.
40
+ */
41
+ import type { CircleIR } from 'circle-ir';
42
+ /**
43
+ * Per-file structural fingerprint used as the clustering signal.
44
+ * CK metrics default to 0 when circle-ir's metric pass didn't run
45
+ * (e.g. procedural-only files, languages without classes).
46
+ */
47
+ export interface ClusterVector {
48
+ cbo: number;
49
+ rfc: number;
50
+ lcom: number;
51
+ dit: number;
52
+ noc: number;
53
+ wmc: number;
54
+ imports: string[];
55
+ publicSurface: number;
56
+ annotations: string[];
57
+ }
58
+ export interface ClusterInput {
59
+ filePath: string;
60
+ vector: ClusterVector;
61
+ }
62
+ export interface ClusterAssignment {
63
+ filePath: string;
64
+ clusterId: number;
65
+ }
66
+ export interface ClusterResult {
67
+ k: number;
68
+ assignments: ClusterAssignment[];
69
+ /** Medoid file path per cluster id (index = clusterId). */
70
+ medoids: string[];
71
+ /** Iterations until convergence (for tuning visibility). */
72
+ iterations: number;
73
+ }
74
+ /**
75
+ * Downstream policy bundle attached to a cluster. Shape locked here
76
+ * so wiring tickets can land without re-negotiating the contract;
77
+ * default policy ("application", everything on) is what consumers
78
+ * should apply until cluster-specific overrides ship.
79
+ */
80
+ export type ContextProfile = 'application' | 'library' | 'framework' | 'tool';
81
+ export interface ClusterPolicy {
82
+ contextProfile: ContextProfile;
83
+ /** Discovery source kinds permitted; empty array = all. */
84
+ sourceWhitelist: string[];
85
+ /** Verification sink classes permitted; empty array = all. */
86
+ sinkWhitelist: string[];
87
+ /** When false, downstream LLM passes skip this cluster entirely. */
88
+ llmEnabled: boolean;
89
+ }
90
+ export declare const DEFAULT_CLUSTER_POLICY: ClusterPolicy;
91
+ /**
92
+ * Extract a ClusterVector from a single-file `CircleIR` analyze()
93
+ * result. Missing metrics default to 0 — the cluster vector is
94
+ * structural, so a procedural file with no CK numbers will land
95
+ * in the "low-complexity" cluster naturally (it's a valid signal,
96
+ * not missing data).
97
+ */
98
+ export declare function extractClusterVector(circleIr: CircleIR): ClusterVector;
99
+ /**
100
+ * Per-axis max-normalize the CK + publicSurface scalars across the
101
+ * input set. Max-normalize (rather than z-score) is deliberate:
102
+ * cluster boundaries should be robust to outliers (one god-class
103
+ * with WMC=400 shouldn't squash everything else into the same
104
+ * cluster), and max-normalize gives a clean [0,1] domain that
105
+ * combines naturally with Jaccard.
106
+ *
107
+ * Returns the maxima so callers can normalize new vectors against
108
+ * the same scale (e.g. for the k-medoids loop on the same set).
109
+ */
110
+ export declare function computeNormalization(inputs: ClusterInput[]): {
111
+ cboMax: number;
112
+ rfcMax: number;
113
+ lcomMax: number;
114
+ ditMax: number;
115
+ nocMax: number;
116
+ wmcMax: number;
117
+ publicSurfaceMax: number;
118
+ };
119
+ /** Euclidean distance on the max-normalized CK + publicSurface scalars. */
120
+ export declare function euclideanCkDistance(a: ClusterVector, b: ClusterVector, norm: ReturnType<typeof computeNormalization>): number;
121
+ /**
122
+ * Jaccard distance on two string sets (1 - |intersection|/|union|).
123
+ * Returns 0 when both sets are empty (semantically: nothing
124
+ * distinguishes them on this axis).
125
+ */
126
+ export declare function jaccardDistance(a: string[], b: string[]): number;
127
+ /**
128
+ * Combined per-pair distance. Weights mirror the issue's signal
129
+ * vector ordering (CK is the primary signal; imports + annotations
130
+ * are tiebreakers). Total is in [0,1] so it composes cleanly with
131
+ * other metric-space algorithms.
132
+ *
133
+ * Weights chosen to sum to 1:
134
+ * 0.6 — CK + publicSurface Euclidean (clamped to 1 by sqrt of 7
135
+ * normalized components, since each (a-b)/max is in [-1,1])
136
+ * 0.2 — imports Jaccard
137
+ * 0.2 — annotations Jaccard
138
+ *
139
+ * Euclidean is divided by sqrt(7) to renormalize to [0,1] before
140
+ * weighting — without this, the CK term can exceed 1 and the total
141
+ * stops being a proper [0,1] distance.
142
+ */
143
+ export declare function mixedDistance(a: ClusterVector, b: ClusterVector, norm: ReturnType<typeof computeNormalization>): number;
144
+ interface KMedoidsOptions {
145
+ /** Desired cluster count. Clamped to `[1, inputs.length]`. */
146
+ k: number;
147
+ /** Maximum iterations before bailing. Default 50. */
148
+ maxIter?: number;
149
+ /** Override the deterministic seed (test-only). */
150
+ seed?: number;
151
+ }
152
+ /**
153
+ * PAM-lite k-medoids on `inputs`.
154
+ *
155
+ * 1. Seed selection ("build" phase): pick the medoid that minimizes
156
+ * total distance to all points, then iteratively pick the point
157
+ * that maximizes minimum distance to existing medoids. This is
158
+ * deterministic given the seed and degenerates gracefully when
159
+ * `inputs.length <= k`.
160
+ * 2. Assignment: each point joins its closest medoid (lex path
161
+ * tiebreak on equal distances).
162
+ * 3. Swap phase: for each cluster, find the point that minimizes
163
+ * intra-cluster distance and promote it to medoid. Repeat until
164
+ * no medoid changes (or `maxIter` hits).
165
+ *
166
+ * Complexity is O(n^2 * iter) on the distance matrix. For the
167
+ * top-100 sweep (n ≈ 5000 per repo at the high end) this is ~25M
168
+ * pairwise computations per iteration — fine for the <2s budget
169
+ * in Acceptance #2 because distances are scalar arithmetic on
170
+ * a 9-dim vector.
171
+ */
172
+ export declare function clusterFiles(inputs: ClusterInput[], options: KMedoidsOptions): ClusterResult;
173
+ export {};
174
+ //# sourceMappingURL=cluster.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../src/cluster/cluster.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,WAAW,CAAC;AAMvD;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,CAAC,EAAE,MAAM,CAAC;IACV,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,2DAA2D;IAC3D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC;AAE9E,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,cAAc,CAAC;IAC/B,2DAA2D;IAC3D,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,8DAA8D;IAC9D,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,oEAAoE;IACpE,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAKpC,CAAC;AAsBF;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,aAAa,CAyDtE;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAYA;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,aAAa,EAChB,CAAC,EAAE,aAAa,EAChB,IAAI,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,GAC5C,MAAM,CAWR;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAShE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAC3B,CAAC,EAAE,aAAa,EAChB,CAAC,EAAE,aAAa,EAChB,IAAI,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,GAC5C,MAAM,CAKR;AAwCD,UAAU,eAAe;IACvB,8DAA8D;IAC9D,CAAC,EAAE,MAAM,CAAC;IACV,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,YAAY,EAAE,EACtB,OAAO,EAAE,eAAe,GACvB,aAAa,CAsJf"}