@ngockhoale/ukit 2.6.6 → 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.
Files changed (59) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +40 -177
  3. package/manifests/documentation.yaml +143 -15
  4. package/manifests/hostCapabilities.yaml +49 -0
  5. package/manifests/instructionRules.yaml +383 -0
  6. package/manifests/platform.full.yaml +15 -0
  7. package/package.json +3 -1
  8. package/scripts/bench/goldTasks.json +38 -0
  9. package/scripts/bench/runGold.mjs +220 -0
  10. package/scripts/docs/render-instructions.mjs +42 -0
  11. package/scripts/release/verify-release.mjs +6 -0
  12. package/src/cli/commands/code.js +182 -0
  13. package/src/cli/commands/doctor.js +35 -3
  14. package/src/cli/commands/indexTools.js +102 -1
  15. package/src/cli/commands/memory.js +137 -0
  16. package/src/cli/index.js +7 -0
  17. package/src/core/codeintel/compiler.js +316 -0
  18. package/src/core/codeintel/diagnostics.js +114 -0
  19. package/src/core/codeintel/freshness.js +295 -0
  20. package/src/core/codeintel/impact.js +251 -0
  21. package/src/core/codeintel/invalidation.js +150 -0
  22. package/src/core/codeintel/manifest.js +176 -0
  23. package/src/core/codeintel/packet.js +146 -0
  24. package/src/core/codeintel/providers.js +201 -0
  25. package/src/core/codeintel/retriever.js +372 -0
  26. package/src/core/codeintel/router.js +149 -0
  27. package/src/core/codeintel/semanticProvider.js +235 -0
  28. package/src/core/docContracts.js +723 -0
  29. package/src/core/memory/migrate.js +324 -0
  30. package/src/core/memory/records.js +172 -0
  31. package/src/core/memory/retrieval.js +161 -11
  32. package/src/core/memory/store.js +398 -0
  33. package/src/core/memory/storeV2.js +171 -0
  34. package/src/core/memory/storeV2Loader.js +22 -0
  35. package/src/core/projectImportant.js +1 -1
  36. package/src/core/runtimeConfig.js +125 -0
  37. package/src/core/runtimePaths.js +3 -0
  38. package/src/core/uninstall.js +1 -1
  39. package/src/index/taskRouting.js +39 -0
  40. package/src/render/instructionRenderer.js +226 -0
  41. package/templates/.claude/ukit/index/route-task.mjs +40 -0
  42. package/templates/.gitignore +2 -2
  43. package/templates/.omp/RULES.md +1 -0
  44. package/templates/AGENTS.md +89 -218
  45. package/templates/CLAUDE.md +85 -212
  46. package/templates/docs/AI_HANDOFF/tasks/_TEMPLATE.md +5 -0
  47. package/templates/docs/BUGFIX.md +2 -19
  48. package/templates/docs/BUG_INDEX.md +43 -0
  49. package/templates/docs/BUG_METRICS.md +1 -5
  50. package/templates/docs/BUG_TEMPLATE.md +1 -11
  51. package/templates/docs/UKIT_INTERNALS.md +223 -0
  52. package/templates/instructions/core.md +157 -0
  53. package/templates/instructions/layout.yaml +149 -0
  54. package/templates/instructions/overlays/agents.md +15 -0
  55. package/templates/instructions/overlays/claude.md +3 -0
  56. package/templates/instructions/overlays/omp-rules.md +74 -0
  57. package/templates/instructions/overlays/repo.md +9 -0
  58. package/templates/instructions/repo-vars.yaml +23 -0
  59. package/templates/ukit/storage/config.json +30 -0
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ // DOC-102 renderer CLI (SPEC §8.2).
3
+ // node scripts/docs/render-instructions.mjs --write (default) regenerate all outputs
4
+ // node scripts/docs/render-instructions.mjs --check print DRIFT <path> per mismatch, exit 1
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ import {
9
+ renderInstructionsFromDisk,
10
+ checkRenderedInstructions,
11
+ } from '../../src/render/instructionRenderer.js';
12
+
13
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
14
+
15
+ const USAGE = 'usage: render-instructions.mjs [--write|--check]';
16
+
17
+ const args = process.argv.slice(2);
18
+ const unknown = args.filter((a) => a !== '--write' && a !== '--check');
19
+ if (unknown.length > 0) {
20
+ console.error(`${USAGE}\nunknown flag: ${unknown[0]}`);
21
+ process.exit(2);
22
+ }
23
+
24
+ const mode = args.includes('--check') ? 'check' : 'write';
25
+
26
+ try {
27
+ if (mode === 'check') {
28
+ const drift = await checkRenderedInstructions({ repoRoot });
29
+ for (const target of drift) {
30
+ console.log(`DRIFT ${target}`);
31
+ }
32
+ process.exit(drift.length > 0 ? 1 : 0);
33
+ }
34
+
35
+ const rendered = await renderInstructionsFromDisk({ repoRoot, write: true });
36
+ for (const [target, content] of rendered) {
37
+ console.log(`WROTE ${target} (${content.split('\n').length - 1} lines)`);
38
+ }
39
+ } catch (err) {
40
+ console.error(err && err.message ? err.message : err);
41
+ process.exit(1);
42
+ }
@@ -45,6 +45,12 @@ const steps = [
45
45
  args: ['test:artifact'],
46
46
  env: process.env,
47
47
  },
48
+ {
49
+ label: 'Doc contracts',
50
+ command: 'yarn',
51
+ args: ['vitest', 'run', 'tests/consistency/docContracts.test.js'],
52
+ env: process.env,
53
+ },
48
54
  {
49
55
  label: 'Core test suite',
50
56
  command: 'yarn',
@@ -0,0 +1,182 @@
1
+ import { compileContext } from '../../core/codeintel/compiler.js';
2
+ import { packetToText, validatePacket } from '../../core/codeintel/packet.js';
3
+ import { IndexFileSyntaxProvider } from '../../core/codeintel/providers.js';
4
+
5
+ // `ukit code` — CLI surface over the codeintel plane (SPEC §11).
6
+ // Every subcommand emits a Context Packet v1: `--json` prints the raw packet,
7
+ // default prints `packetToText`. Unknown/missing subcommand → help + non-zero.
8
+
9
+ const HELP_FLAGS = new Set(['--help', '-h', 'help']);
10
+ const SUBCOMMANDS = new Set(['peek', 'search', 'context', 'impact']);
11
+
12
+ function extractFlag(args, flag) {
13
+ const index = args.indexOf(flag);
14
+ if (index < 0) return { value: null, rest: args };
15
+ return {
16
+ value: args[index + 1] ?? null,
17
+ rest: [...args.slice(0, index), ...args.slice(index + 2)],
18
+ };
19
+ }
20
+
21
+ function hasFlag(args, flag) {
22
+ return args.includes(flag);
23
+ }
24
+
25
+ function parsePositiveInt(value, fallback) {
26
+ const parsed = Number.parseInt(value ?? '', 10);
27
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
28
+ }
29
+
30
+ function printPacket(packet, { json }) {
31
+ if (json) {
32
+ console.log(JSON.stringify(packet, null, 2));
33
+ return;
34
+ }
35
+ console.log(packetToText(packet));
36
+ const validation = validatePacket(packet);
37
+ if (!validation.ok) {
38
+ console.log(`[UKit] packet validation warnings: ${validation.errors.join('; ')}`);
39
+ }
40
+ }
41
+
42
+ async function printFileOutline(projectRoot, filePath) {
43
+ try {
44
+ const provider = new IndexFileSyntaxProvider({ rootDir: projectRoot });
45
+ if (!provider.supports(filePath)) {
46
+ return;
47
+ }
48
+ const symbols = await provider.symbols(filePath);
49
+ if (!Array.isArray(symbols) || symbols.length === 0) {
50
+ return;
51
+ }
52
+ console.log('');
53
+ console.log(`## Outline (${filePath})`);
54
+ for (const item of symbols) {
55
+ console.log(`${item.line ?? '?'}: ${item.name ?? item.kind ?? ''}${item.kind ? ` [${item.kind}]` : ''}`);
56
+ }
57
+ } catch {
58
+ // Outline is best-effort decoration; never fail the packet print for it.
59
+ }
60
+ }
61
+
62
+ export async function runCode({ projectRoot, argv = [] }) {
63
+ const [subcommandRaw, ...rest] = argv;
64
+ const subcommand = (subcommandRaw ?? '').toLowerCase();
65
+
66
+ if (!subcommand || HELP_FLAGS.has(subcommand)) {
67
+ printCodeHelp();
68
+ if (!subcommand) {
69
+ process.exitCode = 1;
70
+ }
71
+ return;
72
+ }
73
+
74
+ if (!SUBCOMMANDS.has(subcommand)) {
75
+ console.error(`[UKit] Unknown code subcommand: ${subcommand}`);
76
+ printCodeHelp();
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+
81
+ const json = hasFlag(rest, '--json');
82
+ const withoutJson = rest.filter((arg) => arg !== '--json');
83
+
84
+ if (subcommand === 'peek') {
85
+ const { value: lines, rest: args } = extractFlag(withoutJson, '--lines');
86
+ const filePath = args[0];
87
+ if (!filePath) {
88
+ console.error('[UKit] Missing file path. Usage: ukit code peek <path> [--lines a-b] [--json]');
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+ const packet = await compileContext(projectRoot, {
93
+ prompt: filePath,
94
+ filesHinted: [filePath],
95
+ }, { mode: 'peek' });
96
+ printPacket(packet, { json });
97
+ if (!json) {
98
+ await printFileOutline(projectRoot, filePath);
99
+ if (lines) {
100
+ console.log(`(note: --lines ${lines} accepted; v1 outline prints full symbol list)`);
101
+ }
102
+ }
103
+ return;
104
+ }
105
+
106
+ if (subcommand === 'search') {
107
+ const { value: limitArg, rest: args } = extractFlag(withoutJson, '--limit');
108
+ const limit = parsePositiveInt(limitArg, 20);
109
+ const query = args.join(' ').trim();
110
+ if (!query) {
111
+ console.error('[UKit] Missing query. Usage: ukit code search "<query>" [--limit n] [--json]');
112
+ process.exitCode = 1;
113
+ return;
114
+ }
115
+ const packet = await compileContext(projectRoot, query);
116
+ if (packet.evidence.length > limit) {
117
+ const dropped = packet.evidence.splice(limit);
118
+ for (const item of dropped) {
119
+ packet.omitted.push({ what: item.path ?? 'evidence', why: 'limit' });
120
+ }
121
+ }
122
+ if (packet.anchors.length > limit) {
123
+ packet.anchors = packet.anchors.slice(0, limit);
124
+ }
125
+ printPacket(packet, { json });
126
+ return;
127
+ }
128
+
129
+ if (subcommand === 'context') {
130
+ const { value: mode, rest: afterMode } = extractFlag(withoutJson, '--mode');
131
+ const { value: budgetArg, rest: afterBudget } = extractFlag(afterMode, '--budget');
132
+ const budget = budgetArg !== null ? parsePositiveInt(budgetArg, null) : undefined;
133
+ const diagnostics = hasFlag(afterBudget, '--diagnostics');
134
+ const args = afterBudget.filter((arg) => arg !== '--diagnostics');
135
+ const task = args.join(' ').trim();
136
+ if (!task) {
137
+ console.error('[UKit] Missing task. Usage: ukit code context "<task>" [--mode m] [--budget n] [--diagnostics] [--json]');
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+ const packet = await compileContext(projectRoot, task, {
142
+ mode: mode ?? undefined,
143
+ budget: budget ?? undefined,
144
+ diagnostics: diagnostics || undefined,
145
+ });
146
+ printPacket(packet, { json });
147
+ return;
148
+ }
149
+
150
+ // subcommand === 'impact'
151
+ const { value: depthArg, rest: impactArgs } = extractFlag(withoutJson, '--depth');
152
+ const depth = depthArg !== null ? parsePositiveInt(depthArg, null) : undefined;
153
+ const target = impactArgs[0];
154
+ if (!target) {
155
+ console.error('[UKit] Missing target. Usage: ukit code impact <path|symbol> [--depth N] [--json]');
156
+ process.exitCode = 1;
157
+ return;
158
+ }
159
+ const packet = await compileContext(projectRoot, {
160
+ prompt: target,
161
+ filesHinted: target.includes('/') || /\.[a-z0-9]+$/i.test(target) ? [target] : [],
162
+ }, { mode: 'impact', depth: depth ?? undefined });
163
+ printPacket(packet, { json });
164
+ }
165
+
166
+ export function printCodeHelp() {
167
+ console.log('UKit Code Commands (codeintel plane)');
168
+ console.log('Usage: ukit code <peek|search|context|impact> [args]');
169
+ console.log('');
170
+ console.log('Subcommands:');
171
+ console.log(' peek <path> [--lines a-b] L0 outline-only packet + file outline');
172
+ console.log(' search "<query>" [--limit n] Routed retrieval packet over the index');
173
+ console.log(' context "<task>" [--mode m] [--budget n] [--diagnostics] Compiled context packet for a task');
174
+ console.log(' impact <path|symbol> [--depth N] L2 packet with multi-hop reverse-import edges');
175
+ console.log('');
176
+ console.log('Options:');
177
+ console.log(' --json Print raw Context Packet JSON (default: text render)');
178
+ console.log(' --mode <m> Force router mode: none|peek|targeted|explore|impact|deep_flow|analogy');
179
+ console.log(' --budget <n> Override routed token budget');
180
+ console.log(' --depth <N> Impact-mode hop depth (clamped 1..codeIntel.impact.maxDepth)');
181
+ console.log(' --diagnostics Include post-edit diagnostics in next_actions');
182
+ }
@@ -22,10 +22,11 @@ import {
22
22
  inspectProjectImportant,
23
23
  inspectProjectImportantWiring,
24
24
  } from '../../core/projectImportant.js';
25
+ import { runDocContractChecks } from '../../core/docContracts.js';
25
26
 
26
27
  export const DOCTOR_HELP_FLAGS = new Set(['--help', '-h']);
27
- const KNOWN_FLAGS = new Set([...DOCTOR_HELP_FLAGS, '--skills', '--gateway']);
28
- const SUPPORTED_FLAGS_LIST = '--help, -h, --skills, --gateway';
28
+ const KNOWN_FLAGS = new Set([...DOCTOR_HELP_FLAGS, '--skills', '--gateway', '--docs']);
29
+ const SUPPORTED_FLAGS_LIST = '--help, -h, --skills, --gateway, --docs';
29
30
 
30
31
  export function printDoctorHelp() {
31
32
  console.log('Usage: ukit doctor [options]');
@@ -36,6 +37,7 @@ export function printDoctorHelp() {
36
37
  console.log(' --help, -h Show this help message');
37
38
  console.log(' --skills Also print a skill word-count/budget report');
38
39
  console.log(' --gateway Live gateway probe (streaming + non-streaming); advisory only');
40
+ console.log(' --docs Run doc-contract checks (manifests/documentation.yaml projects only)');
39
41
  }
40
42
 
41
43
  export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir = os.homedir() }) {
@@ -366,6 +368,36 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir =
366
368
  }
367
369
  }
368
370
 
371
+ // FR-007: doc-contract checks (DOC-203 validator) — only when the project root
372
+ // carries manifests/documentation.yaml (doc-governed maintainer repo). Downstream
373
+ // projects get a skip advisory; error-severity failures block via exitCode 1,
374
+ // warnings are advisory only.
375
+ let docContractErrors = false;
376
+ if (argv.includes('--docs')) {
377
+ const docManifestPath = path.join(projectRoot, 'manifests', 'documentation.yaml');
378
+ if (!(await pathExists(docManifestPath))) {
379
+ console.log('');
380
+ console.log('[UKit] Doc contract checks — not a doc-governed project — skipped (no manifests/documentation.yaml).');
381
+ } else {
382
+ const report = await runDocContractChecks({ rootDir: projectRoot });
383
+ console.log('');
384
+ console.log('[UKit] Doc contract checks:');
385
+ for (const check of report.checks) {
386
+ const mark = check.skipped ? '-' : ok(check.passed);
387
+ const severity = check.severity === 'warning' ? ' (warning)' : '';
388
+ console.log(`[UKit] ${mark} ${check.label}${severity}`);
389
+ if (check.detail) console.log(`[UKit] detail: ${check.detail}`);
390
+ if (check.remedy && (!check.passed || check.severity === 'warning')) {
391
+ console.log(`[UKit] remedy: ${check.remedy}`);
392
+ }
393
+ }
394
+ console.log(
395
+ `[UKit] summary: ${report.summary.passed} passed, ${report.summary.errors} errors, ${report.summary.warnings} warnings, ${report.summary.skipped} skipped`,
396
+ );
397
+ docContractErrors = report.summary.errors > 0;
398
+ }
399
+ }
400
+
369
401
  const allPassed = Object.values(checks).every(Boolean);
370
402
  const failedProjectChecks = projectChecks.filter(
371
403
  (check) => check.applicable !== false && !check.passed,
@@ -381,7 +413,7 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [], homeDir =
381
413
  }
382
414
  }
383
415
 
384
- if (!allPassed || blockingFailures.length > 0) {
416
+ if (!allPassed || blockingFailures.length > 0 || docContractErrors) {
385
417
  console.log('[UKit] Some checks failed.');
386
418
  if (!allPassed && blockingFailures.length === 0) {
387
419
  console.log('[UKit] Re-run `ukit install` to repair install-managed files.');
@@ -10,6 +10,9 @@ import { deriveTaskRoute } from '../../index/taskRouting.js';
10
10
  import { queryCodeIndex, getFileOutline } from '../../index/queryIndex.js';
11
11
  import { triageBug } from '../../bug/triageBug.js';
12
12
  import { installIndexRefreshHooks, removeIndexRefreshHooks } from '../../index/gitHooks.js';
13
+ import fs from 'node:fs/promises';
14
+ import { INDEX_ARTIFACTS, INDEX_SCHEMA_VERSION, getArtifactPath } from '../../index/paths.js';
15
+ import { computeIdentity, readSnapshot, guardLevel } from '../../core/codeintel/freshness.js';
13
16
  import {
14
17
  parseIndexArgs,
15
18
  CONTEXT_FLAG_DEFINITIONS,
@@ -221,6 +224,64 @@ export async function runIndexTools({ projectRoot, argv = [] }) {
221
224
  return;
222
225
  }
223
226
 
227
+ if (subcommand === 'status') {
228
+ // L0 freshness identity report (SPEC §7) — never refreshes, never throws.
229
+ const identity = await computeIdentity(projectRoot);
230
+ const snapshot = await readSnapshot(projectRoot);
231
+ const stale = !snapshot || snapshot.identity !== identity.identity;
232
+
233
+ console.log('[UKit] Index status:');
234
+ console.log(`identity: ${identity.identity}`);
235
+ console.log(`headSha: ${identity.headSha}`);
236
+ console.log(`overlayHash: ${identity.overlayHash}`);
237
+ console.log(`configHash: ${identity.configHash}`);
238
+ console.log(`snapshot: ${snapshot ? snapshot.identity : 'none'}`);
239
+ console.log(`stale: ${stale ? 'yes' : 'no'}`);
240
+
241
+ const artifacts = await collectArtifactAges(projectRoot);
242
+ if (artifacts.length === 0) {
243
+ console.log('artifacts: none (index not built)');
244
+ } else {
245
+ console.log('artifacts:');
246
+ for (const item of artifacts) {
247
+ console.log(` ${item.name}: ${item.age}`);
248
+ }
249
+ }
250
+ return;
251
+ }
252
+
253
+ if (subcommand === 'doctor') {
254
+ // Freshness guard + artifact schema checks (SPEC §7). Exit non-zero when
255
+ // the recommended repair level is L2 or higher.
256
+ const guard = await guardLevel(projectRoot, { maxLevel: 'L4' });
257
+ const problems = [];
258
+
259
+ console.log('[UKit] Index doctor:');
260
+ console.log(`identity: ${guard.identity.identity}`);
261
+ console.log(`headSha: ${guard.identity.headSha}`);
262
+ console.log(`snapshot: ${guard.snapshot ? guard.snapshot.identity : 'none'}`);
263
+ console.log(`stale: ${guard.stale ? 'yes' : 'no'}`);
264
+ console.log(`recommendedLevel: ${guard.recommendedLevel}`);
265
+
266
+ const schemaIssues = await checkArtifactSchemas(projectRoot);
267
+ for (const issue of schemaIssues) {
268
+ problems.push(issue);
269
+ console.log(`problem: ${issue}`);
270
+ }
271
+ if (schemaIssues.length === 0) {
272
+ console.log('schema: ok');
273
+ }
274
+
275
+ const needsRepair = ['L2', 'L3', 'L4'].includes(guard.recommendedLevel) || problems.length > 0;
276
+ if (needsRepair) {
277
+ console.log('[UKit] Index needs repair. Run "ukit index build".');
278
+ process.exitCode = 1;
279
+ } else {
280
+ console.log('[UKit] Index healthy.');
281
+ }
282
+ return;
283
+ }
284
+
224
285
  if (subcommand === 'hooks') {
225
286
  const action = (rest[0] ?? 'install').toLowerCase();
226
287
  if (action === 'help' || action === '--help' || action === '-h') {
@@ -250,7 +311,7 @@ export async function runIndexTools({ projectRoot, argv = [] }) {
250
311
 
251
312
  export function printIndexHelp() {
252
313
  console.log('UKit Index Commands');
253
- console.log('Usage: ukit index <build|refresh|query|triage|context|verify|route> [args]');
314
+ console.log('Usage: ukit index <build|refresh|query|triage|context|verify|route|status|doctor> [args]');
254
315
  console.log('');
255
316
  console.log('Subcommands:');
256
317
  console.log(' build Build codebase index cache');
@@ -260,6 +321,8 @@ export function printIndexHelp() {
260
321
  console.log(' context "<intent>" Suggest minimal file context with reasons');
261
322
  console.log(' verify "<intent>" Suggest verification lane from indexed context');
262
323
  console.log(' route "<prompt>" Suggest skill + context + verification next step');
324
+ console.log(' status Show freshness identity, snapshot, artifact ages');
325
+ console.log(' doctor Freshness guard + artifact schema checks');
263
326
  console.log(' hooks [install|remove] Manage git-hook auto-refresh');
264
327
  console.log('');
265
328
  console.log('Aliases:');
@@ -275,6 +338,44 @@ function printIndexHooksHelp() {
275
338
  console.log(' ukit index hooks remove');
276
339
  }
277
340
 
341
+ async function collectArtifactAges(rootDir) {
342
+ const entries = [];
343
+ for (const [key, fileName] of Object.entries(INDEX_ARTIFACTS)) {
344
+ try {
345
+ const stat = await fs.stat(getArtifactPath(rootDir, fileName));
346
+ const ageMs = Date.now() - stat.mtimeMs;
347
+ const ageMinutes = Math.max(0, Math.round(ageMs / 60000));
348
+ entries.push({ name: fileName, age: `${ageMinutes}m old` });
349
+ } catch {
350
+ // missing artifact — not listed
351
+ }
352
+ }
353
+ return entries;
354
+ }
355
+
356
+ async function checkArtifactSchemas(rootDir) {
357
+ const issues = [];
358
+ const required = ['meta', 'files', 'symbols'];
359
+ for (const key of required) {
360
+ const fileName = INDEX_ARTIFACTS[key];
361
+ let parsed;
362
+ try {
363
+ const raw = await fs.readFile(getArtifactPath(rootDir, fileName), 'utf8');
364
+ parsed = JSON.parse(raw);
365
+ } catch {
366
+ issues.push(`${fileName}: missing or unreadable`);
367
+ continue;
368
+ }
369
+ if (parsed?.schemaVersion !== INDEX_SCHEMA_VERSION) {
370
+ issues.push(`${fileName}: schemaVersion ${parsed?.schemaVersion ?? 'none'} != ${INDEX_SCHEMA_VERSION}`);
371
+ }
372
+ if (key !== 'meta' && !Array.isArray(parsed?.items)) {
373
+ issues.push(`${fileName}: items is not an array`);
374
+ }
375
+ }
376
+ return issues;
377
+ }
378
+
278
379
  async function refreshIndexIfStale(rootDir) {
279
380
  const lastRefreshMs = await getIndexArtifactGeneratedAt({ rootDir });
280
381
  const stale = await isIndexStale({
@@ -7,6 +7,14 @@ import {
7
7
  resolvePatternCandidate,
8
8
  runProjectHygiene,
9
9
  } from '../../core/memory/store.js';
10
+ import {
11
+ getRecord,
12
+ loadRecords,
13
+ queryRecords,
14
+ stats as v2Stats,
15
+ updateRecord,
16
+ } from '../../core/memory/storeV2.js';
17
+ import { runMigration } from '../../core/memory/migrate.js';
10
18
  import { getContextInjection, search } from '../../core/memory/retrieval.js';
11
19
  import { inspectRuntimeConfig } from '../../core/runtimeConfig.js';
12
20
  import { buildRuntimePaths } from '../../core/runtimePaths.js';
@@ -26,6 +34,30 @@ function extractFlag(args, flag) {
26
34
  }
27
35
 
28
36
  async function listAllPendingPatternCandidates(projectRoot, runtimePaths) {
37
+ // v2 mode: pending pattern candidates live as derived_fact records with
38
+ // meta.legacyStatus === 'pending' (facade handles the conversion).
39
+ // Respect memoryV2.enabled — without this check loadRecords would run the
40
+ // lazy v1→v2 migration even when the whole v2 lane is disabled.
41
+ let v2Enabled = true;
42
+ try {
43
+ const { config } = await inspectRuntimeConfig(projectRoot);
44
+ if (config?.memoryV2?.enabled === false) v2Enabled = false;
45
+ } catch {
46
+ // unreadable config → default enabled
47
+ }
48
+ const records = v2Enabled ? await loadRecords(projectRoot) : [];
49
+ if (records.length > 0) {
50
+ const projectIds = new Set(records.map((r) => r.project_id).filter(Boolean));
51
+ const results = [];
52
+ for (const projectId of projectIds) {
53
+ const pending = await listPendingPatternCandidates(projectRoot, projectId);
54
+ for (const candidate of pending) {
55
+ results.push({ ...candidate, projectId });
56
+ }
57
+ }
58
+ return results;
59
+ }
60
+
29
61
  let entries = [];
30
62
  try {
31
63
  entries = await fs.readdir(runtimePaths.projectsDir, { withFileTypes: true });
@@ -45,6 +77,106 @@ async function listAllPendingPatternCandidates(projectRoot, runtimePaths) {
45
77
  return results;
46
78
  }
47
79
 
80
+ // ---- `ukit memory v2` ops (SPEC §11) ----
81
+
82
+ async function runMemoryV2(projectRoot, args) {
83
+ const [opRaw, ...rest] = args;
84
+ const op = (opRaw ?? 'list').toLowerCase();
85
+
86
+ if (op === 'list') {
87
+ const typeFlag = extractFlag(rest, '--type');
88
+ const scopeFlag = extractFlag(typeFlag.rest, '--scope');
89
+ const statusFlag = extractFlag(scopeFlag.rest, '--status');
90
+ const records = await queryRecords(projectRoot, {
91
+ type: typeFlag.value ?? undefined,
92
+ scope: scopeFlag.value ?? undefined,
93
+ status: statusFlag.value ?? undefined,
94
+ });
95
+ if (records.length === 0) {
96
+ console.log('[UKit] No memory v2 records found.');
97
+ return;
98
+ }
99
+ for (const record of records) {
100
+ const scopeLabel = record.project_id ?? record.scope;
101
+ console.log(`${record.id} [${record.type}/${scopeLabel}/${record.status}] — ${record.text}`);
102
+ }
103
+ return;
104
+ }
105
+
106
+ if (op === 'show') {
107
+ const id = rest.join(' ').trim();
108
+ if (!id) {
109
+ throw new Error('Missing record id. Usage: ukit memory v2 show <id>');
110
+ }
111
+ const record = await getRecord(projectRoot, id);
112
+ if (!record) {
113
+ throw new Error(`Memory v2 record not found: ${id}`);
114
+ }
115
+ console.log(JSON.stringify(record, null, 2));
116
+ return;
117
+ }
118
+
119
+ if (op === 'promote') {
120
+ const id = rest.join(' ').trim();
121
+ if (!id) {
122
+ throw new Error('Missing record id. Usage: ukit memory v2 promote <id>');
123
+ }
124
+ const record = await getRecord(projectRoot, id);
125
+ if (!record) {
126
+ throw new Error(`Memory v2 record not found: ${id}`);
127
+ }
128
+ if (record.type === 'project_rule') {
129
+ console.log(`[UKit] ${id} is already a project_rule — nothing to do.`);
130
+ return;
131
+ }
132
+ if (record.type !== 'derived_fact' && record.type !== 'episode') {
133
+ throw new Error(`Cannot promote ${record.type} records (only derived_fact or episode).`);
134
+ }
135
+ // Explicit approval only — no auto-promotion (SPEC §11,
136
+ // memoryV2.promotion.episodeToRuleRequiresApproval).
137
+ const updated = await updateRecord(projectRoot, id, {
138
+ type: 'project_rule',
139
+ scope: 'repo',
140
+ provenance: `${record.provenance ?? 'manual'};promoted-from:${record.id}`,
141
+ });
142
+ console.log(`[UKit] Promoted ${updated.id} → project_rule.`);
143
+ return;
144
+ }
145
+
146
+ if (op === 'stale') {
147
+ const id = rest.join(' ').trim();
148
+ if (!id) {
149
+ throw new Error('Missing record id. Usage: ukit memory v2 stale <id>');
150
+ }
151
+ const updated = await updateRecord(projectRoot, id, { status: 'stale' });
152
+ if (!updated) {
153
+ throw new Error(`Memory v2 record not found: ${id}`);
154
+ }
155
+ console.log(`[UKit] Marked ${id} as stale.`);
156
+ return;
157
+ }
158
+
159
+ if (op === 'migrate') {
160
+ const dryRun = rest.includes('--dry-run');
161
+ const result = await runMigration(projectRoot, { dryRun });
162
+ if (dryRun) {
163
+ console.log(`[UKit] Dry-run: would migrate ${result.migrated} record(s), skip ${result.skipped}.`);
164
+ return;
165
+ }
166
+ if (result.migrated === 0) {
167
+ console.log('[UKit] Nothing to migrate (marker present or no legacy memory).');
168
+ return;
169
+ }
170
+ console.log(`[UKit] Migrated ${result.migrated} record(s) to v2.`);
171
+ console.log(`[UKit] Backup: ${result.backupDir ?? 'none'} — marker: ${result.markerPath}`);
172
+ const stats = await v2Stats(projectRoot);
173
+ console.log(`[UKit] v2 store: ${stats.total} record(s) — ${JSON.stringify(stats.byType)}`);
174
+ return;
175
+ }
176
+
177
+ throw new Error(`Unknown memory v2 op: ${op}. Expected list|show|promote|stale|migrate.`);
178
+ }
179
+
48
180
  export async function runMemory({ projectRoot, argv = [] }) {
49
181
  const runtimePaths = buildRuntimePaths(projectRoot);
50
182
  if (!(await pathExists(runtimePaths.runtimeRoot))) {
@@ -59,6 +191,11 @@ export async function runMemory({ projectRoot, argv = [] }) {
59
191
  return;
60
192
  }
61
193
 
194
+ if (subcommand === 'v2') {
195
+ await runMemoryV2(projectRoot, rest);
196
+ return;
197
+ }
198
+
62
199
  if (subcommand === 'list') {
63
200
  if (rest.includes('--pending')) {
64
201
  const pending = await listAllPendingPatternCandidates(projectRoot, runtimePaths);
package/src/cli/index.js CHANGED
@@ -6,6 +6,7 @@ import { runIndexTools } from './commands/indexTools.js';
6
6
  import { runStatus } from './commands/status.js';
7
7
  import { runMemory } from './commands/memory.js';
8
8
  import { runUpdate } from './commands/update.js';
9
+ import { runCode } from './commands/code.js';
9
10
 
10
11
  const GLOBAL_FLAGS = new Set(['--help', '-h', '--version', '-v']);
11
12
 
@@ -49,6 +50,11 @@ export async function runCli({ argv, packageRoot, projectRoot, packageVersion })
49
50
  return;
50
51
  }
51
52
 
53
+ if (command === 'code') {
54
+ await runCode({ projectRoot, argv: commandArgv });
55
+ return;
56
+ }
57
+
52
58
  if (command === 'status') {
53
59
  await runStatus({ projectRoot, argv: commandArgv });
54
60
  return;
@@ -89,6 +95,7 @@ export async function runCli({ argv, packageRoot, projectRoot, packageVersion })
89
95
  console.log(' doctor Validate manifest, stack, docs, and UKit state');
90
96
  console.log(' uninstall Remove all UKit-managed assets');
91
97
  console.log(' index Codebase index tools (build/query/triage)');
98
+ console.log(' code Context compiler surface (peek/search/context/impact)');
92
99
  console.log(' status Show UKit runtime status');
93
100
  console.log(' memory Inspect shared UKit memory');
94
101
  console.log(' update Upgrade the global UKit CLI to the latest version');