@ngockhoale/ukit 3.0.8 → 3.0.10
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 +18 -1
- package/manifests/documentation.yaml +11 -0
- package/package.json +1 -1
- package/scripts/audit/decision-coverage.mjs +29 -2
- package/scripts/bench/data-foundation.mjs +52 -3
- package/scripts/bench/decision-runtime-baseline.mjs +427 -0
- package/scripts/bench/decision-runtime-metrics.mjs +67 -0
- package/scripts/bench/decision-runtime-variant.mjs +626 -0
- package/scripts/bench/memory-ablation.mjs +495 -0
- package/scripts/bench/memory-baseline.mjs +596 -0
- package/scripts/bench/memory-bench.mjs +661 -0
- package/scripts/bench/memory-canary.mjs +321 -0
- package/scripts/bench/memory-corpus.mjs +354 -0
- package/scripts/bench/memory-gate.mjs +389 -0
- package/scripts/bench/memory-metrics.mjs +179 -0
- package/scripts/bench/parallel-agents.mjs +33 -11
- package/scripts/bench/recorder-overhead.mjs +204 -0
- package/scripts/bench/sqlite-spike.mjs +451 -0
- package/scripts/measure-decision-gateway.mjs +306 -0
- package/scripts/perf/audit-perf.mjs +35 -17
- package/src/bug/triageBug.js +4 -3
- package/src/cli/commands/memory.js +357 -63
- package/src/context/detectProjectContext.js +11 -1
- package/src/core/agentRuntime/adapters.js +254 -0
- package/src/core/agentRuntime/artifacts.js +192 -0
- package/src/core/agentRuntime/completionGate.js +176 -0
- package/src/core/agentRuntime/context.js +149 -0
- package/src/core/agentRuntime/contract.js +247 -0
- package/src/core/agentRuntime/diagnostics.js +244 -0
- package/src/core/agentRuntime/evaluation.js +163 -0
- package/src/core/agentRuntime/eventStore.js +404 -0
- package/src/core/agentRuntime/liveness.js +60 -0
- package/src/core/agentRuntime/planCompiler.js +322 -0
- package/src/core/agentRuntime/promotion.js +53 -0
- package/src/core/agentRuntime/qualityComparison.js +112 -0
- package/src/core/agentRuntime/recovery.js +266 -0
- package/src/core/agentRuntime/resourcePolicy.js +78 -0
- package/src/core/agentRuntime/runtimeSupport.js +237 -0
- package/src/core/agentRuntime/supervisor.js +565 -0
- package/src/core/agentRuntime/vmEngine.js +621 -0
- package/src/core/codeintel/analogy.js +3 -2
- package/src/core/experiments/dynamicWorkflow.js +17 -2
- package/src/core/fileOps.js +21 -3
- package/src/core/memory/deltaOverlays.js +75 -30
- package/src/core/memory/learningCandidates.js +93 -48
- package/src/core/memory/memoryFlags.js +83 -0
- package/src/core/memory/memoryFreshness.js +190 -0
- package/src/core/memory/memoryHit.js +144 -0
- package/src/core/memory/migrate.js +69 -189
- package/src/core/memory/migrateMapping.js +232 -0
- package/src/core/memory/mutateMemory.js +323 -0
- package/src/core/memory/policy.js +96 -0
- package/src/core/memory/projectIdentity.js +266 -0
- package/src/core/memory/recordIndex.js +178 -0
- package/src/core/memory/recordStore.js +133 -20
- package/src/core/memory/records.js +144 -6
- package/src/core/memory/retrieval.js +259 -125
- package/src/core/memory/store.js +16 -5
- package/src/core/memory/storeBackup.js +226 -0
- package/src/core/memory/storeV2.js +63 -26
- package/src/core/memory/storeV2Loader.js +30 -12
- package/src/core/memory/userMemory.js +38 -20
- package/src/core/memory/writeClassification.js +161 -0
- package/src/core/memory/writeGuard.js +129 -0
- package/src/core/observability/adapters/hookTelemetryAdapter.js +90 -0
- package/src/core/observability/analytics/cohorts.js +148 -0
- package/src/core/observability/analytics/storeDigest.js +163 -0
- package/src/core/observability/evaluation/experimentPlan.js +95 -0
- package/src/core/observability/evaluation/findings.js +99 -0
- package/src/core/observability/evaluation/optimizationKnowledge.js +10 -1
- package/src/core/observability/evaluation/perturbation.js +273 -0
- package/src/core/observability/evaluation/replay.js +7 -1
- package/src/core/observability/evaluation/scorecard.js +23 -3
- package/src/core/observability/rollout.js +11 -7
- package/src/core/observability/schema/compatibility.js +135 -0
- package/src/core/observability/schema/registry.js +99 -0
- package/src/core/observability/schema/validate.js +7 -0
- package/src/core/observability/support/import.js +53 -9
- package/src/core/observability/support/paths.js +13 -3
- package/src/core/observability/support/projector.js +148 -12
- package/src/core/output/index.js +12 -2
- package/src/core/runtimeConfig.js +83 -0
- package/src/core/runtimePaths.js +3 -0
- package/src/core/sensitiveValueScanner.js +40 -0
- package/src/core/token/index.js +40 -3
- package/src/decision/client.js +37 -13
- package/src/decision/protocol.js +1 -1
- package/src/decision/registry.js +5 -3
- package/src/decision/runtimeDecide.js +242 -0
- package/src/decision/runtimeFilter.js +150 -0
- package/src/decision/runtimeScheduler.js +239 -0
- package/src/index/buildIndex.js +13 -12
- package/src/index/queryIndex.js +35 -14
- package/src/index/relatedTests.js +50 -8
- package/src/index/resolveContext.js +9 -4
- package/src/manifest/selectItems.js +7 -3
- package/src/render/instructionRenderer.js +17 -5
- package/template_project/.claude/ukit/index/lib/index-core.mjs +94 -39
- package/template_project/.claude/ukit/index/route-task.mjs +121 -19
- package/template_project/.claude/ukit/index/unic-decision.mjs +28 -13
- package/template_project/.claude/ukit/runtime/memory-flags.mjs +51 -0
- package/template_project/.claude/ukit/runtime/memory-freshness.mjs +155 -0
- package/template_project/.claude/ukit/runtime/memory-policy.mjs +286 -0
- package/template_project/.claude/ukit/runtime/output-compression.mjs +3 -0
- package/template_project/.claude/ukit/runtime/reinject-context.mjs +145 -14
package/src/index/queryIndex.js
CHANGED
|
@@ -23,6 +23,17 @@ function setBoundedCacheEntry(map, key, value, maxEntries = MAX_CACHE_ENTRIES) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Returns the artifact's item rows as plain objects, dropping poisoned rows
|
|
27
|
+
// (null, undefined, primitives). Hand-corrupted or truncated `.cache/index/*.json`
|
|
28
|
+
// files can carry `items: [null]` or `items: "corrupted"` — every consumer that
|
|
29
|
+
// dereferences `item.filePath` must see only object rows.
|
|
30
|
+
function safeItems(artifact) {
|
|
31
|
+
if (!artifact || typeof artifact !== 'object' || !Array.isArray(artifact.items)) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
return artifact.items.filter((entry) => entry && typeof entry === 'object');
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
export async function queryCodeIndex({ rootDir = process.cwd(), query, limit = 5 } = {}) {
|
|
27
38
|
const normalizedQuery = String(query ?? '').trim();
|
|
28
39
|
if (!normalizedQuery) {
|
|
@@ -54,7 +65,7 @@ export async function queryCodeIndex({ rootDir = process.cwd(), query, limit = 5
|
|
|
54
65
|
|
|
55
66
|
const directScores = new Map();
|
|
56
67
|
|
|
57
|
-
for (const file of files
|
|
68
|
+
for (const file of safeItems(files)) {
|
|
58
69
|
const filePath = file.filePath;
|
|
59
70
|
if (isLikelyTestFilePath(filePath) && !includeLikelyTestFiles) {
|
|
60
71
|
continue;
|
|
@@ -161,7 +172,7 @@ export async function getFileOutline({ rootDir = process.cwd(), filePath, limit
|
|
|
161
172
|
return Object.freeze([]);
|
|
162
173
|
}
|
|
163
174
|
|
|
164
|
-
const rows = (safeSymbolsArtifact
|
|
175
|
+
const rows = safeItems(safeSymbolsArtifact)
|
|
165
176
|
.filter((item) => item?.filePath === filePath && item?.line !== null && item?.line !== undefined)
|
|
166
177
|
.filter((item) => item?.type !== 'callable' || isFunctionLikeSignature(item?.signature))
|
|
167
178
|
.sort((a, b) => a.line - b.line);
|
|
@@ -226,10 +237,10 @@ async function loadQuerySearchBundle(rootDir) {
|
|
|
226
237
|
.then(([files, symbols]) => {
|
|
227
238
|
const safeFiles = ensureArtifact(files);
|
|
228
239
|
const safeSymbols = ensureArtifact(symbols);
|
|
229
|
-
const symbolMap = groupBy(safeSymbols
|
|
240
|
+
const symbolMap = groupBy(safeItems(safeSymbols), (item) => item.filePath);
|
|
230
241
|
return {
|
|
231
242
|
files: safeFiles,
|
|
232
|
-
searchDescriptorsByFile: buildFileSearchDescriptors(safeFiles
|
|
243
|
+
searchDescriptorsByFile: buildFileSearchDescriptors(safeItems(safeFiles), symbolMap),
|
|
233
244
|
};
|
|
234
245
|
})
|
|
235
246
|
.catch((error) => {
|
|
@@ -263,24 +274,24 @@ async function loadQuerySupportBundle(rootDir) {
|
|
|
263
274
|
const safeTestsMap = ensureArtifact(testsMap);
|
|
264
275
|
const safeHotspots = ensureArtifact(hotspots);
|
|
265
276
|
const safeArchetypes = ensureArtifact(archetypes);
|
|
266
|
-
const indexedFileSet = new Set((safeFiles
|
|
267
|
-
const aliasContextPromise = importsNeedAliasContext(safeImports
|
|
277
|
+
const indexedFileSet = new Set(safeItems(safeFiles).map((item) => item.filePath));
|
|
278
|
+
const aliasContextPromise = importsNeedAliasContext(safeItems(safeImports))
|
|
268
279
|
? loadImportAliasContext({ rootDir: absoluteRoot })
|
|
269
280
|
: Promise.resolve(null);
|
|
270
281
|
return aliasContextPromise.then((importAliasContext) => {
|
|
271
282
|
const { resolvedImportsBySource, importersByTarget } = buildResolvedImportGraphs(
|
|
272
283
|
absoluteRoot,
|
|
273
|
-
safeImports
|
|
284
|
+
safeItems(safeImports),
|
|
274
285
|
indexedFileSet,
|
|
275
286
|
importAliasContext,
|
|
276
287
|
);
|
|
277
288
|
|
|
278
289
|
return {
|
|
279
|
-
hotspotScores: new Map((safeHotspots
|
|
280
|
-
testsMapBySource: new Map((safeTestsMap
|
|
290
|
+
hotspotScores: new Map(safeItems(safeHotspots).map((item) => [item.filePath, item.count])),
|
|
291
|
+
testsMapBySource: new Map(safeItems(safeTestsMap).map((item) => [item.sourceFile, item.tests ?? []])),
|
|
281
292
|
resolvedImportsBySource,
|
|
282
293
|
importersByTarget,
|
|
283
|
-
archetypeByFile: new Map((safeArchetypes
|
|
294
|
+
archetypeByFile: new Map(safeItems(safeArchetypes).map((item) => [item.filePath, item.archetype])),
|
|
284
295
|
};
|
|
285
296
|
});
|
|
286
297
|
})
|
|
@@ -607,11 +618,15 @@ export async function queryAnalog({ rootDir = process.cwd(), filePath, limit = 5
|
|
|
607
618
|
const analogPromise = readArtifact(absoluteRoot, INDEX_ARTIFACTS.analogs)
|
|
608
619
|
.then((analogs) => {
|
|
609
620
|
const safeAnalogs = ensureArtifact(analogs);
|
|
610
|
-
const entry = (safeAnalogs
|
|
621
|
+
const entry = safeItems(safeAnalogs).find((item) => item.filePath === normalizedFilePath);
|
|
611
622
|
if (!entry) {
|
|
612
623
|
return Object.freeze([]);
|
|
613
624
|
}
|
|
614
|
-
|
|
625
|
+
// `entry.analogs` is an item-row field that can be hand-corrupted into a
|
|
626
|
+
// truthy non-array (e.g. analogs: 42) — `?.slice` and `?? []` do not fire
|
|
627
|
+
// on those, so coerce before freezeAnalogResults' .map.
|
|
628
|
+
const entryAnalogs = Array.isArray(entry.analogs) ? entry.analogs : [];
|
|
629
|
+
return freezeAnalogResults(entryAnalogs.slice(0, limit));
|
|
615
630
|
})
|
|
616
631
|
.catch((error) => {
|
|
617
632
|
ANALOG_RESULT_CACHE.delete(analogCacheKey);
|
|
@@ -675,8 +690,14 @@ function ensureArtifact(artifact) {
|
|
|
675
690
|
}
|
|
676
691
|
// A schema-current artifact whose `items` is not an array (truncated or
|
|
677
692
|
// hand-corrupted JSON) must degrade to an empty item list — callers iterate
|
|
678
|
-
// `items` unconditionally and a non-array value crashes the query path.
|
|
679
|
-
|
|
693
|
+
// `items` unconditionally and a non-array value crashes the query path. Rows
|
|
694
|
+
// that are not objects (null entries from partial writes) are dropped too.
|
|
695
|
+
return {
|
|
696
|
+
...artifact,
|
|
697
|
+
items: Array.isArray(artifact.items)
|
|
698
|
+
? artifact.items.filter((entry) => entry && typeof entry === 'object')
|
|
699
|
+
: [],
|
|
700
|
+
};
|
|
680
701
|
}
|
|
681
702
|
|
|
682
703
|
function escapeJsonString(value) {
|
|
@@ -123,8 +123,12 @@ export function buildRelatedTestLookups({
|
|
|
123
123
|
relationsArtifact = { items: [] },
|
|
124
124
|
} = {}) {
|
|
125
125
|
return {
|
|
126
|
-
analogsMap: new Map(
|
|
127
|
-
|
|
126
|
+
analogsMap: new Map(
|
|
127
|
+
safeItems(analogsArtifact).map((item) => [item.filePath, safeArray(item.analogs)]),
|
|
128
|
+
),
|
|
129
|
+
relationsMap: new Map(
|
|
130
|
+
safeItems(relationsArtifact).map((item) => [item.filePath, normalizeRelations(item.relations)]),
|
|
131
|
+
),
|
|
128
132
|
};
|
|
129
133
|
}
|
|
130
134
|
|
|
@@ -148,7 +152,7 @@ export function inferRelatedTestsFromArtifacts({
|
|
|
148
152
|
|
|
149
153
|
for (const [candidateRank, candidateFile] of normalizedCandidates.entries()) {
|
|
150
154
|
const rankPenalty = candidateRank * 5;
|
|
151
|
-
const relations = relationsMap.get(candidateFile)
|
|
155
|
+
const relations = normalizeRelations(relationsMap.get(candidateFile));
|
|
152
156
|
|
|
153
157
|
addTestsFromSource({
|
|
154
158
|
sourceFilePath: candidateFile,
|
|
@@ -158,7 +162,7 @@ export function inferRelatedTestsFromArtifacts({
|
|
|
158
162
|
reason: `direct test for ${candidateFile}`,
|
|
159
163
|
});
|
|
160
164
|
|
|
161
|
-
for (const siblingFile of (relations.siblings
|
|
165
|
+
for (const siblingFile of safeArray(relations.siblings).slice(0, 3)) {
|
|
162
166
|
addTestsFromSource({
|
|
163
167
|
sourceFilePath: siblingFile,
|
|
164
168
|
relationsMap,
|
|
@@ -168,7 +172,9 @@ export function inferRelatedTestsFromArtifacts({
|
|
|
168
172
|
});
|
|
169
173
|
}
|
|
170
174
|
|
|
171
|
-
for (const analog of (analogsMap.get(candidateFile)
|
|
175
|
+
for (const analog of safeArray(analogsMap.get(candidateFile))
|
|
176
|
+
.filter((analogRow) => analogRow && typeof analogRow.filePath === 'string')
|
|
177
|
+
.slice(0, 3)) {
|
|
172
178
|
addTestsFromSource({
|
|
173
179
|
sourceFilePath: analog.filePath,
|
|
174
180
|
relationsMap,
|
|
@@ -179,7 +185,7 @@ export function inferRelatedTestsFromArtifacts({
|
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
for (const { key, label } of RELATED_TEST_RELATION_TYPES) {
|
|
182
|
-
for (const relatedFile of (relations[key]
|
|
188
|
+
for (const relatedFile of safeArray(relations[key]).slice(0, 2)) {
|
|
183
189
|
addTestsFromSource({
|
|
184
190
|
sourceFilePath: relatedFile,
|
|
185
191
|
relationsMap,
|
|
@@ -209,7 +215,7 @@ async function readArtifact(rootDir, artifactName) {
|
|
|
209
215
|
RELATED_TEST_ARTIFACT_CACHE,
|
|
210
216
|
artifactPath,
|
|
211
217
|
fs.readFile(artifactPath, 'utf8')
|
|
212
|
-
.then((content) => JSON.parse(content))
|
|
218
|
+
.then((content) => ensureArtifact(JSON.parse(content)))
|
|
213
219
|
.catch(() => {
|
|
214
220
|
RELATED_TEST_ARTIFACT_CACHE.delete(artifactPath);
|
|
215
221
|
return { items: [] };
|
|
@@ -220,6 +226,42 @@ async function readArtifact(rootDir, artifactName) {
|
|
|
220
226
|
return RELATED_TEST_ARTIFACT_CACHE.get(artifactPath);
|
|
221
227
|
}
|
|
222
228
|
|
|
229
|
+
// Mirrors ensureArtifact in queryIndex.js: a shape-corrupt artifact (non-object
|
|
230
|
+
// root, non-array `items`, or null item rows) degrades to an empty item list so
|
|
231
|
+
// triage/context survive hand-edited or truncated `.cache/index/*.json` files.
|
|
232
|
+
function ensureArtifact(artifact) {
|
|
233
|
+
if (!artifact || typeof artifact !== 'object') {
|
|
234
|
+
return { items: [] };
|
|
235
|
+
}
|
|
236
|
+
return { ...artifact, items: safeItems(artifact) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function safeItems(artifact) {
|
|
240
|
+
if (!artifact || typeof artifact !== 'object' || !Array.isArray(artifact.items)) {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
return artifact.items.filter((entry) => entry && typeof entry === 'object');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Item-row fields (`analogs`, `relations.*`) can be hand-edited into non-array
|
|
247
|
+
// shapes (e.g. analogs: "corrupted"). `?? []` does not fire on truthy
|
|
248
|
+
// non-arrays, so coerce when building the lookup maps and again at consumers —
|
|
249
|
+
// inferRelatedTestsFromArtifacts also accepts foreign maps.
|
|
250
|
+
function safeArray(value) {
|
|
251
|
+
return Array.isArray(value) ? value : [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function normalizeRelations(relations) {
|
|
255
|
+
if (!relations || typeof relations !== 'object') {
|
|
256
|
+
return {};
|
|
257
|
+
}
|
|
258
|
+
const normalized = { ...relations };
|
|
259
|
+
for (const key of ['tests', 'siblings', 'styles', ...RELATED_TEST_RELATION_TYPES.map(({ key }) => key)]) {
|
|
260
|
+
normalized[key] = safeArray(normalized[key]);
|
|
261
|
+
}
|
|
262
|
+
return normalized;
|
|
263
|
+
}
|
|
264
|
+
|
|
223
265
|
function addTestsFromSource({
|
|
224
266
|
sourceFilePath,
|
|
225
267
|
relationsMap,
|
|
@@ -231,7 +273,7 @@ function addTestsFromSource({
|
|
|
231
273
|
return;
|
|
232
274
|
}
|
|
233
275
|
|
|
234
|
-
const tests = relationsMap.get(sourceFilePath)?.tests
|
|
276
|
+
const tests = safeArray(relationsMap.get(sourceFilePath)?.tests);
|
|
235
277
|
for (const testFilePath of tests) {
|
|
236
278
|
if (!suggestions.has(testFilePath)) {
|
|
237
279
|
suggestions.set(testFilePath, {
|
|
@@ -103,11 +103,13 @@ export async function resolveContext({
|
|
|
103
103
|
const rels = relationsMap.get(primary) ?? {};
|
|
104
104
|
|
|
105
105
|
// Related tests
|
|
106
|
-
for (const testPath of rels.tests
|
|
106
|
+
for (const testPath of Array.isArray(rels.tests) ? rels.tests : []) {
|
|
107
107
|
addExplainedFile(result, 'relatedTests', testPath, `test linked to ${primary}`);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
// `styles` is a relations field that can carry a truthy non-array after
|
|
111
|
+
// hand-corruption (relations.styles: 42) — `?? []` does not fire on it.
|
|
112
|
+
for (const stylePath of Array.isArray(rels.styles) ? rels.styles : []) {
|
|
111
113
|
addExplainedFile(result, 'styleFiles', stylePath, `style imported by ${primary}`);
|
|
112
114
|
}
|
|
113
115
|
}
|
|
@@ -160,8 +162,11 @@ export async function resolveContext({
|
|
|
160
162
|
|
|
161
163
|
// Step 3: Find analog files for primary targets
|
|
162
164
|
for (const primary of result.primaryTargets.slice(0, 2)) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
+
const rawAnalogs = analogsMap.get(primary);
|
|
166
|
+
const analogs = Array.isArray(rawAnalogs) ? rawAnalogs : [];
|
|
167
|
+
for (const analog of analogs
|
|
168
|
+
.filter((analogRow) => analogRow && typeof analogRow.filePath === 'string')
|
|
169
|
+
.slice(0, 3)) {
|
|
165
170
|
if (result.primaryTargets.includes(analog.filePath)) {
|
|
166
171
|
continue;
|
|
167
172
|
}
|
|
@@ -18,8 +18,12 @@ export const OPTIONAL_ADAPTER_ITEM_IDS = new Set([
|
|
|
18
18
|
'multi-omp-agent-ukit-vision-analyst',
|
|
19
19
|
]);
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// Resolve installation order over the selected items, auto-including required
|
|
22
|
+
// dependencies that were filtered out of the selection (e.g. by pack or adapter
|
|
23
|
+
// filtering). `requires` is a hard dependency, so a dep that exists in the
|
|
24
|
+
// manifest is pulled in transitively; only deps absent from `allItems` throw.
|
|
25
|
+
function resolveItemOrder(items, allItems) {
|
|
26
|
+
const itemById = new Map((allItems ?? items).map((item) => [item.id, item]));
|
|
23
27
|
const visiting = new Set();
|
|
24
28
|
const visited = new Set();
|
|
25
29
|
const ordered = [];
|
|
@@ -81,5 +85,5 @@ export function selectManifestItems({ items, detectedPacks, selectedAdapterItemI
|
|
|
81
85
|
return true;
|
|
82
86
|
});
|
|
83
87
|
|
|
84
|
-
return resolveItemOrder(selected);
|
|
88
|
+
return resolveItemOrder(selected, items);
|
|
85
89
|
}
|
|
@@ -183,19 +183,30 @@ const SOURCE_FILES = {
|
|
|
183
183
|
/**
|
|
184
184
|
* Read layout + sources + repo-vars + package.json version from disk, render
|
|
185
185
|
* all outputs, and (write=true) write each target. Returns Map<target, content>.
|
|
186
|
+
* FR-015: every input read/parse failure is rethrown as a typed
|
|
187
|
+
* `Failed to read render input <rel>: <code|message>` naming the rel path.
|
|
188
|
+
* FR-016: target parent dirs are created before each write.
|
|
186
189
|
*/
|
|
187
190
|
export async function renderInstructionsFromDisk({ repoRoot, write = true } = {}) {
|
|
188
191
|
const abs = (p) => path.join(repoRoot, p);
|
|
189
|
-
const
|
|
190
|
-
|
|
192
|
+
const readInput = (rel, parse) => {
|
|
193
|
+
try {
|
|
194
|
+
return parse(fs.readFileSync(abs(rel), 'utf8'));
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const reason = err && err.code ? err.code : err && err.message ? err.message : String(err);
|
|
197
|
+
throw new Error(`Failed to read render input ${rel}: ${reason}`);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const layout = readInput('template_project/instructions/layout.yaml', (text) =>
|
|
201
|
+
YAML.parse(text),
|
|
191
202
|
);
|
|
192
203
|
const repoVars =
|
|
193
|
-
|
|
194
|
-
const pkg =
|
|
204
|
+
readInput('template_project/instructions/repo-vars.yaml', (text) => YAML.parse(text)) || {};
|
|
205
|
+
const pkg = readInput('package.json', (text) => JSON.parse(text));
|
|
195
206
|
|
|
196
207
|
const sourceTexts = {};
|
|
197
208
|
for (const [name, rel] of Object.entries(SOURCE_FILES)) {
|
|
198
|
-
sourceTexts[name] =
|
|
209
|
+
sourceTexts[name] = readInput(rel, (text) => text);
|
|
199
210
|
}
|
|
200
211
|
|
|
201
212
|
const vars = { ...repoVars, ukit: { ...(repoVars.ukit || {}), version: pkg.version } };
|
|
@@ -203,6 +214,7 @@ export async function renderInstructionsFromDisk({ repoRoot, write = true } = {}
|
|
|
203
214
|
|
|
204
215
|
if (write) {
|
|
205
216
|
for (const [target, content] of rendered) {
|
|
217
|
+
fs.mkdirSync(path.dirname(abs(target)), { recursive: true });
|
|
206
218
|
fs.writeFileSync(abs(target), content);
|
|
207
219
|
}
|
|
208
220
|
}
|
|
@@ -314,9 +314,9 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
314
314
|
&& Array.isArray(previousCallsArtifact.items);
|
|
315
315
|
|
|
316
316
|
if (canReuseParsedArtifacts) {
|
|
317
|
-
const previousSymbolsByPath = groupBy(previousSymbolsArtifact.items ?? [], (item) => item
|
|
318
|
-
const previousImportsByPath = groupBy(previousImportsArtifact.items ?? [], (item) => item
|
|
319
|
-
const previousCallsByPath = groupBy(previousCallsArtifact.items ?? [], (item) => item
|
|
317
|
+
const previousSymbolsByPath = groupBy(previousSymbolsArtifact.items ?? [], (item) => item?.filePath);
|
|
318
|
+
const previousImportsByPath = groupBy(previousImportsArtifact.items ?? [], (item) => item?.from);
|
|
319
|
+
const previousCallsByPath = groupBy(previousCallsArtifact.items ?? [], (item) => item?.filePath);
|
|
320
320
|
|
|
321
321
|
for (const filePath of reusableCodeFiles) {
|
|
322
322
|
symbols.push(...(previousSymbolsByPath.get(filePath) ?? []));
|
|
@@ -888,9 +888,9 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
|
|
|
888
888
|
}
|
|
889
889
|
const styleFilePaths = styleEntries.filter(Boolean).map((entry) => normalizeRelative(absoluteRoot, entry.absolutePath));
|
|
890
890
|
|
|
891
|
-
const previousSymbolsByPath = groupBy(symbolsArtifact.items ?? [], (item) => item
|
|
892
|
-
const previousImportsByPath = groupBy(importsArtifact.items ?? [], (item) => item
|
|
893
|
-
const previousCallsByPath = groupBy(callsArtifact.items ?? [], (item) => item
|
|
891
|
+
const previousSymbolsByPath = groupBy(symbolsArtifact.items ?? [], (item) => item?.filePath);
|
|
892
|
+
const previousImportsByPath = groupBy(importsArtifact.items ?? [], (item) => item?.from);
|
|
893
|
+
const previousCallsByPath = groupBy(callsArtifact.items ?? [], (item) => item?.filePath);
|
|
894
894
|
|
|
895
895
|
const parsedSet = new Set(parsedFiles);
|
|
896
896
|
const codeFiles = mergedFileRecords.filter((record) => CODE_EXTENSIONS.has(record.ext));
|
|
@@ -1149,7 +1149,7 @@ export async function getFileOutline({ rootDir = process.cwd(), filePath, limit
|
|
|
1149
1149
|
return Object.freeze([]);
|
|
1150
1150
|
}
|
|
1151
1151
|
|
|
1152
|
-
const rows = (safeSymbolsArtifact
|
|
1152
|
+
const rows = safeItems(safeSymbolsArtifact)
|
|
1153
1153
|
.filter((item) => item?.filePath === filePath && item?.line !== null && item?.line !== undefined)
|
|
1154
1154
|
.filter((item) => item?.type !== 'callable' || isFunctionLikeSignature(item?.signature))
|
|
1155
1155
|
.sort((a, b) => a.line - b.line);
|
|
@@ -1196,7 +1196,7 @@ export async function queryCodeIndex({ rootDir = process.cwd(), query, limit = 5
|
|
|
1196
1196
|
const includeLikelyTestFiles = shouldIncludeLikelyTestFiles(queryDescriptor);
|
|
1197
1197
|
|
|
1198
1198
|
const directScores = new Map();
|
|
1199
|
-
for (const file of files
|
|
1199
|
+
for (const file of safeItems(files)) {
|
|
1200
1200
|
const filePath = file.filePath;
|
|
1201
1201
|
if (isLikelyTestFilePath(filePath) && !includeLikelyTestFiles) continue;
|
|
1202
1202
|
const searchDescriptor = searchDescriptorsByFile.get(filePath) ?? createFileSearchDescriptor({ filePath, symbols: [] });
|
|
@@ -1275,9 +1275,13 @@ export async function queryAnalog({ rootDir = process.cwd(), filePath, limit = 5
|
|
|
1275
1275
|
const analogPromise = readArtifact(absoluteRoot, INDEX_ARTIFACTS.analogs)
|
|
1276
1276
|
.then((analogs) => {
|
|
1277
1277
|
const safeAnalogs = ensureArtifact(analogs);
|
|
1278
|
-
const entry = (safeAnalogs
|
|
1278
|
+
const entry = safeItems(safeAnalogs).find((item) => item.filePath === normalizedFilePath);
|
|
1279
1279
|
if (!entry) return Object.freeze([]);
|
|
1280
|
-
|
|
1280
|
+
// `entry.analogs` is an item-row field that can be hand-corrupted into a
|
|
1281
|
+
// truthy non-array (e.g. analogs: 42) — `?.slice` and `?? []` do not fire
|
|
1282
|
+
// on those, so coerce before freezeAnalogResults' .map.
|
|
1283
|
+
const entryAnalogs = Array.isArray(entry.analogs) ? entry.analogs : [];
|
|
1284
|
+
return freezeAnalogResults(entryAnalogs.slice(0, limit));
|
|
1281
1285
|
})
|
|
1282
1286
|
.catch((error) => {
|
|
1283
1287
|
ANALOG_RESULT_CACHE.delete(analogCacheKey);
|
|
@@ -1336,14 +1340,31 @@ function freezeQueryResults(results) {
|
|
|
1336
1340
|
function freezeAnalogResults(results) {
|
|
1337
1341
|
return Object.freeze(results.map((item) => Object.freeze({ ...item })));
|
|
1338
1342
|
}
|
|
1343
|
+
// Returns the artifact's item rows as plain objects, dropping poisoned rows
|
|
1344
|
+
// (null, undefined, primitives). Hand-corrupted or truncated `.cache/index/*.json`
|
|
1345
|
+
// files can carry `items: [null]` or `items: "corrupted"` — every consumer that
|
|
1346
|
+
// dereferences `item.filePath` must see only object rows.
|
|
1347
|
+
function safeItems(artifact) {
|
|
1348
|
+
if (!artifact || typeof artifact !== 'object' || !Array.isArray(artifact.items)) {
|
|
1349
|
+
return [];
|
|
1350
|
+
}
|
|
1351
|
+
return artifact.items.filter((entry) => entry && typeof entry === 'object');
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1339
1354
|
function ensureArtifact(artifact) {
|
|
1340
1355
|
if (!artifact || typeof artifact !== 'object') {
|
|
1341
1356
|
return { items: [] };
|
|
1342
1357
|
}
|
|
1343
1358
|
// A schema-current artifact whose `items` is not an array (truncated or
|
|
1344
1359
|
// hand-corrupted JSON) must degrade to an empty item list — callers iterate
|
|
1345
|
-
// `items` unconditionally and a non-array value crashes the query path.
|
|
1346
|
-
|
|
1360
|
+
// `items` unconditionally and a non-array value crashes the query path. Rows
|
|
1361
|
+
// that are not objects (null entries from partial writes) are dropped too.
|
|
1362
|
+
return {
|
|
1363
|
+
...artifact,
|
|
1364
|
+
items: Array.isArray(artifact.items)
|
|
1365
|
+
? artifact.items.filter((entry) => entry && typeof entry === 'object')
|
|
1366
|
+
: [],
|
|
1367
|
+
};
|
|
1347
1368
|
}
|
|
1348
1369
|
|
|
1349
1370
|
function escapeJsonString(value) {
|
|
@@ -1359,6 +1380,8 @@ const TASK_TYPE_BUDGETS = {
|
|
|
1359
1380
|
'non-trivial': { minFiles: 4, maxFiles: 8 },
|
|
1360
1381
|
};
|
|
1361
1382
|
|
|
1383
|
+
const TRIVIAL_SIGNALS = ['typo', 'label', 'text', 'rename', 'color', 'spacing', 'toggle', 'config', 'comment'];
|
|
1384
|
+
|
|
1362
1385
|
const RISKY_SIGNALS = [
|
|
1363
1386
|
'auth', 'authentication', 'authorization', 'security', 'migration', 'uninstall',
|
|
1364
1387
|
'password', 'token', 'permission', 'delete all', 'drop table', 'race', 'flaky',
|
|
@@ -1449,10 +1472,12 @@ export async function resolveContext({
|
|
|
1449
1472
|
|
|
1450
1473
|
for (const primary of result.primaryTargets) {
|
|
1451
1474
|
const rels = relationsMap.get(primary) ?? {};
|
|
1452
|
-
for (const testPath of rels.tests
|
|
1475
|
+
for (const testPath of Array.isArray(rels.tests) ? rels.tests : []) {
|
|
1453
1476
|
addExplainedFile(result, 'relatedTests', testPath, `test linked to ${primary}`);
|
|
1454
1477
|
}
|
|
1455
|
-
|
|
1478
|
+
// `styles` is a relations field that can carry a truthy non-array after
|
|
1479
|
+
// hand-corruption (relations.styles: 42) — `?? []` does not fire on it.
|
|
1480
|
+
for (const stylePath of Array.isArray(rels.styles) ? rels.styles : []) {
|
|
1456
1481
|
addExplainedFile(result, 'styleFiles', stylePath, `style imported by ${primary}`);
|
|
1457
1482
|
}
|
|
1458
1483
|
}
|
|
@@ -1501,8 +1526,11 @@ export async function resolveContext({
|
|
|
1501
1526
|
});
|
|
1502
1527
|
|
|
1503
1528
|
for (const primary of result.primaryTargets.slice(0, 2)) {
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1529
|
+
const rawAnalogs = analogsMap.get(primary);
|
|
1530
|
+
const analogs = Array.isArray(rawAnalogs) ? rawAnalogs : [];
|
|
1531
|
+
for (const analog of analogs
|
|
1532
|
+
.filter((analogRow) => analogRow && typeof analogRow.filePath === 'string')
|
|
1533
|
+
.slice(0, 3)) {
|
|
1506
1534
|
if (result.primaryTargets.includes(analog.filePath)) continue;
|
|
1507
1535
|
addExplainedFile(
|
|
1508
1536
|
result,
|
|
@@ -1696,8 +1724,12 @@ function buildRelatedTestLookups({
|
|
|
1696
1724
|
relationsArtifact = { items: [] },
|
|
1697
1725
|
} = {}) {
|
|
1698
1726
|
return {
|
|
1699
|
-
analogsMap: new Map(
|
|
1700
|
-
|
|
1727
|
+
analogsMap: new Map(
|
|
1728
|
+
safeItems(analogsArtifact).map((item) => [item.filePath, safeArray(item.analogs)]),
|
|
1729
|
+
),
|
|
1730
|
+
relationsMap: new Map(
|
|
1731
|
+
safeItems(relationsArtifact).map((item) => [item.filePath, normalizeRelations(item.relations)]),
|
|
1732
|
+
),
|
|
1701
1733
|
};
|
|
1702
1734
|
}
|
|
1703
1735
|
|
|
@@ -1721,7 +1753,7 @@ function inferRelatedTestsFromArtifacts({
|
|
|
1721
1753
|
|
|
1722
1754
|
for (const [candidateRank, candidateFile] of normalizedCandidates.entries()) {
|
|
1723
1755
|
const rankPenalty = candidateRank * 5;
|
|
1724
|
-
const relations = relationsMap.get(candidateFile)
|
|
1756
|
+
const relations = normalizeRelations(relationsMap.get(candidateFile));
|
|
1725
1757
|
|
|
1726
1758
|
addTestsFromSource({
|
|
1727
1759
|
sourceFilePath: candidateFile,
|
|
@@ -1731,7 +1763,7 @@ function inferRelatedTestsFromArtifacts({
|
|
|
1731
1763
|
reason: `direct test for ${candidateFile}`,
|
|
1732
1764
|
});
|
|
1733
1765
|
|
|
1734
|
-
for (const siblingFile of (relations.siblings
|
|
1766
|
+
for (const siblingFile of safeArray(relations.siblings).slice(0, 3)) {
|
|
1735
1767
|
addTestsFromSource({
|
|
1736
1768
|
sourceFilePath: siblingFile,
|
|
1737
1769
|
relationsMap,
|
|
@@ -1741,7 +1773,9 @@ function inferRelatedTestsFromArtifacts({
|
|
|
1741
1773
|
});
|
|
1742
1774
|
}
|
|
1743
1775
|
|
|
1744
|
-
for (const analog of (analogsMap.get(candidateFile)
|
|
1776
|
+
for (const analog of safeArray(analogsMap.get(candidateFile))
|
|
1777
|
+
.filter((analogRow) => analogRow && typeof analogRow.filePath === 'string')
|
|
1778
|
+
.slice(0, 3)) {
|
|
1745
1779
|
addTestsFromSource({
|
|
1746
1780
|
sourceFilePath: analog.filePath,
|
|
1747
1781
|
relationsMap,
|
|
@@ -1752,7 +1786,7 @@ function inferRelatedTestsFromArtifacts({
|
|
|
1752
1786
|
}
|
|
1753
1787
|
|
|
1754
1788
|
for (const { key, label } of RELATED_TEST_RELATION_TYPES) {
|
|
1755
|
-
for (const relatedFile of (relations[key]
|
|
1789
|
+
for (const relatedFile of safeArray(relations[key]).slice(0, 2)) {
|
|
1756
1790
|
addTestsFromSource({
|
|
1757
1791
|
sourceFilePath: relatedFile,
|
|
1758
1792
|
relationsMap,
|
|
@@ -1785,7 +1819,7 @@ function addTestsFromSource({
|
|
|
1785
1819
|
return;
|
|
1786
1820
|
}
|
|
1787
1821
|
|
|
1788
|
-
const tests = relationsMap.get(sourceFilePath)?.tests
|
|
1822
|
+
const tests = safeArray(relationsMap.get(sourceFilePath)?.tests);
|
|
1789
1823
|
for (const testFilePath of tests) {
|
|
1790
1824
|
if (!suggestions.has(testFilePath)) {
|
|
1791
1825
|
suggestions.set(testFilePath, {
|
|
@@ -1803,6 +1837,25 @@ function addTestsFromSource({
|
|
|
1803
1837
|
}
|
|
1804
1838
|
}
|
|
1805
1839
|
|
|
1840
|
+
// Item-row fields (`analogs`, `relations.*`) can be hand-edited into non-array
|
|
1841
|
+
// shapes (e.g. analogs: "corrupted"). `?? []` does not fire on truthy
|
|
1842
|
+
// non-arrays, so coerce when building the lookup maps and again at consumers —
|
|
1843
|
+
// inferRelatedTestsFromArtifacts also accepts foreign maps.
|
|
1844
|
+
function safeArray(value) {
|
|
1845
|
+
return Array.isArray(value) ? value : [];
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function normalizeRelations(relations) {
|
|
1849
|
+
if (!relations || typeof relations !== 'object') {
|
|
1850
|
+
return {};
|
|
1851
|
+
}
|
|
1852
|
+
const normalized = { ...relations };
|
|
1853
|
+
for (const key of ['tests', 'siblings', 'styles', ...RELATED_TEST_RELATION_TYPES.map(({ key }) => key)]) {
|
|
1854
|
+
normalized[key] = safeArray(normalized[key]);
|
|
1855
|
+
}
|
|
1856
|
+
return normalized;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1806
1859
|
// ── Bug Triage ──
|
|
1807
1860
|
|
|
1808
1861
|
export async function triageBug({ rootDir = process.cwd(), signature } = {}) {
|
|
@@ -1837,9 +1890,10 @@ export async function triageBug({ rootDir = process.cwd(), signature } = {}) {
|
|
|
1837
1890
|
limit: 3,
|
|
1838
1891
|
});
|
|
1839
1892
|
|
|
1840
|
-
const
|
|
1841
|
-
|
|
1842
|
-
|
|
1893
|
+
const topAnalogs = top ? relatedArtifacts?.analogsMap.get(top.filePath) : null;
|
|
1894
|
+
const analogFiles = (Array.isArray(topAnalogs) ? topAnalogs : [])
|
|
1895
|
+
.filter((analog) => analog && typeof analog.filePath === 'string')
|
|
1896
|
+
.slice(0, 2);
|
|
1843
1897
|
|
|
1844
1898
|
return {
|
|
1845
1899
|
signature,
|
|
@@ -1960,7 +2014,7 @@ const ARCHETYPE_RULES = [
|
|
|
1960
2014
|
];
|
|
1961
2015
|
|
|
1962
2016
|
function classifyArchetypes(fileRecords, symbols) {
|
|
1963
|
-
const symbolNamesByPath = groupBy(symbols, (s) => s
|
|
2017
|
+
const symbolNamesByPath = groupBy(symbols, (s) => s?.filePath);
|
|
1964
2018
|
|
|
1965
2019
|
return fileRecords
|
|
1966
2020
|
.filter((f) => CODE_EXTENSIONS.has(f.ext))
|
|
@@ -1980,12 +2034,13 @@ function classifyArchetypes(fileRecords, symbols) {
|
|
|
1980
2034
|
|
|
1981
2035
|
if (!archetype) {
|
|
1982
2036
|
const fileSymbols = symbolNamesByPath.get(file.filePath) ?? [];
|
|
1983
|
-
const hasDefaultExport = fileSymbols.some((s) => s
|
|
2037
|
+
const hasDefaultExport = fileSymbols.some((s) => s?.name === 'default');
|
|
1984
2038
|
const hasComponentSignal = file.ext === '.vue'
|
|
1985
2039
|
|| fileSymbols.some((s) =>
|
|
1986
|
-
s
|
|
1987
|
-
|| s
|
|
1988
|
-
|
|
2040
|
+
s?.type === 'component-name'
|
|
2041
|
+
|| (typeof s?.name === 'string'
|
|
2042
|
+
&& (s.name.toLowerCase().includes('component')
|
|
2043
|
+
|| s.name.toLowerCase().includes('page'))),
|
|
1989
2044
|
);
|
|
1990
2045
|
|
|
1991
2046
|
if ((hasDefaultExport || file.ext === '.vue') && hasComponentSignal) {
|
|
@@ -2875,10 +2930,10 @@ async function loadQuerySearchBundle(rootDir) {
|
|
|
2875
2930
|
.then(([files, symbols]) => {
|
|
2876
2931
|
const safeFiles = ensureArtifact(files);
|
|
2877
2932
|
const safeSymbols = ensureArtifact(symbols);
|
|
2878
|
-
const symbolMap = groupBy(safeSymbols
|
|
2933
|
+
const symbolMap = groupBy(safeItems(safeSymbols), (item) => item.filePath);
|
|
2879
2934
|
return {
|
|
2880
2935
|
files: safeFiles,
|
|
2881
|
-
searchDescriptorsByFile: buildFileSearchDescriptors(safeFiles
|
|
2936
|
+
searchDescriptorsByFile: buildFileSearchDescriptors(safeItems(safeFiles), symbolMap),
|
|
2882
2937
|
};
|
|
2883
2938
|
})
|
|
2884
2939
|
.catch((error) => {
|
|
@@ -2911,23 +2966,23 @@ async function loadQuerySupportBundle(rootDir) {
|
|
|
2911
2966
|
const safeTestsMap = ensureArtifact(testsMap);
|
|
2912
2967
|
const safeHotspots = ensureArtifact(hotspots);
|
|
2913
2968
|
const safeArchetypes = ensureArtifact(archetypes);
|
|
2914
|
-
const indexedFileSet = new Set((safeFiles
|
|
2915
|
-
const importAliasContext = importsNeedAliasContext(safeImports
|
|
2969
|
+
const indexedFileSet = new Set(safeItems(safeFiles).map((item) => item.filePath));
|
|
2970
|
+
const importAliasContext = importsNeedAliasContext(safeItems(safeImports))
|
|
2916
2971
|
? await loadImportAliasContext({ rootDir: absoluteRoot })
|
|
2917
2972
|
: null;
|
|
2918
2973
|
const { resolvedImportsBySource, importersByTarget } = buildResolvedImportGraphs(
|
|
2919
2974
|
absoluteRoot,
|
|
2920
|
-
safeImports
|
|
2975
|
+
safeItems(safeImports),
|
|
2921
2976
|
indexedFileSet,
|
|
2922
2977
|
importAliasContext,
|
|
2923
2978
|
);
|
|
2924
2979
|
|
|
2925
2980
|
return {
|
|
2926
|
-
hotspotScores: new Map((safeHotspots
|
|
2927
|
-
testsMapBySource: new Map((safeTestsMap
|
|
2981
|
+
hotspotScores: new Map(safeItems(safeHotspots).map((item) => [item.filePath, item.count])),
|
|
2982
|
+
testsMapBySource: new Map(safeItems(safeTestsMap).map((item) => [item.sourceFile, item.tests ?? []])),
|
|
2928
2983
|
resolvedImportsBySource,
|
|
2929
2984
|
importersByTarget,
|
|
2930
|
-
archetypeByFile: new Map((safeArchetypes
|
|
2985
|
+
archetypeByFile: new Map(safeItems(safeArchetypes).map((item) => [item.filePath, item.archetype])),
|
|
2931
2986
|
};
|
|
2932
2987
|
})
|
|
2933
2988
|
.catch((error) => {
|