@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.
- package/CHANGELOG.md +37 -0
- package/README.md +40 -177
- package/manifests/documentation.yaml +143 -15
- package/manifests/hostCapabilities.yaml +49 -0
- package/manifests/instructionRules.yaml +383 -0
- package/manifests/platform.full.yaml +15 -0
- package/package.json +3 -1
- package/scripts/bench/goldTasks.json +38 -0
- package/scripts/bench/runGold.mjs +220 -0
- package/scripts/docs/render-instructions.mjs +42 -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/projectImportant.js +1 -1
- package/src/core/runtimeConfig.js +125 -0
- package/src/core/runtimePaths.js +3 -0
- package/src/core/uninstall.js +1 -1
- package/src/index/taskRouting.js +39 -0
- package/src/render/instructionRenderer.js +226 -0
- package/templates/.claude/ukit/index/route-task.mjs +40 -0
- package/templates/.gitignore +2 -2
- package/templates/.omp/RULES.md +1 -0
- package/templates/AGENTS.md +89 -218
- package/templates/CLAUDE.md +85 -212
- 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 +223 -0
- package/templates/instructions/core.md +157 -0
- package/templates/instructions/layout.yaml +149 -0
- package/templates/instructions/overlays/agents.md +15 -0
- package/templates/instructions/overlays/claude.md +3 -0
- package/templates/instructions/overlays/omp-rules.md +74 -0
- package/templates/instructions/overlays/repo.md +9 -0
- package/templates/instructions/repo-vars.yaml +23 -0
- 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');
|