@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,316 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { createPacket, addEvidence, validatePacket } from './packet.js';
5
+ import { guardLevel } from './freshness.js';
6
+ import { routeTask } from './router.js';
7
+ import { retrieve } from './retriever.js';
8
+ import { impactSet } from './impact.js';
9
+ import { readDirty } from './invalidation.js';
10
+ import { runDiagnostics } from './diagnostics.js';
11
+ import { createEdge } from './providers.js';
12
+ import { loadRuntimeConfig } from '../runtimeConfig.js';
13
+
14
+ // Context Compiler v1 (SPEC §5–§8) — the peek→expand→read detail ladder.
15
+ //
16
+ // Pipeline: routeTask (unless mode forced) → guardLevel (L0 freshness guard)
17
+ // → retrieve → budget-fitted evidence selection (lowest-confidence first out,
18
+ // drops recorded in `omitted`) → validatePacket.
19
+ //
20
+ // Depth ladder (evidence levels):
21
+ // peek → L0 outline only (no excerpts, no relations)
22
+ // targeted | explore → L1 + anchors + relations
23
+ // impact → L2 + 1-hop reverse-import edges (retrieve mode impact)
24
+ // deep_flow | analogy → routed but no graph/embedding lanes in v1 →
25
+ // `omitted: { what:'graph evidence', why:'provider unavailable' }`
26
+ // read → L2 excerpts; emitted when depth >= 2 or `read:true`.
27
+ //
28
+ // Budget units are token estimates (≈ 4 chars/token over serialized evidence).
29
+ // `budget.used` never exceeds `budget.requested` — anything that does not fit
30
+ // lands in `omitted` with `why:'budget'`. Selective by contract: `none` mode
31
+ // returns a valid packet with empty evidence, never fabricates context.
32
+
33
+ const EXCERPT_MAX_LINES = 40;
34
+ const EXCERPT_MAX_CHARS = 4000;
35
+ const TOKEN_CHAR_RATIO = 4;
36
+
37
+ const DEPTH_LEVEL = Object.freeze({
38
+ none: null,
39
+ peek: 'L0',
40
+ targeted: 'L1',
41
+ explore: 'L1',
42
+ impact: 'L2',
43
+ deep_flow: 'L2',
44
+ analogy: 'L2',
45
+ });
46
+
47
+ const UNAVAILABLE_LANES = new Set(['deep_flow', 'analogy']);
48
+
49
+ function estimateTokens(value) {
50
+ try {
51
+ return Math.max(1, Math.ceil(JSON.stringify(value).length / TOKEN_CHAR_RATIO));
52
+ } catch {
53
+ return 1;
54
+ }
55
+ }
56
+
57
+ // Confidence proxy: retriever emits strongest lanes first, so array order is
58
+ // the ranking. Selection walks candidates in order; whatever does not fit is
59
+ // recorded in `omitted` with `why:'budget'` (lowest-confidence tail drops).
60
+ // Evidence, anchors and relations all draw from the same token budget —
61
+ // `budget.used` covers the whole emitted payload and never exceeds `requested`.
62
+ function fitToBudget({ evidence, anchors, relations }, budgetTokens, dropped) {
63
+ let used = 0;
64
+ const fit = (items, label) => {
65
+ const kept = [];
66
+ for (const item of items) {
67
+ const cost = estimateTokens(item);
68
+ if (used + cost <= budgetTokens) {
69
+ kept.push(item);
70
+ used += cost;
71
+ } else {
72
+ dropped.push({ what: item.path ?? item.why ?? item.from ?? label, why: 'budget' });
73
+ }
74
+ }
75
+ return kept;
76
+ };
77
+ return {
78
+ evidence: fit(evidence, 'evidence'),
79
+ anchors: fit(anchors, 'anchor'),
80
+ relations: fit(relations, 'relation'),
81
+ used,
82
+ };
83
+ }
84
+
85
+ async function readExcerpt(rootDir, relPath) {
86
+ try {
87
+ const content = await fs.readFile(path.join(rootDir, relPath), 'utf8');
88
+ const lines = content.split('\n').slice(0, EXCERPT_MAX_LINES);
89
+ return lines.join('\n').slice(0, EXCERPT_MAX_CHARS);
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ function normalizeTask(task) {
96
+ if (typeof task === 'string') return { prompt: task };
97
+ if (task && typeof task === 'object') return { ...task };
98
+ return { prompt: '' };
99
+ }
100
+
101
+ /**
102
+ * compileContext(projectRoot, task, { mode?, budget?, depth?, read?, filesHinted?, diagnostics? })
103
+ * → packet (Context Packet v1, always validatePacket-clean)
104
+ *
105
+ * `task` is a prompt string or `{ prompt, hasError?, filesHinted?, flags? }`.
106
+ * `mode` forces the router mode (same as `task.flags.mode`).
107
+ * `budget` overrides the routed token budget. `read:true` forces L2 excerpts.
108
+ * `depth` (>1, impact mode only) routes relations through multi-hop `impactSet`;
109
+ * `diagnostics` (default config.codeIntel.diagnostics.enabled) appends object
110
+ * `next_actions` — `{kind:'stale'}` for the dirty set + `{kind:'diagnostic'}`
111
+ * per `runDiagnostics` result.
112
+ */
113
+ export async function compileContext(projectRoot, task, options = {}) {
114
+ const rootDir = path.resolve(projectRoot ?? process.cwd());
115
+ const input = normalizeTask(task);
116
+ const opts = options && typeof options === 'object' ? options : {};
117
+
118
+ const flags = { ...(input.flags && typeof input.flags === 'object' ? input.flags : {}) };
119
+ if (typeof opts.mode === 'string' && opts.mode.trim() !== '') {
120
+ flags.mode = opts.mode.trim();
121
+ }
122
+
123
+ let config = null;
124
+ try {
125
+ config = await loadRuntimeConfig(rootDir);
126
+ } catch {
127
+ config = null;
128
+ }
129
+
130
+ const routed = routeTask(
131
+ { prompt: input.prompt ?? '', hasError: input.hasError === true, filesHinted: opts.filesHinted ?? input.filesHinted, flags },
132
+ { config },
133
+ );
134
+ const mode = routed.mode;
135
+ const requested = typeof opts.budget === 'number' && Number.isFinite(opts.budget) && opts.budget >= 0
136
+ ? opts.budget
137
+ : routed.budget;
138
+ const level = DEPTH_LEVEL[mode] ?? 'L1';
139
+ // `read` step of the ladder: L2 excerpts when routed depth >= 2 or --read.
140
+ const wantRead = opts.read === true || routed.depth >= 2;
141
+
142
+ // L0 freshness guard — never throws; staleness is reported, not repaired here.
143
+ let guard = null;
144
+ try {
145
+ guard = await guardLevel(rootDir, { maxLevel: 'L0' });
146
+ } catch {
147
+ guard = null;
148
+ }
149
+ const identity = guard?.identity ?? {};
150
+ const stale = guard?.stale === true;
151
+
152
+ const packet = createPacket({
153
+ repo: rootDir,
154
+ snapshot: typeof identity.identity === 'string' ? identity.identity : 'unknown',
155
+ taskType: mode,
156
+ budget: { requested },
157
+ });
158
+ packet.freshness = {
159
+ level: guard?.recommendedLevel ?? 'L0',
160
+ headSha: identity.headSha ?? '',
161
+ overlayHash: identity.overlayHash ?? '',
162
+ configHash: identity.configHash ?? '',
163
+ stale,
164
+ };
165
+ if (stale) {
166
+ packet.next_actions.push(`ukit index refresh — snapshot stale (repair level ${guard?.recommendedLevel ?? 'L2'})`);
167
+ }
168
+
169
+ // `none` mode: valid packet, empty evidence, reason recorded — never fabricate.
170
+ if (mode === 'none' || requested === 0) {
171
+ packet.omitted.push({ what: 'retrieval', why: mode === 'none' ? 'mode-none' : 'budget' });
172
+ return packet;
173
+ }
174
+
175
+ if (UNAVAILABLE_LANES.has(mode)) {
176
+ packet.omitted.push({ what: 'graph evidence', why: 'provider unavailable' });
177
+ }
178
+
179
+ const query = typeof opts.query === 'string' && opts.query.trim() !== ''
180
+ ? opts.query
181
+ : (input.prompt ?? '');
182
+ const retrieveMode = mode === 'impact' ? 'impact' : 'search';
183
+
184
+ // Impact-mode hop depth: explicit opt > config default. depth > 1 switches
185
+ // relations to the multi-hop impactSet lane; depth 1 keeps the C26 1-hop lane.
186
+ const impactDefaults = config?.codeIntel?.impact ?? {};
187
+ const configuredDepth = typeof impactDefaults.defaultDepth === 'number' ? impactDefaults.defaultDepth : 2;
188
+ const depth = typeof opts.depth === 'number' && Number.isFinite(opts.depth) && opts.depth > 0
189
+ ? opts.depth
190
+ : configuredDepth;
191
+ const multiHopImpact = mode === 'impact' && depth > 1;
192
+
193
+ let result = { anchors: [], evidence: [], relations: [], omitted: [] };
194
+ try {
195
+ result = await retrieve(rootDir, query, {
196
+ // Multi-hop lane handles relations itself — retriever stays in 'search'
197
+ // mode so its 1-hop edges do not duplicate impactSet output.
198
+ mode: multiHopImpact ? 'search' : retrieveMode,
199
+ snapshot: typeof identity.identity === 'string' ? identity.identity : null,
200
+ });
201
+ } catch {
202
+ packet.omitted.push({ what: 'retrieval', why: 'retriever-error' });
203
+ }
204
+
205
+ for (const entry of result.omitted ?? []) {
206
+ packet.omitted.push(entry);
207
+ }
208
+
209
+ // Multi-hop impact relations (SPEC §8): BFS over imports/calls artifacts at
210
+ // the requested depth, mapped to the canonical createEdge shape.
211
+ if (multiHopImpact) {
212
+ try {
213
+ const seeds = (result.anchors ?? []).map((a) => a?.path).filter(Boolean);
214
+ const impact = await impactSet(rootDir, seeds, { depth, direction: 'dependents' });
215
+ result = {
216
+ ...result,
217
+ relations: impact.edges.map((edge) => createEdge({
218
+ from: edge.from,
219
+ to: edge.to,
220
+ kind: edge.kind ?? 'import',
221
+ confidence: 1,
222
+ provider: 'index-file',
223
+ evidence: 'imports.json',
224
+ snapshot: typeof identity.identity === 'string' ? identity.identity : null,
225
+ })),
226
+ };
227
+ for (const entry of impact.omitted ?? []) packet.omitted.push(entry);
228
+ if (impact.truncated) {
229
+ packet.omitted.push({ what: 'impact-nodes', why: 'truncated' });
230
+ }
231
+ } catch {
232
+ packet.omitted.push({ what: 'impact-lane', why: 'impact-error' });
233
+ }
234
+ }
235
+
236
+ // L0 peek emits outline-level evidence only: same paths, no excerpts.
237
+ const candidates = (result.evidence ?? []).map((item) => {
238
+ const next = { ...item, level };
239
+ delete next.excerpt;
240
+ return next;
241
+ });
242
+
243
+ // L2 read lane: attach excerpts to the top candidates only.
244
+ if (wantRead && level !== 'L0') {
245
+ for (const item of candidates) {
246
+ if (!item.path) continue;
247
+ const excerpt = await readExcerpt(rootDir, item.path);
248
+ if (excerpt) item.excerpt = excerpt;
249
+ }
250
+ }
251
+
252
+ const dropped = [];
253
+ const expandLanes = level !== 'L0';
254
+ const fitted = fitToBudget(
255
+ {
256
+ evidence: candidates,
257
+ anchors: expandLanes ? (result.anchors ?? []) : [],
258
+ relations: expandLanes ? (result.relations ?? []) : [],
259
+ },
260
+ requested,
261
+ dropped,
262
+ );
263
+ for (const item of fitted.evidence) {
264
+ try {
265
+ addEvidence(packet, item);
266
+ } catch {
267
+ dropped.push({ what: item.path ?? 'evidence', why: 'invalid-evidence' });
268
+ }
269
+ }
270
+ if (!expandLanes) {
271
+ for (const rel of result.relations ?? []) {
272
+ dropped.push({ what: `${rel.from}→${rel.to}`, why: 'budget' });
273
+ }
274
+ }
275
+ packet.anchors = fitted.anchors;
276
+ packet.relations = fitted.relations;
277
+ packet.omitted.push(...dropped);
278
+ packet.budget.used = fitted.used;
279
+
280
+ // Diagnostics lane (SPEC §8): non-destructive dirty-set read → stale entry +
281
+ // cheap per-file checks → diagnostic entries. Mixed string/object
282
+ // next_actions is contract-legal (validatePacket only requires an array).
283
+ const diagnosticsEnabled = typeof opts.diagnostics === 'boolean'
284
+ ? opts.diagnostics
285
+ : config?.codeIntel?.diagnostics?.enabled === true;
286
+ if (diagnosticsEnabled) {
287
+ try {
288
+ const dirty = await readDirty(rootDir);
289
+ if (dirty.length > 0) {
290
+ packet.next_actions.push({ kind: 'stale', files: dirty.slice(0, 10), why: 'dirty-since-index' });
291
+ const timeoutMs = typeof config?.codeIntel?.diagnostics?.timeoutMs === 'number'
292
+ ? config.codeIntel.diagnostics.timeoutMs
293
+ : 8000;
294
+ const diags = await runDiagnostics(rootDir, dirty, { timeoutMs });
295
+ for (const d of diags.slice(0, 10)) {
296
+ packet.next_actions.push({
297
+ kind: 'diagnostic',
298
+ file: d.file,
299
+ message: d.message,
300
+ severity: d.severity,
301
+ });
302
+ }
303
+ }
304
+ } catch {
305
+ packet.omitted.push({ what: 'diagnostics', why: 'diagnostics-error' });
306
+ }
307
+ }
308
+
309
+ const validation = validatePacket(packet);
310
+ if (!validation.ok) {
311
+ // Contract safety net: a packet that cannot validate is still returned in
312
+ // minimal form rather than throwing — callers always get a packet.
313
+ packet.omitted.push({ what: 'packet', why: `validation: ${validation.errors.join('; ')}` });
314
+ }
315
+ return packet;
316
+ }
@@ -0,0 +1,114 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ // Post-edit diagnostics (SPEC §7 — CI-206). Cheap checks over dirty paths:
6
+ // `node --check` for .js/.mjs/.cjs, `JSON.parse` for .json. Everything else is
7
+ // skipped silently. Never throws: missing files, spawn failures and timeouts
8
+ // degrade to a `severity:'warning'` entry or are skipped — the caller always
9
+ // gets a (possibly empty) normalized diagnostics list.
10
+
11
+ const SYNTAX_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
12
+ const MAX_DIAGNOSTICS = 50;
13
+
14
+ function parseCheckLine(stderr) {
15
+ const line = String(stderr ?? '')
16
+ .split('\n')
17
+ .map((l) => l.trim())
18
+ .find((l) => l.length > 0);
19
+ if (!line) return { message: 'syntax check failed', line: null };
20
+ const match = line.match(/:(\d+)\s*$/);
21
+ return {
22
+ message: line,
23
+ line: match ? Number.parseInt(match[1], 10) : null,
24
+ };
25
+ }
26
+
27
+ async function checkJson(rootDir, relPath) {
28
+ let content;
29
+ try {
30
+ content = await fs.readFile(path.join(rootDir, relPath), 'utf8');
31
+ } catch {
32
+ return null; // missing file → skip
33
+ }
34
+ try {
35
+ JSON.parse(content);
36
+ return null;
37
+ } catch (error) {
38
+ const message = error?.message ?? String(error);
39
+ const match = message.match(/position (\d+)/i);
40
+ const diag = { file: relPath, severity: 'error', message, source: 'json' };
41
+ if (match) {
42
+ const offset = Number.parseInt(match[1], 10);
43
+ const line = content.slice(0, offset).split('\n').length;
44
+ if (Number.isFinite(line) && line > 0) diag.line = line;
45
+ }
46
+ return diag;
47
+ }
48
+ }
49
+
50
+ async function checkSyntax(rootDir, relPath, timeoutMs, spawn) {
51
+ const absPath = path.join(rootDir, relPath);
52
+ try {
53
+ await fs.access(absPath);
54
+ } catch {
55
+ return null; // missing file → skip
56
+ }
57
+ let result;
58
+ try {
59
+ result = spawn(process.execPath, ['--check', absPath], {
60
+ timeout: timeoutMs,
61
+ encoding: 'utf8',
62
+ maxBuffer: 1024 * 1024,
63
+ });
64
+ } catch (error) {
65
+ return { file: relPath, severity: 'warning', message: `spawn failed: ${error?.message ?? error}`, source: 'syntax' };
66
+ }
67
+ if (!result || typeof result !== 'object') {
68
+ return { file: relPath, severity: 'warning', message: 'spawn failed: no result', source: 'syntax' };
69
+ }
70
+ const timedOut = result.signal === 'SIGTERM' || result.error?.code === 'ETIMEDOUT';
71
+ if (timedOut) {
72
+ return { file: relPath, severity: 'warning', message: `syntax check timed out after ${timeoutMs}ms`, source: 'syntax' };
73
+ }
74
+ if (result.error) {
75
+ return { file: relPath, severity: 'warning', message: `spawn failed: ${result.error.message}`, source: 'syntax' };
76
+ }
77
+ if (result.status === 0) return null;
78
+ const { message, line } = parseCheckLine(result.stderr || result.stdout);
79
+ const diag = { file: relPath, severity: 'error', message, source: 'syntax' };
80
+ if (line !== null) diag.line = line;
81
+ return diag;
82
+ }
83
+
84
+ /**
85
+ * runDiagnostics(projectRoot, relPaths, { timeoutMs?, maxFiles? })
86
+ * → [{ file, severity:'error'|'warning', message, line?, source:'syntax'|'json' }]
87
+ *
88
+ * Deterministic order (input order preserved); capped at `maxFiles` inputs and
89
+ * MAX_DIAGNOSTICS total entries. `_spawn` is a test seam for timeout injection.
90
+ */
91
+ export async function runDiagnostics(projectRoot, relPaths, {
92
+ timeoutMs = 8000,
93
+ maxFiles = 50,
94
+ _spawn = spawnSync,
95
+ } = {}) {
96
+ const rootDir = path.resolve(projectRoot ?? process.cwd());
97
+ const diagnostics = [];
98
+ const inputs = (Array.isArray(relPaths) ? relPaths : [])
99
+ .filter((p) => typeof p === 'string' && p.length > 0)
100
+ .slice(0, Math.max(0, maxFiles));
101
+
102
+ for (const relPath of inputs) {
103
+ if (diagnostics.length >= MAX_DIAGNOSTICS) break;
104
+ const ext = path.posix.extname(relPath.replace(/\\/g, '/')).toLowerCase();
105
+ let diag = null;
106
+ if (ext === '.json') {
107
+ diag = await checkJson(rootDir, relPath);
108
+ } else if (SYNTAX_EXTENSIONS.has(ext)) {
109
+ diag = await checkSyntax(rootDir, relPath, timeoutMs, _spawn);
110
+ }
111
+ if (diag) diagnostics.push(diag);
112
+ }
113
+ return diagnostics;
114
+ }