@ngockhoale/ukit 2.6.7 → 2.6.8
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/manifests/documentation.yaml +77 -6
- package/manifests/hostCapabilities.yaml +49 -0
- package/manifests/instructionRules.yaml +7 -0
- package/package.json +1 -1
- package/scripts/bench/goldTasks.json +38 -0
- package/scripts/bench/runGold.mjs +220 -0
- package/scripts/release/verify-release.mjs +6 -0
- package/src/cli/commands/code.js +182 -0
- package/src/cli/commands/doctor.js +35 -3
- package/src/cli/commands/indexTools.js +102 -1
- package/src/cli/commands/memory.js +137 -0
- package/src/cli/index.js +7 -0
- package/src/core/codeintel/compiler.js +316 -0
- package/src/core/codeintel/diagnostics.js +114 -0
- package/src/core/codeintel/freshness.js +295 -0
- package/src/core/codeintel/impact.js +251 -0
- package/src/core/codeintel/invalidation.js +150 -0
- package/src/core/codeintel/manifest.js +176 -0
- package/src/core/codeintel/packet.js +146 -0
- package/src/core/codeintel/providers.js +201 -0
- package/src/core/codeintel/retriever.js +372 -0
- package/src/core/codeintel/router.js +149 -0
- package/src/core/codeintel/semanticProvider.js +235 -0
- package/src/core/docContracts.js +723 -0
- package/src/core/memory/migrate.js +324 -0
- package/src/core/memory/records.js +172 -0
- package/src/core/memory/retrieval.js +161 -11
- package/src/core/memory/store.js +398 -0
- package/src/core/memory/storeV2.js +171 -0
- package/src/core/memory/storeV2Loader.js +22 -0
- package/src/core/runtimeConfig.js +125 -0
- package/src/core/runtimePaths.js +3 -0
- package/src/index/taskRouting.js +39 -0
- package/templates/.claude/ukit/index/route-task.mjs +40 -0
- package/templates/AGENTS.md +46 -99
- package/templates/CLAUDE.md +46 -99
- package/templates/docs/AI_HANDOFF/tasks/_TEMPLATE.md +5 -0
- package/templates/docs/BUGFIX.md +2 -19
- package/templates/docs/BUG_INDEX.md +43 -0
- package/templates/docs/BUG_METRICS.md +1 -5
- package/templates/docs/BUG_TEMPLATE.md +1 -11
- package/templates/docs/UKIT_INTERNALS.md +4 -0
- package/templates/instructions/core.md +46 -99
- package/templates/ukit/storage/config.json +30 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Memory v2 store — SPEC §3.
|
|
2
|
+
// Single atomic JSON document at <memoryRoot>/v2/records.json
|
|
3
|
+
// ({ schemaVersion: 2, records: [...] }); writes via fileOps.writeJson (tmp+rename).
|
|
4
|
+
// Read ops call ensureMigrated() first so every consumer triggers the lazy
|
|
5
|
+
// v1→v2 migration (SPEC §4); migrate.js is lazy-imported to break the
|
|
6
|
+
// storeV2↔migrate import cycle (runMigration writes via saveRecords).
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import { buildRuntimePaths } from '../runtimePaths.js';
|
|
10
|
+
import { readJsonIfExists, writeJson } from '../fileOps.js';
|
|
11
|
+
import { loadRuntimeConfig } from '../runtimeConfig.js';
|
|
12
|
+
import { createRecord, normalizeRecord } from './records.js';
|
|
13
|
+
|
|
14
|
+
const SCHEMA_VERSION = 2;
|
|
15
|
+
|
|
16
|
+
// Per-process memoization for ensureMigrated — one-shot check per projectRoot.
|
|
17
|
+
const migratedRoots = new Set();
|
|
18
|
+
|
|
19
|
+
async function ensureMigrated(projectRoot) {
|
|
20
|
+
const key = String(projectRoot);
|
|
21
|
+
if (migratedRoots.has(key)) return;
|
|
22
|
+
migratedRoots.add(key); // mark first: a failed/again migration must not loop
|
|
23
|
+
try {
|
|
24
|
+
const paths = buildRuntimePaths(projectRoot);
|
|
25
|
+
let recordsExists = false;
|
|
26
|
+
try {
|
|
27
|
+
await fs.access(paths.memoryV2RecordsPath);
|
|
28
|
+
recordsExists = true;
|
|
29
|
+
} catch {
|
|
30
|
+
recordsExists = false;
|
|
31
|
+
}
|
|
32
|
+
if (recordsExists) return;
|
|
33
|
+
|
|
34
|
+
const config = await loadRuntimeConfig(projectRoot);
|
|
35
|
+
if (config?.memoryV2?.autoMigrate === false) return;
|
|
36
|
+
|
|
37
|
+
const migrate = await import('./migrate.js');
|
|
38
|
+
if (await migrate.needsMigration(projectRoot)) {
|
|
39
|
+
await migrate.runMigration(projectRoot);
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Lazy migration is best-effort: a failed auto-run must not break reads.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isRawRecord(input) {
|
|
47
|
+
return input && typeof input === 'object' && typeof input.id === 'string'
|
|
48
|
+
&& typeof input.status === 'string';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* loadRecords(projectRoot) → record[] — tolerant: invalid entries skipped.
|
|
53
|
+
*/
|
|
54
|
+
export async function loadRecords(projectRoot) {
|
|
55
|
+
await ensureMigrated(projectRoot);
|
|
56
|
+
const { records } = await readStore(projectRoot);
|
|
57
|
+
return records;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readStore(projectRoot) {
|
|
61
|
+
const paths = buildRuntimePaths(projectRoot);
|
|
62
|
+
let doc;
|
|
63
|
+
try {
|
|
64
|
+
doc = await readJsonIfExists(paths.memoryV2RecordsPath);
|
|
65
|
+
} catch {
|
|
66
|
+
return { records: [], invalidSkipped: 1 };
|
|
67
|
+
}
|
|
68
|
+
if (!doc) return { records: [], invalidSkipped: 0 };
|
|
69
|
+
|
|
70
|
+
const rawRecords = Array.isArray(doc.records) ? doc.records : [];
|
|
71
|
+
const records = [];
|
|
72
|
+
let invalidSkipped = Array.isArray(doc.records) ? 0 : 1;
|
|
73
|
+
for (const raw of rawRecords) {
|
|
74
|
+
const normalized = normalizeRecord(raw);
|
|
75
|
+
if (normalized) records.push(normalized);
|
|
76
|
+
else invalidSkipped += 1;
|
|
77
|
+
}
|
|
78
|
+
return { records, invalidSkipped };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* saveRecords(projectRoot, records) → void — atomic write; drops unrecoverable entries.
|
|
83
|
+
*/
|
|
84
|
+
export async function saveRecords(projectRoot, records) {
|
|
85
|
+
const paths = buildRuntimePaths(projectRoot);
|
|
86
|
+
const normalized = (Array.isArray(records) ? records : [])
|
|
87
|
+
.map((raw) => normalizeRecord(raw))
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
await writeJson(paths.memoryV2RecordsPath, {
|
|
90
|
+
schemaVersion: SCHEMA_VERSION,
|
|
91
|
+
records: normalized,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* addRecord(projectRoot, recordInput) → record
|
|
97
|
+
* Accepts either createRecord-style input or a full raw record (normalized).
|
|
98
|
+
*/
|
|
99
|
+
export async function addRecord(projectRoot, recordInput) {
|
|
100
|
+
const record = isRawRecord(recordInput)
|
|
101
|
+
? normalizeRecord(recordInput)
|
|
102
|
+
: createRecord(recordInput);
|
|
103
|
+
if (!record) {
|
|
104
|
+
throw new Error('addRecord: input could not be normalized into a valid record');
|
|
105
|
+
}
|
|
106
|
+
const { records } = await readStore(projectRoot);
|
|
107
|
+
await saveRecords(projectRoot, [...records, record]);
|
|
108
|
+
return record;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const PATCHABLE_FIELDS = ['type', 'scope', 'text', 'provenance', 'confidence',
|
|
112
|
+
'valid_until', 'status', 'project_id', 'meta', 'source_fingerprint'];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* updateRecord(projectRoot, id, patch) → record | null
|
|
116
|
+
*/
|
|
117
|
+
export async function updateRecord(projectRoot, id, patch = {}) {
|
|
118
|
+
await ensureMigrated(projectRoot);
|
|
119
|
+
const { records } = await readStore(projectRoot);
|
|
120
|
+
const index = records.findIndex((r) => r.id === id);
|
|
121
|
+
if (index === -1) return null;
|
|
122
|
+
|
|
123
|
+
const next = { ...records[index] };
|
|
124
|
+
for (const field of PATCHABLE_FIELDS) {
|
|
125
|
+
if (Object.prototype.hasOwnProperty.call(patch, field)) {
|
|
126
|
+
next[field] = patch[field];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const normalized = normalizeRecord(next);
|
|
130
|
+
if (!normalized) {
|
|
131
|
+
throw new Error(`updateRecord: patch produces an invalid record (id ${id})`);
|
|
132
|
+
}
|
|
133
|
+
records[index] = normalized;
|
|
134
|
+
await saveRecords(projectRoot, records);
|
|
135
|
+
return normalized;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* getRecord(projectRoot, id) → record | null
|
|
140
|
+
*/
|
|
141
|
+
export async function getRecord(projectRoot, id) {
|
|
142
|
+
await ensureMigrated(projectRoot);
|
|
143
|
+
const { records } = await readStore(projectRoot);
|
|
144
|
+
return records.find((r) => r.id === id) ?? null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* queryRecords(projectRoot, { type?, scope?, status?, projectId? }) → record[]
|
|
149
|
+
*/
|
|
150
|
+
export async function queryRecords(projectRoot, { type, scope, status, projectId } = {}) {
|
|
151
|
+
const records = await loadRecords(projectRoot);
|
|
152
|
+
return records.filter((r) => (type == null || r.type === type)
|
|
153
|
+
&& (scope == null || r.scope === scope)
|
|
154
|
+
&& (status == null || r.status === status)
|
|
155
|
+
&& (projectId == null || r.project_id === projectId));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* stats(projectRoot) → { total, byType, byStatus, invalidSkipped }
|
|
160
|
+
*/
|
|
161
|
+
export async function stats(projectRoot) {
|
|
162
|
+
await ensureMigrated(projectRoot);
|
|
163
|
+
const { records, invalidSkipped } = await readStore(projectRoot);
|
|
164
|
+
const byType = {};
|
|
165
|
+
const byStatus = {};
|
|
166
|
+
for (const r of records) {
|
|
167
|
+
byType[r.type] = (byType[r.type] ?? 0) + 1;
|
|
168
|
+
byStatus[r.status] = (byStatus[r.status] ?? 0) + 1;
|
|
169
|
+
}
|
|
170
|
+
return { total: records.length, byType, byStatus, invalidSkipped };
|
|
171
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Lazy/defensive bridge to storeV2.js (owned by TASK-002).
|
|
2
|
+
// Real storeV2.queryRecords performs ensureMigrated auto-migration internally —
|
|
3
|
+
// consumers must never call migration themselves; just call this and treat an
|
|
4
|
+
// empty result as "fall back to the legacy memory path".
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* loadV2Records(projectRoot) → record[] | []
|
|
8
|
+
* Returns [] when storeV2 is unavailable, disabled upstream, or the v2 store
|
|
9
|
+
* holds no records yet. Callers decide fallback.
|
|
10
|
+
*/
|
|
11
|
+
export async function loadV2Records(projectRoot, filter = {}) {
|
|
12
|
+
try {
|
|
13
|
+
const storeV2 = await import('./storeV2.js');
|
|
14
|
+
if (typeof storeV2?.queryRecords !== 'function') {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const records = await storeV2.queryRecords(projectRoot, filter);
|
|
18
|
+
return Array.isArray(records) ? records : [];
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -157,6 +157,36 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
157
157
|
cap: 'smart',
|
|
158
158
|
},
|
|
159
159
|
},
|
|
160
|
+
codeIntel: {
|
|
161
|
+
enabled: true,
|
|
162
|
+
router: { enabled: true, defaultMode: 'auto' },
|
|
163
|
+
budgets: {
|
|
164
|
+
peek: 500,
|
|
165
|
+
targeted: 2000,
|
|
166
|
+
impact: 3000,
|
|
167
|
+
explore: 4000,
|
|
168
|
+
deep_flow: 6000,
|
|
169
|
+
analogy: 4000,
|
|
170
|
+
},
|
|
171
|
+
freshness: { guardLevel: 'L0', autoRefresh: false, incremental: true },
|
|
172
|
+
retriever: {
|
|
173
|
+
limit: 20,
|
|
174
|
+
bm25: { k1: 1.2, b: 0.75 },
|
|
175
|
+
merge: 'rrf',
|
|
176
|
+
rrfK: 60,
|
|
177
|
+
weights: { exact: 1.0, symbol: 1.2, bm25: 0.8, semantic: 1.0 },
|
|
178
|
+
},
|
|
179
|
+
impact: { defaultDepth: 2, maxDepth: 4, maxNodes: 200 },
|
|
180
|
+
diagnostics: { enabled: true, timeoutMs: 8000 },
|
|
181
|
+
providers: { semantic: 'null' },
|
|
182
|
+
},
|
|
183
|
+
memoryV2: {
|
|
184
|
+
enabled: true,
|
|
185
|
+
autoMigrate: true,
|
|
186
|
+
episodeTtlDays: 90,
|
|
187
|
+
promotion: { episodeToRuleRequiresApproval: true },
|
|
188
|
+
recall: { maxRecords: 8 },
|
|
189
|
+
},
|
|
160
190
|
memory: {
|
|
161
191
|
enabled: true,
|
|
162
192
|
autoCapture: true,
|
|
@@ -354,6 +384,101 @@ export function validateRuntimeConfig(config) {
|
|
|
354
384
|
}
|
|
355
385
|
}
|
|
356
386
|
|
|
387
|
+
if (!isPlainObject(config.codeIntel)) {
|
|
388
|
+
errors.push('codeIntel must be an object.');
|
|
389
|
+
} else {
|
|
390
|
+
const codeIntel = config.codeIntel;
|
|
391
|
+
pushBooleanError(errors, codeIntel.enabled, 'codeIntel.enabled');
|
|
392
|
+
if (!isPlainObject(codeIntel.router)) {
|
|
393
|
+
errors.push('codeIntel.router must be an object.');
|
|
394
|
+
} else {
|
|
395
|
+
pushBooleanError(errors, codeIntel.router.enabled, 'codeIntel.router.enabled');
|
|
396
|
+
const VALID_ROUTER_MODES = new Set(['auto', 'none', 'peek', 'targeted', 'explore', 'impact', 'deep_flow', 'analogy']);
|
|
397
|
+
if (!VALID_ROUTER_MODES.has(codeIntel.router.defaultMode)) {
|
|
398
|
+
errors.push(`codeIntel.router.defaultMode must be one of: ${[...VALID_ROUTER_MODES].join(', ')}.`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!isPlainObject(codeIntel.budgets)) {
|
|
402
|
+
errors.push('codeIntel.budgets must be an object.');
|
|
403
|
+
} else {
|
|
404
|
+
for (const key of ['peek', 'targeted', 'impact', 'explore', 'deep_flow', 'analogy']) {
|
|
405
|
+
pushPositiveNumberError(errors, codeIntel.budgets[key], `codeIntel.budgets.${key}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (!isPlainObject(codeIntel.freshness)) {
|
|
409
|
+
errors.push('codeIntel.freshness must be an object.');
|
|
410
|
+
} else {
|
|
411
|
+
const VALID_GUARD_LEVELS = new Set(['L0', 'L1', 'L2', 'L3', 'L4']);
|
|
412
|
+
if (!VALID_GUARD_LEVELS.has(codeIntel.freshness.guardLevel)) {
|
|
413
|
+
errors.push(`codeIntel.freshness.guardLevel must be one of: ${[...VALID_GUARD_LEVELS].join(', ')}.`);
|
|
414
|
+
}
|
|
415
|
+
pushBooleanError(errors, codeIntel.freshness.autoRefresh, 'codeIntel.freshness.autoRefresh');
|
|
416
|
+
pushBooleanError(errors, codeIntel.freshness.incremental, 'codeIntel.freshness.incremental');
|
|
417
|
+
}
|
|
418
|
+
if (!isPlainObject(codeIntel.retriever)) {
|
|
419
|
+
errors.push('codeIntel.retriever must be an object.');
|
|
420
|
+
} else {
|
|
421
|
+
pushPositiveNumberError(errors, codeIntel.retriever.limit, 'codeIntel.retriever.limit');
|
|
422
|
+
if (!isPlainObject(codeIntel.retriever.bm25)) {
|
|
423
|
+
errors.push('codeIntel.retriever.bm25 must be an object.');
|
|
424
|
+
} else {
|
|
425
|
+
pushPositiveNumberError(errors, codeIntel.retriever.bm25.k1, 'codeIntel.retriever.bm25.k1');
|
|
426
|
+
pushPositiveNumberError(errors, codeIntel.retriever.bm25.b, 'codeIntel.retriever.bm25.b');
|
|
427
|
+
}
|
|
428
|
+
const VALID_RETRIEVER_MERGES = new Set(['rrf', 'concat']);
|
|
429
|
+
if (!VALID_RETRIEVER_MERGES.has(codeIntel.retriever.merge)) {
|
|
430
|
+
errors.push(`codeIntel.retriever.merge must be one of: ${[...VALID_RETRIEVER_MERGES].join(', ')}.`);
|
|
431
|
+
}
|
|
432
|
+
pushPositiveNumberError(errors, codeIntel.retriever.rrfK, 'codeIntel.retriever.rrfK');
|
|
433
|
+
if (!isPlainObject(codeIntel.retriever.weights)) {
|
|
434
|
+
errors.push('codeIntel.retriever.weights must be an object.');
|
|
435
|
+
} else {
|
|
436
|
+
for (const lane of ['exact', 'symbol', 'bm25', 'semantic']) {
|
|
437
|
+
if (typeof codeIntel.retriever.weights[lane] !== 'number' || Number.isNaN(codeIntel.retriever.weights[lane])) {
|
|
438
|
+
errors.push(`codeIntel.retriever.weights.${lane} must be a number.`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (!isPlainObject(codeIntel.impact)) {
|
|
444
|
+
errors.push('codeIntel.impact must be an object.');
|
|
445
|
+
} else {
|
|
446
|
+
pushPositiveNumberError(errors, codeIntel.impact.defaultDepth, 'codeIntel.impact.defaultDepth');
|
|
447
|
+
pushPositiveNumberError(errors, codeIntel.impact.maxDepth, 'codeIntel.impact.maxDepth');
|
|
448
|
+
pushPositiveNumberError(errors, codeIntel.impact.maxNodes, 'codeIntel.impact.maxNodes');
|
|
449
|
+
}
|
|
450
|
+
if (!isPlainObject(codeIntel.diagnostics)) {
|
|
451
|
+
errors.push('codeIntel.diagnostics must be an object.');
|
|
452
|
+
} else {
|
|
453
|
+
pushBooleanError(errors, codeIntel.diagnostics.enabled, 'codeIntel.diagnostics.enabled');
|
|
454
|
+
pushPositiveNumberError(errors, codeIntel.diagnostics.timeoutMs, 'codeIntel.diagnostics.timeoutMs');
|
|
455
|
+
}
|
|
456
|
+
if (!isPlainObject(codeIntel.providers)) {
|
|
457
|
+
errors.push('codeIntel.providers must be an object.');
|
|
458
|
+
} else {
|
|
459
|
+
pushNonEmptyStringError(errors, codeIntel.providers.semantic, 'codeIntel.providers.semantic');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!isPlainObject(config.memoryV2)) {
|
|
464
|
+
errors.push('memoryV2 must be an object.');
|
|
465
|
+
} else {
|
|
466
|
+
const memoryV2 = config.memoryV2;
|
|
467
|
+
pushBooleanError(errors, memoryV2.enabled, 'memoryV2.enabled');
|
|
468
|
+
pushBooleanError(errors, memoryV2.autoMigrate, 'memoryV2.autoMigrate');
|
|
469
|
+
pushPositiveNumberError(errors, memoryV2.episodeTtlDays, 'memoryV2.episodeTtlDays');
|
|
470
|
+
if (!isPlainObject(memoryV2.promotion)) {
|
|
471
|
+
errors.push('memoryV2.promotion must be an object.');
|
|
472
|
+
} else {
|
|
473
|
+
pushBooleanError(errors, memoryV2.promotion.episodeToRuleRequiresApproval, 'memoryV2.promotion.episodeToRuleRequiresApproval');
|
|
474
|
+
}
|
|
475
|
+
if (!isPlainObject(memoryV2.recall)) {
|
|
476
|
+
errors.push('memoryV2.recall must be an object.');
|
|
477
|
+
} else {
|
|
478
|
+
pushPositiveNumberError(errors, memoryV2.recall.maxRecords, 'memoryV2.recall.maxRecords');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
357
482
|
if (!isPlainObject(config.memory)) {
|
|
358
483
|
errors.push('memory must be an object.');
|
|
359
484
|
} else {
|
package/src/core/runtimePaths.js
CHANGED
|
@@ -4,6 +4,7 @@ export function buildRuntimePaths(projectRoot) {
|
|
|
4
4
|
const runtimeRoot = path.join(projectRoot, '.ukit');
|
|
5
5
|
const storageRoot = path.join(runtimeRoot, 'storage');
|
|
6
6
|
const memoryRoot = path.join(storageRoot, 'memory');
|
|
7
|
+
const memoryV2Dir = path.join(memoryRoot, 'v2');
|
|
7
8
|
const cacheRoot = path.join(storageRoot, 'cache');
|
|
8
9
|
|
|
9
10
|
return {
|
|
@@ -11,6 +12,8 @@ export function buildRuntimePaths(projectRoot) {
|
|
|
11
12
|
storageRoot,
|
|
12
13
|
cacheRoot,
|
|
13
14
|
memoryRoot,
|
|
15
|
+
memoryV2Dir,
|
|
16
|
+
memoryV2RecordsPath: path.join(memoryV2Dir, 'records.json'),
|
|
14
17
|
teeCacheDir: path.join(cacheRoot, 'tee'),
|
|
15
18
|
configPath: path.join(storageRoot, 'config.json'),
|
|
16
19
|
promptCachePath: path.join(cacheRoot, 'prompt-cache.json'),
|
package/src/index/taskRouting.js
CHANGED
|
@@ -14,6 +14,34 @@ import {
|
|
|
14
14
|
|
|
15
15
|
const MAX_ACTIVE_ROUTE_SKILLS = 2;
|
|
16
16
|
|
|
17
|
+
// Declared complexity→docs mapping (DOC-201 FR-001). Canonical declaration lives in
|
|
18
|
+
// manifests/documentation.yaml `context_layers`; this constant is the routing-side copy
|
|
19
|
+
// (the router must not read the registry at runtime — zero new deps). Keep in sync.
|
|
20
|
+
// `queued-task` is omitted v1: docs/TASKS.md does not exist in every repo (SPEC §14).
|
|
21
|
+
// `shared-simple` mirrors the non-trivial layer to match deriveContextMode's FULL lane.
|
|
22
|
+
const CONTEXT_LAYER_DOCS = {
|
|
23
|
+
taskTypes: {
|
|
24
|
+
trivial: [],
|
|
25
|
+
simple: ['docs/MEMORY.md'],
|
|
26
|
+
'non-trivial': ['docs/MEMORY.md', 'docs/PROJECT.md', 'docs/CODE_MAP.md'],
|
|
27
|
+
'shared-simple': ['docs/MEMORY.md', 'docs/PROJECT.md', 'docs/CODE_MAP.md'],
|
|
28
|
+
},
|
|
29
|
+
intents: {
|
|
30
|
+
'open-ended': ['docs/STATUS.md'],
|
|
31
|
+
'open-ended-status': ['docs/STATUS.md'],
|
|
32
|
+
handoff: ['docs/AI_HANDOFF/INDEX.md'],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const CONTEXT_DOCS_MAX = 4;
|
|
36
|
+
|
|
37
|
+
function deriveContextDocs({ taskType = null, intentMode = null } = {}) {
|
|
38
|
+
const docs = [
|
|
39
|
+
...(CONTEXT_LAYER_DOCS.taskTypes[taskType] ?? []),
|
|
40
|
+
...(CONTEXT_LAYER_DOCS.intents[intentMode] ?? []),
|
|
41
|
+
];
|
|
42
|
+
return unique(docs).slice(0, CONTEXT_DOCS_MAX);
|
|
43
|
+
}
|
|
44
|
+
|
|
17
45
|
export async function deriveTaskRoute({
|
|
18
46
|
rootDir = process.cwd(),
|
|
19
47
|
promptText = '',
|
|
@@ -206,6 +234,7 @@ export function buildRouteSummary({
|
|
|
206
234
|
nextAction = null,
|
|
207
235
|
handoffBudget = null,
|
|
208
236
|
worklogBudget = null,
|
|
237
|
+
contextDocs = null,
|
|
209
238
|
} = {}) {
|
|
210
239
|
const autonomyLevel = routingContext.autonomyLevel ?? 'balanced';
|
|
211
240
|
const delegationRecommendation = deriveDelegationRecommendation({
|
|
@@ -264,10 +293,19 @@ export function buildRouteSummary({
|
|
|
264
293
|
);
|
|
265
294
|
const nextActionCommand = compactHelperLane ? null : nextAction?.command ?? null;
|
|
266
295
|
const handoffFile = routingContext.intentMode === 'handoff' ? 'docs/AI_HANDOFF/ACTIVE.md' : null;
|
|
296
|
+
// DOC-201 FR-002: resolved context-layer docs (declared in manifests/documentation.yaml).
|
|
297
|
+
// Explicit override wins; otherwise derive from taskType + intentMode.
|
|
298
|
+
const resolvedContextDocs = ((Array.isArray(contextDocs) ? contextDocs : null)
|
|
299
|
+
?? deriveContextDocs({ taskType, intentMode: routingContext.intentMode ?? null }))
|
|
300
|
+
.slice(0, CONTEXT_DOCS_MAX);
|
|
301
|
+
const docsSegment = resolvedContextDocs.length > 0
|
|
302
|
+
? `docs=[${resolvedContextDocs.slice(0, CONTEXT_DOCS_MAX).map((p) => path.posix.basename(String(p).replaceAll('\\', '/'))).join(',')}]`
|
|
303
|
+
: null;
|
|
267
304
|
const summaryLine = [
|
|
268
305
|
routingContext.taskType ? `task=${routingContext.taskType}` : null,
|
|
269
306
|
handoffFile ? `handoff=${handoffFile}` : null,
|
|
270
307
|
formatCompactSegment('targets', primaryTargets),
|
|
308
|
+
docsSegment,
|
|
271
309
|
formatCompactSegment('tests', relatedTests),
|
|
272
310
|
formatCompactSegment('styles', styleFiles),
|
|
273
311
|
editGuardHint ? `editGuard=${editGuardHint}` : null,
|
|
@@ -303,6 +341,7 @@ export function buildRouteSummary({
|
|
|
303
341
|
nextActionCommand,
|
|
304
342
|
helperHint,
|
|
305
343
|
contextMode,
|
|
344
|
+
contextDocs: resolvedContextDocs,
|
|
306
345
|
line: summaryLine || 'task=unknown',
|
|
307
346
|
};
|
|
308
347
|
}
|
|
@@ -22,6 +22,34 @@ const {
|
|
|
22
22
|
} = indexCore;
|
|
23
23
|
|
|
24
24
|
const MAX_ACTIVE_ROUTE_SKILLS = 2;
|
|
25
|
+
|
|
26
|
+
// Declared complexity→docs mapping (DOC-201 FR-001). Canonical declaration lives in
|
|
27
|
+
// manifests/documentation.yaml `context_layers`; this constant is the routing-side copy
|
|
28
|
+
// (the router must not read the registry at runtime — zero new deps). Keep in sync.
|
|
29
|
+
// `queued-task` is omitted v1: docs/TASKS.md does not exist in every repo (SPEC §14).
|
|
30
|
+
// `shared-simple` mirrors the non-trivial layer to match deriveContextMode's FULL lane.
|
|
31
|
+
const CONTEXT_LAYER_DOCS = {
|
|
32
|
+
taskTypes: {
|
|
33
|
+
trivial: [],
|
|
34
|
+
simple: ['docs/MEMORY.md'],
|
|
35
|
+
'non-trivial': ['docs/MEMORY.md', 'docs/PROJECT.md', 'docs/CODE_MAP.md'],
|
|
36
|
+
'shared-simple': ['docs/MEMORY.md', 'docs/PROJECT.md', 'docs/CODE_MAP.md'],
|
|
37
|
+
},
|
|
38
|
+
intents: {
|
|
39
|
+
'open-ended': ['docs/STATUS.md'],
|
|
40
|
+
'open-ended-status': ['docs/STATUS.md'],
|
|
41
|
+
handoff: ['docs/AI_HANDOFF/INDEX.md'],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const CONTEXT_DOCS_MAX = 4;
|
|
45
|
+
|
|
46
|
+
function deriveContextDocs({ taskType = null, intentMode = null } = {}) {
|
|
47
|
+
const docs = [
|
|
48
|
+
...(CONTEXT_LAYER_DOCS.taskTypes[taskType] ?? []),
|
|
49
|
+
...(CONTEXT_LAYER_DOCS.intents[intentMode] ?? []),
|
|
50
|
+
];
|
|
51
|
+
return unique(docs).slice(0, CONTEXT_DOCS_MAX);
|
|
52
|
+
}
|
|
25
53
|
const STOPWORDS = new Set([
|
|
26
54
|
'the', 'a', 'an', 'and', 'or', 'to', 'for', 'of', 'with', 'in', 'on', 'is', 'are',
|
|
27
55
|
'this', 'that', 'it', 'as', 'by', 'be', 'use', 'using', 'implement', 'fix', 'task',
|
|
@@ -1293,6 +1321,7 @@ function formatDisplayRouteSummary(routeSummary = null, routingContext = {}) {
|
|
|
1293
1321
|
taskSegment,
|
|
1294
1322
|
extractRouteLineSegment(line, 'handoff'),
|
|
1295
1323
|
extractRouteLineSegment(line, 'targets'),
|
|
1324
|
+
extractRouteLineSegment(line, 'docs'),
|
|
1296
1325
|
extractRouteLineSegment(line, 'tests'),
|
|
1297
1326
|
extractRouteLineSegment(line, 'styles'),
|
|
1298
1327
|
policySegment,
|
|
@@ -2108,6 +2137,7 @@ function buildRouteSummary({
|
|
|
2108
2137
|
contextRecommendation = null,
|
|
2109
2138
|
verificationRecommendation = null,
|
|
2110
2139
|
nextAction = null,
|
|
2140
|
+
contextDocs = null,
|
|
2111
2141
|
} = {}) {
|
|
2112
2142
|
const autonomyLevel = routingContext.autonomyLevel ?? 'balanced';
|
|
2113
2143
|
const delegationRecommendation = deriveDelegationRecommendation({
|
|
@@ -2174,10 +2204,19 @@ function buildRouteSummary({
|
|
|
2174
2204
|
const nextActionCommand = compactHelperLane ? null : nextAction?.command ?? null;
|
|
2175
2205
|
const handoffFile = routingContext.intentMode === 'handoff' ? 'docs/AI_HANDOFF/ACTIVE.md' : null;
|
|
2176
2206
|
const visionLane = Boolean(routingContext.visionLane);
|
|
2207
|
+
// DOC-201 FR-002: resolved context-layer docs (declared in manifests/documentation.yaml).
|
|
2208
|
+
// Explicit override wins; otherwise derive from taskType + intentMode.
|
|
2209
|
+
const resolvedContextDocs = ((Array.isArray(contextDocs) ? contextDocs : null)
|
|
2210
|
+
?? deriveContextDocs({ taskType, intentMode: routingContext.intentMode ?? null }))
|
|
2211
|
+
.slice(0, CONTEXT_DOCS_MAX);
|
|
2212
|
+
const docsSegment = resolvedContextDocs.length > 0
|
|
2213
|
+
? `docs=[${resolvedContextDocs.slice(0, CONTEXT_DOCS_MAX).map((p) => path.posix.basename(String(p).replaceAll('\\', '/'))).join(',')}]`
|
|
2214
|
+
: null;
|
|
2177
2215
|
const line = [
|
|
2178
2216
|
routingContext.taskType ? `task=${routingContext.taskType}` : null,
|
|
2179
2217
|
handoffFile ? `handoff=${handoffFile}` : null,
|
|
2180
2218
|
formatCompactSegment('targets', primaryTargets),
|
|
2219
|
+
docsSegment,
|
|
2181
2220
|
formatCompactSegment('tests', relatedTests),
|
|
2182
2221
|
formatCompactSegment('styles', styleFiles),
|
|
2183
2222
|
editGuardHint ? `editGuard=${editGuardHint}` : null,
|
|
@@ -2209,6 +2248,7 @@ function buildRouteSummary({
|
|
|
2209
2248
|
nextActionCommand,
|
|
2210
2249
|
helperHint,
|
|
2211
2250
|
contextMode,
|
|
2251
|
+
contextDocs: resolvedContextDocs,
|
|
2212
2252
|
...(visionLane ? {
|
|
2213
2253
|
visionLane: true,
|
|
2214
2254
|
visionReason: routingContext.visionReason ?? null,
|