@ngockhoale/ukit 2.6.7 → 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.
Files changed (55) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/manifests/documentation.yaml +77 -6
  3. package/manifests/hostCapabilities.yaml +49 -0
  4. package/manifests/instructionRules.yaml +7 -0
  5. package/package.json +1 -1
  6. package/scripts/bench/goldTasks.json +38 -0
  7. package/scripts/bench/runGold.mjs +220 -0
  8. package/scripts/index/build-index.mjs +2 -1
  9. package/scripts/index/query-index.mjs +2 -0
  10. package/scripts/release/verify-release.mjs +6 -0
  11. package/src/cli/commands/code.js +182 -0
  12. package/src/cli/commands/doctor.js +35 -3
  13. package/src/cli/commands/indexTools.js +107 -1
  14. package/src/cli/commands/install.js +2 -1
  15. package/src/cli/commands/memory.js +137 -0
  16. package/src/cli/index.js +7 -0
  17. package/src/core/codeintel/analogy.js +197 -0
  18. package/src/core/codeintel/cochange.js +205 -0
  19. package/src/core/codeintel/compiler.js +389 -0
  20. package/src/core/codeintel/diagnostics.js +114 -0
  21. package/src/core/codeintel/freshness.js +295 -0
  22. package/src/core/codeintel/graph.js +291 -0
  23. package/src/core/codeintel/impact.js +274 -0
  24. package/src/core/codeintel/invalidation.js +150 -0
  25. package/src/core/codeintel/manifest.js +176 -0
  26. package/src/core/codeintel/packet.js +147 -0
  27. package/src/core/codeintel/providers.js +201 -0
  28. package/src/core/codeintel/retriever.js +418 -0
  29. package/src/core/codeintel/router.js +149 -0
  30. package/src/core/codeintel/semanticProvider.js +235 -0
  31. package/src/core/codeintel/summaries.js +194 -0
  32. package/src/core/codeintel/vectorProvider.js +213 -0
  33. package/src/core/docContracts.js +723 -0
  34. package/src/core/memory/migrate.js +324 -0
  35. package/src/core/memory/records.js +172 -0
  36. package/src/core/memory/retrieval.js +161 -11
  37. package/src/core/memory/store.js +398 -0
  38. package/src/core/memory/storeV2.js +171 -0
  39. package/src/core/memory/storeV2Loader.js +22 -0
  40. package/src/core/runtimeConfig.js +173 -0
  41. package/src/core/runtimePaths.js +3 -0
  42. package/src/index/buildIndex.js +29 -0
  43. package/src/index/paths.js +2 -0
  44. package/src/index/taskRouting.js +39 -0
  45. package/templates/.claude/ukit/index/route-task.mjs +40 -0
  46. package/templates/AGENTS.md +46 -99
  47. package/templates/CLAUDE.md +46 -99
  48. package/templates/docs/AI_HANDOFF/tasks/_TEMPLATE.md +5 -0
  49. package/templates/docs/BUGFIX.md +2 -19
  50. package/templates/docs/BUG_INDEX.md +43 -0
  51. package/templates/docs/BUG_METRICS.md +1 -5
  52. package/templates/docs/BUG_TEMPLATE.md +1 -11
  53. package/templates/docs/UKIT_INTERNALS.md +4 -0
  54. package/templates/instructions/core.md +46 -99
  55. package/templates/ukit/storage/config.json +35 -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,42 @@ 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, vector: 0.6 },
178
+ },
179
+ impact: { defaultDepth: 2, maxDepth: 4, maxNodes: 200 },
180
+ diagnostics: { enabled: true, timeoutMs: 8000 },
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' },
188
+ },
189
+ memoryV2: {
190
+ enabled: true,
191
+ autoMigrate: true,
192
+ episodeTtlDays: 90,
193
+ promotion: { episodeToRuleRequiresApproval: true },
194
+ recall: { maxRecords: 8 },
195
+ },
160
196
  memory: {
161
197
  enabled: true,
162
198
  autoCapture: true,
@@ -354,6 +390,143 @@ export function validateRuntimeConfig(config) {
354
390
  }
355
391
  }
356
392
 
393
+ if (!isPlainObject(config.codeIntel)) {
394
+ errors.push('codeIntel must be an object.');
395
+ } else {
396
+ const codeIntel = config.codeIntel;
397
+ pushBooleanError(errors, codeIntel.enabled, 'codeIntel.enabled');
398
+ if (!isPlainObject(codeIntel.router)) {
399
+ errors.push('codeIntel.router must be an object.');
400
+ } else {
401
+ pushBooleanError(errors, codeIntel.router.enabled, 'codeIntel.router.enabled');
402
+ const VALID_ROUTER_MODES = new Set(['auto', 'none', 'peek', 'targeted', 'explore', 'impact', 'deep_flow', 'analogy']);
403
+ if (!VALID_ROUTER_MODES.has(codeIntel.router.defaultMode)) {
404
+ errors.push(`codeIntel.router.defaultMode must be one of: ${[...VALID_ROUTER_MODES].join(', ')}.`);
405
+ }
406
+ }
407
+ if (!isPlainObject(codeIntel.budgets)) {
408
+ errors.push('codeIntel.budgets must be an object.');
409
+ } else {
410
+ for (const key of ['peek', 'targeted', 'impact', 'explore', 'deep_flow', 'analogy']) {
411
+ pushPositiveNumberError(errors, codeIntel.budgets[key], `codeIntel.budgets.${key}`);
412
+ }
413
+ }
414
+ if (!isPlainObject(codeIntel.freshness)) {
415
+ errors.push('codeIntel.freshness must be an object.');
416
+ } else {
417
+ const VALID_GUARD_LEVELS = new Set(['L0', 'L1', 'L2', 'L3', 'L4']);
418
+ if (!VALID_GUARD_LEVELS.has(codeIntel.freshness.guardLevel)) {
419
+ errors.push(`codeIntel.freshness.guardLevel must be one of: ${[...VALID_GUARD_LEVELS].join(', ')}.`);
420
+ }
421
+ pushBooleanError(errors, codeIntel.freshness.autoRefresh, 'codeIntel.freshness.autoRefresh');
422
+ pushBooleanError(errors, codeIntel.freshness.incremental, 'codeIntel.freshness.incremental');
423
+ }
424
+ if (!isPlainObject(codeIntel.retriever)) {
425
+ errors.push('codeIntel.retriever must be an object.');
426
+ } else {
427
+ pushPositiveNumberError(errors, codeIntel.retriever.limit, 'codeIntel.retriever.limit');
428
+ if (!isPlainObject(codeIntel.retriever.bm25)) {
429
+ errors.push('codeIntel.retriever.bm25 must be an object.');
430
+ } else {
431
+ pushPositiveNumberError(errors, codeIntel.retriever.bm25.k1, 'codeIntel.retriever.bm25.k1');
432
+ pushPositiveNumberError(errors, codeIntel.retriever.bm25.b, 'codeIntel.retriever.bm25.b');
433
+ }
434
+ const VALID_RETRIEVER_MERGES = new Set(['rrf', 'concat']);
435
+ if (!VALID_RETRIEVER_MERGES.has(codeIntel.retriever.merge)) {
436
+ errors.push(`codeIntel.retriever.merge must be one of: ${[...VALID_RETRIEVER_MERGES].join(', ')}.`);
437
+ }
438
+ pushPositiveNumberError(errors, codeIntel.retriever.rrfK, 'codeIntel.retriever.rrfK');
439
+ if (!isPlainObject(codeIntel.retriever.weights)) {
440
+ errors.push('codeIntel.retriever.weights must be an object.');
441
+ } else {
442
+ for (const lane of ['exact', 'symbol', 'bm25', 'semantic', 'vector']) {
443
+ if (typeof codeIntel.retriever.weights[lane] !== 'number' || Number.isNaN(codeIntel.retriever.weights[lane])) {
444
+ errors.push(`codeIntel.retriever.weights.${lane} must be a number.`);
445
+ }
446
+ }
447
+ }
448
+ }
449
+ if (!isPlainObject(codeIntel.impact)) {
450
+ errors.push('codeIntel.impact must be an object.');
451
+ } else {
452
+ pushPositiveNumberError(errors, codeIntel.impact.defaultDepth, 'codeIntel.impact.defaultDepth');
453
+ pushPositiveNumberError(errors, codeIntel.impact.maxDepth, 'codeIntel.impact.maxDepth');
454
+ pushPositiveNumberError(errors, codeIntel.impact.maxNodes, 'codeIntel.impact.maxNodes');
455
+ }
456
+ if (!isPlainObject(codeIntel.diagnostics)) {
457
+ errors.push('codeIntel.diagnostics must be an object.');
458
+ } else {
459
+ pushBooleanError(errors, codeIntel.diagnostics.enabled, 'codeIntel.diagnostics.enabled');
460
+ pushPositiveNumberError(errors, codeIntel.diagnostics.timeoutMs, 'codeIntel.diagnostics.timeoutMs');
461
+ }
462
+ if (!isPlainObject(codeIntel.providers)) {
463
+ errors.push('codeIntel.providers must be an object.');
464
+ } else {
465
+ pushNonEmptyStringError(errors, codeIntel.providers.semantic, 'codeIntel.providers.semantic');
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
+ }
509
+ }
510
+
511
+ if (!isPlainObject(config.memoryV2)) {
512
+ errors.push('memoryV2 must be an object.');
513
+ } else {
514
+ const memoryV2 = config.memoryV2;
515
+ pushBooleanError(errors, memoryV2.enabled, 'memoryV2.enabled');
516
+ pushBooleanError(errors, memoryV2.autoMigrate, 'memoryV2.autoMigrate');
517
+ pushPositiveNumberError(errors, memoryV2.episodeTtlDays, 'memoryV2.episodeTtlDays');
518
+ if (!isPlainObject(memoryV2.promotion)) {
519
+ errors.push('memoryV2.promotion must be an object.');
520
+ } else {
521
+ pushBooleanError(errors, memoryV2.promotion.episodeToRuleRequiresApproval, 'memoryV2.promotion.episodeToRuleRequiresApproval');
522
+ }
523
+ if (!isPlainObject(memoryV2.recall)) {
524
+ errors.push('memoryV2.recall must be an object.');
525
+ } else {
526
+ pushPositiveNumberError(errors, memoryV2.recall.maxRecords, 'memoryV2.recall.maxRecords');
527
+ }
528
+ }
529
+
357
530
  if (!isPlainObject(config.memory)) {
358
531
  errors.push('memory must be an object.');
359
532
  } else {
@@ -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'),
@@ -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 = [];
@@ -11,6 +11,8 @@ export const INDEX_ARTIFACTS = {
11
11
  archetypes: 'archetypes.json',
12
12
  relations: 'relations.json',
13
13
  analogs: 'analogs.json',
14
+ cochange: 'cochange.json',
15
+ codegraph: 'codegraph.json',
14
16
  };
15
17
 
16
18
  export const INDEX_SCHEMA_VERSION = 8;
@@ -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,