@ngockhoale/ukit 2.6.8 → 2.6.9
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 +52 -0
- package/package.json +1 -1
- package/scripts/index/build-index.mjs +2 -1
- package/scripts/index/query-index.mjs +2 -0
- package/src/cli/commands/indexTools.js +5 -0
- package/src/cli/commands/install.js +2 -1
- package/src/core/codeintel/analogy.js +197 -0
- package/src/core/codeintel/cochange.js +205 -0
- package/src/core/codeintel/compiler.js +82 -9
- package/src/core/codeintel/graph.js +291 -0
- package/src/core/codeintel/impact.js +23 -0
- package/src/core/codeintel/packet.js +1 -0
- package/src/core/codeintel/retriever.js +49 -3
- package/src/core/codeintel/summaries.js +194 -0
- package/src/core/codeintel/vectorProvider.js +213 -0
- package/src/core/runtimeConfig.js +50 -2
- package/src/index/buildIndex.js +29 -0
- package/src/index/paths.js +2 -0
- package/templates/ukit/storage/config.json +7 -2
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import { getArtifactPath, INDEX_ARTIFACTS, INDEX_SCHEMA_VERSION } from '../../index/paths.js';
|
|
4
|
+
import { createEdge } from './providers.js';
|
|
5
|
+
|
|
6
|
+
// Dep-free embedding lane (SPEC §2): feature hashing (FNV-1a 32-bit) over word
|
|
7
|
+
// tokens + word-boundary trigrams, L2-normalized, cosine-ranked against the
|
|
8
|
+
// same doc text the BM25 lane uses (file paths + symbol names). No file reads,
|
|
9
|
+
// no deps — real embeddings plug in later through the injectable `loader`
|
|
10
|
+
// seam (config.codeIntel.embedding.provider === 'auto'), degrading to the
|
|
11
|
+
// hashed provider on ANY failure. Never throws.
|
|
12
|
+
|
|
13
|
+
const DEFAULT_DIMENSIONS = 256;
|
|
14
|
+
const DEFAULT_LIMIT = 20;
|
|
15
|
+
|
|
16
|
+
function fnv1a(str) {
|
|
17
|
+
let h = 0x811c9dc5;
|
|
18
|
+
for (let i = 0; i < str.length; i += 1) {
|
|
19
|
+
h ^= str.charCodeAt(i);
|
|
20
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
21
|
+
}
|
|
22
|
+
return h >>> 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function wordTokens(text) {
|
|
26
|
+
return String(text ?? '')
|
|
27
|
+
.split(/[^A-Za-z0-9_$]+/)
|
|
28
|
+
.map((t) => t.toLowerCase())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function trigrams(token) {
|
|
33
|
+
const padded = `#${token}#`;
|
|
34
|
+
const grams = [];
|
|
35
|
+
for (let i = 0; i + 3 <= padded.length; i += 1) {
|
|
36
|
+
grams.push(padded.slice(i, i + 3));
|
|
37
|
+
}
|
|
38
|
+
return grams;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* hashVectorize(text, { dimensions }) → number[] of length `dimensions`,
|
|
43
|
+
* L2-normalized (zero vector for empty/null input). Deterministic.
|
|
44
|
+
*/
|
|
45
|
+
export function hashVectorize(text, { dimensions = DEFAULT_DIMENSIONS } = {}) {
|
|
46
|
+
const dims = Number.isInteger(dimensions) && dimensions > 0 ? dimensions : DEFAULT_DIMENSIONS;
|
|
47
|
+
const vec = new Array(dims).fill(0);
|
|
48
|
+
const tokens = wordTokens(text);
|
|
49
|
+
for (const token of tokens) {
|
|
50
|
+
vec[fnv1a(`w:${token}`) % dims] += 1;
|
|
51
|
+
for (const gram of trigrams(token)) {
|
|
52
|
+
vec[fnv1a(`t:${gram}`) % dims] += 0.1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const norm = Math.sqrt(vec.reduce((sum, v) => sum + v * v, 0));
|
|
56
|
+
if (norm > 0) {
|
|
57
|
+
for (let i = 0; i < dims; i += 1) vec[i] /= norm;
|
|
58
|
+
}
|
|
59
|
+
return vec;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** cosineSimilarity(a, b) → number in [-1, 1]; 0 on dim mismatch/empty. */
|
|
63
|
+
export function cosineSimilarity(a, b) {
|
|
64
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || a.length !== b.length) {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
let dot = 0;
|
|
68
|
+
let na = 0;
|
|
69
|
+
let nb = 0;
|
|
70
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
71
|
+
dot += a[i] * b[i];
|
|
72
|
+
na += a[i] * a[i];
|
|
73
|
+
nb += b[i] * b[i];
|
|
74
|
+
}
|
|
75
|
+
if (na === 0 || nb === 0) return 0;
|
|
76
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function readArtifact(rootDir, name) {
|
|
80
|
+
try {
|
|
81
|
+
const raw = await fs.readFile(getArtifactPath(rootDir, name), 'utf8');
|
|
82
|
+
const parsed = JSON.parse(raw);
|
|
83
|
+
if (parsed?.schemaVersion !== undefined && parsed.schemaVersion !== INDEX_SCHEMA_VERSION) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return parsed;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class HashedEmbeddingProvider {
|
|
93
|
+
constructor({ projectRoot = process.cwd(), dimensions = DEFAULT_DIMENSIONS, embedFn = null } = {}) {
|
|
94
|
+
this.name = 'hashed-vector';
|
|
95
|
+
this.projectRoot = projectRoot;
|
|
96
|
+
this.dimensions = Number.isInteger(dimensions) && dimensions > 0 ? dimensions : DEFAULT_DIMENSIONS;
|
|
97
|
+
this._embedFn = typeof embedFn === 'function' ? embedFn : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async available() {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
capabilities() {
|
|
105
|
+
return { embeddings: true, similarity: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async embed(text) {
|
|
109
|
+
if (this._embedFn) {
|
|
110
|
+
const vec = await this._embedFn(String(text ?? ''));
|
|
111
|
+
if (Array.isArray(vec)) return vec;
|
|
112
|
+
}
|
|
113
|
+
return hashVectorize(text, { dimensions: this.dimensions });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* resolve(query, ctx { limit?, snapshot? }) → edge[] | null.
|
|
118
|
+
* Corpus: files.json paths + symbols.json names — index-bound, no file reads.
|
|
119
|
+
* Missing index → null (caller records 'unavailable'); empty index → [].
|
|
120
|
+
*/
|
|
121
|
+
async resolve(query, ctx = {}) {
|
|
122
|
+
try {
|
|
123
|
+
const [filesArtifact, symbolsArtifact] = await Promise.all([
|
|
124
|
+
readArtifact(this.projectRoot, INDEX_ARTIFACTS.files),
|
|
125
|
+
readArtifact(this.projectRoot, INDEX_ARTIFACTS.symbols),
|
|
126
|
+
]);
|
|
127
|
+
if (!filesArtifact && !symbolsArtifact) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const limit = Number.isInteger(ctx?.limit) && ctx.limit > 0 ? ctx.limit : DEFAULT_LIMIT;
|
|
131
|
+
const queryVec = await this.embed(query);
|
|
132
|
+
const docs = new Map();
|
|
133
|
+
for (const item of filesArtifact?.items ?? []) {
|
|
134
|
+
if (!item?.filePath) continue;
|
|
135
|
+
docs.set(item.filePath, item.filePath);
|
|
136
|
+
}
|
|
137
|
+
for (const sym of symbolsArtifact?.items ?? []) {
|
|
138
|
+
if (!sym?.filePath || !sym?.name) continue;
|
|
139
|
+
docs.set(sym.filePath, `${docs.get(sym.filePath) ?? sym.filePath} ${sym.name}`);
|
|
140
|
+
}
|
|
141
|
+
const snapshot = ctx?.snapshot ?? null;
|
|
142
|
+
const scored = await Promise.all([...docs.entries()].map(async ([filePath, docText]) => ({
|
|
143
|
+
filePath,
|
|
144
|
+
score: cosineSimilarity(queryVec, await this.embed(docText)),
|
|
145
|
+
})));
|
|
146
|
+
return scored
|
|
147
|
+
.filter((entry) => entry.score > 0)
|
|
148
|
+
.sort((a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath))
|
|
149
|
+
.slice(0, limit)
|
|
150
|
+
.map((entry) => createEdge({
|
|
151
|
+
from: String(query ?? ''),
|
|
152
|
+
to: `${entry.filePath}:0`,
|
|
153
|
+
kind: 'vector',
|
|
154
|
+
confidence: entry.score,
|
|
155
|
+
provider: this.name,
|
|
156
|
+
evidence: 'hash-vectorizer',
|
|
157
|
+
snapshot,
|
|
158
|
+
}));
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export class NullEmbeddingProvider {
|
|
166
|
+
constructor() {
|
|
167
|
+
this.name = 'null';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async available() {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
capabilities() {
|
|
175
|
+
return { embeddings: false, similarity: false };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async embed() {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async resolve() {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* createEmbeddingProvider({ projectRoot, config, loader }) → provider.
|
|
189
|
+
* config.codeIntel.embedding.provider:
|
|
190
|
+
* 'hashed' (default) → HashedEmbeddingProvider
|
|
191
|
+
* 'auto' → loader() module exposing embed(); ANY failure → Hashed
|
|
192
|
+
* 'null' → NullEmbeddingProvider
|
|
193
|
+
*/
|
|
194
|
+
export function createEmbeddingProvider({ projectRoot = process.cwd(), config, loader } = {}) {
|
|
195
|
+
const embedding = config?.codeIntel?.embedding;
|
|
196
|
+
const setting = embedding?.provider ?? 'hashed';
|
|
197
|
+
const dimensions = embedding?.dimensions;
|
|
198
|
+
if (setting === 'null') {
|
|
199
|
+
return new NullEmbeddingProvider();
|
|
200
|
+
}
|
|
201
|
+
if (setting === 'auto' && typeof loader === 'function') {
|
|
202
|
+
try {
|
|
203
|
+
const mod = loader();
|
|
204
|
+
if (mod && typeof mod.embed === 'function') {
|
|
205
|
+
return new HashedEmbeddingProvider({ projectRoot, dimensions, embedFn: mod.embed });
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// fall through — degrade to hashed
|
|
209
|
+
}
|
|
210
|
+
return new HashedEmbeddingProvider({ projectRoot, dimensions });
|
|
211
|
+
}
|
|
212
|
+
return new HashedEmbeddingProvider({ projectRoot, dimensions });
|
|
213
|
+
}
|
|
@@ -174,11 +174,17 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
174
174
|
bm25: { k1: 1.2, b: 0.75 },
|
|
175
175
|
merge: 'rrf',
|
|
176
176
|
rrfK: 60,
|
|
177
|
-
weights: { exact: 1.0, symbol: 1.2, bm25: 0.8, semantic: 1.0 },
|
|
177
|
+
weights: { exact: 1.0, symbol: 1.2, bm25: 0.8, semantic: 1.0, vector: 0.6 },
|
|
178
178
|
},
|
|
179
179
|
impact: { defaultDepth: 2, maxDepth: 4, maxNodes: 200 },
|
|
180
180
|
diagnostics: { enabled: true, timeoutMs: 8000 },
|
|
181
181
|
providers: { semantic: 'null' },
|
|
182
|
+
// C30 additive blocks (SPEC §10 — TASK-222 single owner).
|
|
183
|
+
embedding: { provider: 'hashed', dimensions: 256 },
|
|
184
|
+
analogy: { enabled: true, limit: 5 },
|
|
185
|
+
cochange: { enabled: true, maxCommits: 500, minCount: 2, timeoutMs: 5000 },
|
|
186
|
+
summaries: { enabled: true, maxItems: 5 },
|
|
187
|
+
graph: { enabled: true, store: 'json' },
|
|
182
188
|
},
|
|
183
189
|
memoryV2: {
|
|
184
190
|
enabled: true,
|
|
@@ -433,7 +439,7 @@ export function validateRuntimeConfig(config) {
|
|
|
433
439
|
if (!isPlainObject(codeIntel.retriever.weights)) {
|
|
434
440
|
errors.push('codeIntel.retriever.weights must be an object.');
|
|
435
441
|
} else {
|
|
436
|
-
for (const lane of ['exact', 'symbol', 'bm25', 'semantic']) {
|
|
442
|
+
for (const lane of ['exact', 'symbol', 'bm25', 'semantic', 'vector']) {
|
|
437
443
|
if (typeof codeIntel.retriever.weights[lane] !== 'number' || Number.isNaN(codeIntel.retriever.weights[lane])) {
|
|
438
444
|
errors.push(`codeIntel.retriever.weights.${lane} must be a number.`);
|
|
439
445
|
}
|
|
@@ -458,6 +464,48 @@ export function validateRuntimeConfig(config) {
|
|
|
458
464
|
} else {
|
|
459
465
|
pushNonEmptyStringError(errors, codeIntel.providers.semantic, 'codeIntel.providers.semantic');
|
|
460
466
|
}
|
|
467
|
+
// C30 additive blocks (SPEC §10 — TASK-222).
|
|
468
|
+
if (!isPlainObject(codeIntel.embedding)) {
|
|
469
|
+
errors.push('codeIntel.embedding must be an object.');
|
|
470
|
+
} else {
|
|
471
|
+
const VALID_EMBEDDING_PROVIDERS = new Set(['hashed', 'auto', 'null']);
|
|
472
|
+
if (!VALID_EMBEDDING_PROVIDERS.has(codeIntel.embedding.provider)) {
|
|
473
|
+
errors.push(`codeIntel.embedding.provider must be one of: ${[...VALID_EMBEDDING_PROVIDERS].join(', ')}.`);
|
|
474
|
+
}
|
|
475
|
+
const dimensions = codeIntel.embedding.dimensions;
|
|
476
|
+
if (typeof dimensions !== 'number' || !Number.isInteger(dimensions) || dimensions <= 0 || dimensions > 4096) {
|
|
477
|
+
errors.push('codeIntel.embedding.dimensions must be a positive integer <= 4096.');
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (!isPlainObject(codeIntel.analogy)) {
|
|
481
|
+
errors.push('codeIntel.analogy must be an object.');
|
|
482
|
+
} else {
|
|
483
|
+
pushBooleanError(errors, codeIntel.analogy.enabled, 'codeIntel.analogy.enabled');
|
|
484
|
+
pushPositiveNumberError(errors, codeIntel.analogy.limit, 'codeIntel.analogy.limit');
|
|
485
|
+
}
|
|
486
|
+
if (!isPlainObject(codeIntel.cochange)) {
|
|
487
|
+
errors.push('codeIntel.cochange must be an object.');
|
|
488
|
+
} else {
|
|
489
|
+
pushBooleanError(errors, codeIntel.cochange.enabled, 'codeIntel.cochange.enabled');
|
|
490
|
+
pushPositiveNumberError(errors, codeIntel.cochange.maxCommits, 'codeIntel.cochange.maxCommits');
|
|
491
|
+
pushPositiveNumberError(errors, codeIntel.cochange.minCount, 'codeIntel.cochange.minCount');
|
|
492
|
+
pushPositiveNumberError(errors, codeIntel.cochange.timeoutMs, 'codeIntel.cochange.timeoutMs');
|
|
493
|
+
}
|
|
494
|
+
if (!isPlainObject(codeIntel.summaries)) {
|
|
495
|
+
errors.push('codeIntel.summaries must be an object.');
|
|
496
|
+
} else {
|
|
497
|
+
pushBooleanError(errors, codeIntel.summaries.enabled, 'codeIntel.summaries.enabled');
|
|
498
|
+
pushPositiveNumberError(errors, codeIntel.summaries.maxItems, 'codeIntel.summaries.maxItems');
|
|
499
|
+
}
|
|
500
|
+
if (!isPlainObject(codeIntel.graph)) {
|
|
501
|
+
errors.push('codeIntel.graph must be an object.');
|
|
502
|
+
} else {
|
|
503
|
+
pushBooleanError(errors, codeIntel.graph.enabled, 'codeIntel.graph.enabled');
|
|
504
|
+
const VALID_GRAPH_STORES = new Set(['json', 'scip', 'pdg', 'lsp', 'null']);
|
|
505
|
+
if (!VALID_GRAPH_STORES.has(codeIntel.graph.store)) {
|
|
506
|
+
errors.push(`codeIntel.graph.store must be one of: ${[...VALID_GRAPH_STORES].join(', ')}.`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
461
509
|
}
|
|
462
510
|
|
|
463
511
|
if (!isPlainObject(config.memoryV2)) {
|
package/src/index/buildIndex.js
CHANGED
|
@@ -1156,6 +1156,35 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
|
|
|
1156
1156
|
};
|
|
1157
1157
|
}
|
|
1158
1158
|
|
|
1159
|
+
// Derived artifacts (CI-303/CI-305): git-history co-change mining and the
|
|
1160
|
+
// merged code-graph store. Exported for the index CLI/install callers to run
|
|
1161
|
+
// after buildCodeIndex — intentionally NOT inside buildCodeIndex so the core
|
|
1162
|
+
// build's read/write discipline (atomic renames, no secondary artifact reads
|
|
1163
|
+
// on fresh builds) stays untouched. Optional, config-gated, never fatal: a
|
|
1164
|
+
// failure leaves the artifact absent and readers degrade to omitted/empty.
|
|
1165
|
+
export async function buildDerivedIndexArtifacts(absoluteRoot) {
|
|
1166
|
+
const built = { cochange: null, codegraph: null };
|
|
1167
|
+
try {
|
|
1168
|
+
const { loadRuntimeConfig } = await import('../core/runtimeConfig.js');
|
|
1169
|
+
const config = await loadRuntimeConfig(absoluteRoot);
|
|
1170
|
+
const codeIntel = config?.codeIntel ?? {};
|
|
1171
|
+
if (codeIntel.cochange?.enabled !== false) {
|
|
1172
|
+
const { mineCoChanges } = await import('../core/codeintel/cochange.js');
|
|
1173
|
+
built.cochange = await mineCoChanges(absoluteRoot, {
|
|
1174
|
+
maxCommits: codeIntel.cochange?.maxCommits,
|
|
1175
|
+
timeoutMs: codeIntel.cochange?.timeoutMs,
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
if (codeIntel.graph?.enabled !== false) {
|
|
1179
|
+
const { buildGraphArtifact } = await import('../core/codeintel/graph.js');
|
|
1180
|
+
built.codegraph = await buildGraphArtifact(absoluteRoot);
|
|
1181
|
+
}
|
|
1182
|
+
} catch {
|
|
1183
|
+
// Derivation is best-effort; core artifacts already landed.
|
|
1184
|
+
}
|
|
1185
|
+
return built;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1159
1188
|
async function collectFiles(scanRoots, discovery = null) {
|
|
1160
1189
|
const budget = discovery ?? createDiscoveryBudget({});
|
|
1161
1190
|
const result = [];
|
package/src/index/paths.js
CHANGED
|
@@ -192,11 +192,16 @@
|
|
|
192
192
|
"bm25": { "k1": 1.2, "b": 0.75 },
|
|
193
193
|
"merge": "rrf",
|
|
194
194
|
"rrfK": 60,
|
|
195
|
-
"weights": { "exact": 1.0, "symbol": 1.2, "bm25": 0.8, "semantic": 1.0 }
|
|
195
|
+
"weights": { "exact": 1.0, "symbol": 1.2, "bm25": 0.8, "semantic": 1.0, "vector": 0.6 }
|
|
196
196
|
},
|
|
197
197
|
"impact": { "defaultDepth": 2, "maxDepth": 4, "maxNodes": 200 },
|
|
198
198
|
"diagnostics": { "enabled": true, "timeoutMs": 8000 },
|
|
199
|
-
"providers": { "semantic": "null" }
|
|
199
|
+
"providers": { "semantic": "null" },
|
|
200
|
+
"embedding": { "provider": "hashed", "dimensions": 256 },
|
|
201
|
+
"analogy": { "enabled": true, "limit": 5 },
|
|
202
|
+
"cochange": { "enabled": true, "maxCommits": 500, "minCount": 2, "timeoutMs": 5000 },
|
|
203
|
+
"summaries": { "enabled": true, "maxItems": 5 },
|
|
204
|
+
"graph": { "enabled": true, "store": "json" }
|
|
200
205
|
},
|
|
201
206
|
"memoryV2": {
|
|
202
207
|
"enabled": true,
|