devmind 1.1.1 → 1.1.2
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/README.md +174 -169
- package/dist/cli/handlers.js +24 -4
- package/dist/cli/handlers.js.map +1 -1
- package/dist/cli/register-analysis.js +11 -0
- package/dist/cli/register-analysis.js.map +1 -1
- package/dist/codebase/index.js +3 -2
- package/dist/codebase/index.js.map +1 -1
- package/dist/codebase/parsers/typescript.js +122 -72
- package/dist/codebase/parsers/typescript.js.map +1 -1
- package/dist/codebase/scanners/filesystem.d.ts +2 -2
- package/dist/codebase/scanners/filesystem.js +31 -6
- package/dist/codebase/scanners/filesystem.js.map +1 -1
- package/dist/commands/analyze.js +4 -3
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/audit-design.js +74 -0
- package/dist/commands/audit-design.js.map +1 -1
- package/dist/commands/audit.js +4 -3
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/autosave.d.ts +14 -0
- package/dist/commands/autosave.js +121 -0
- package/dist/commands/autosave.js.map +1 -1
- package/dist/commands/design-system.js +8 -0
- package/dist/commands/design-system.js.map +1 -1
- package/dist/commands/extract.js +4 -3
- package/dist/commands/extract.js.map +1 -1
- package/dist/commands/retrieve.d.ts +3 -0
- package/dist/commands/retrieve.js +593 -5
- package/dist/commands/retrieve.js.map +1 -1
- package/dist/commands/status.js +4 -3
- package/dist/commands/status.js.map +1 -1
- package/dist/core/devmind-ignore.d.ts +2 -0
- package/dist/core/devmind-ignore.js +99 -0
- package/dist/core/devmind-ignore.js.map +1 -0
- package/dist/core/errors.js +32 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/tool-output.d.ts +18 -0
- package/dist/core/tool-output.js +95 -0
- package/dist/core/tool-output.js.map +1 -0
- package/dist/generators/unified.d.ts +1 -1
- package/dist/generators/unified.js +301 -72
- package/dist/generators/unified.js.map +1 -1
- package/package.json +86 -86
|
@@ -26,6 +26,410 @@ function tokenize(input) {
|
|
|
26
26
|
.map((token) => token.trim())
|
|
27
27
|
.filter((token) => token.length >= 2);
|
|
28
28
|
}
|
|
29
|
+
function hasUiTokenSignal(tokens) {
|
|
30
|
+
const hasToken = tokens.includes('token') || tokens.includes('tokens');
|
|
31
|
+
if (!hasToken)
|
|
32
|
+
return false;
|
|
33
|
+
return (tokens.includes('design') ||
|
|
34
|
+
tokens.includes('theme') ||
|
|
35
|
+
tokens.includes('spacing') ||
|
|
36
|
+
tokens.includes('typography') ||
|
|
37
|
+
tokens.includes('color') ||
|
|
38
|
+
tokens.includes('colors') ||
|
|
39
|
+
tokens.includes('component') ||
|
|
40
|
+
tokens.includes('components'));
|
|
41
|
+
}
|
|
42
|
+
function hasAuthTokenSignal(tokens) {
|
|
43
|
+
const hasToken = tokens.includes('token') || tokens.includes('tokens');
|
|
44
|
+
return hasToken && !hasUiTokenSignal(tokens);
|
|
45
|
+
}
|
|
46
|
+
function detectRoutes(tokens) {
|
|
47
|
+
const routes = [];
|
|
48
|
+
const hasAny = (values) => values.some((value) => tokens.includes(value));
|
|
49
|
+
const authSignal = hasAny(['auth', 'authentication', 'authorize', 'authorization', 'login']) ||
|
|
50
|
+
hasAuthTokenSignal(tokens);
|
|
51
|
+
if (authSignal) {
|
|
52
|
+
routes.push('auth');
|
|
53
|
+
}
|
|
54
|
+
if (hasAny(['db', 'database', 'schema', 'sql', 'query', 'migration', 'postgres', 'mysql'])) {
|
|
55
|
+
routes.push('db');
|
|
56
|
+
}
|
|
57
|
+
const uiSignal = hasAny([
|
|
58
|
+
'ui',
|
|
59
|
+
'ux',
|
|
60
|
+
'frontend',
|
|
61
|
+
'component',
|
|
62
|
+
'layout',
|
|
63
|
+
'design',
|
|
64
|
+
'theme',
|
|
65
|
+
'hydration',
|
|
66
|
+
'ssr',
|
|
67
|
+
'csr',
|
|
68
|
+
'a11y',
|
|
69
|
+
'accessibility',
|
|
70
|
+
'form',
|
|
71
|
+
'client',
|
|
72
|
+
'server-component',
|
|
73
|
+
'animation',
|
|
74
|
+
'animations',
|
|
75
|
+
'motion',
|
|
76
|
+
'framer',
|
|
77
|
+
'gsap',
|
|
78
|
+
'lottie',
|
|
79
|
+
'keyframe',
|
|
80
|
+
'keyframes',
|
|
81
|
+
]) || hasUiTokenSignal(tokens);
|
|
82
|
+
if (uiSignal) {
|
|
83
|
+
routes.push('ui');
|
|
84
|
+
}
|
|
85
|
+
return routes;
|
|
86
|
+
}
|
|
87
|
+
function detectContractTargets(tokens) {
|
|
88
|
+
const targets = [];
|
|
89
|
+
const hasAny = (values) => values.some((value) => tokens.includes(value));
|
|
90
|
+
if (hasAny([
|
|
91
|
+
'econnrefused',
|
|
92
|
+
'eaddrinuse',
|
|
93
|
+
'port',
|
|
94
|
+
'listen',
|
|
95
|
+
'upstream',
|
|
96
|
+
'proxy',
|
|
97
|
+
'http',
|
|
98
|
+
'gateway',
|
|
99
|
+
'basepath',
|
|
100
|
+
'header',
|
|
101
|
+
])) {
|
|
102
|
+
targets.push('http');
|
|
103
|
+
}
|
|
104
|
+
if (hasAny([
|
|
105
|
+
'middleware',
|
|
106
|
+
'helper',
|
|
107
|
+
'helpers',
|
|
108
|
+
'signature',
|
|
109
|
+
'next',
|
|
110
|
+
'ctx',
|
|
111
|
+
'req',
|
|
112
|
+
'res',
|
|
113
|
+
'wrapper',
|
|
114
|
+
])) {
|
|
115
|
+
targets.push('middleware');
|
|
116
|
+
}
|
|
117
|
+
const authContractSignal = hasAny([
|
|
118
|
+
'auth',
|
|
119
|
+
'jwt',
|
|
120
|
+
'session',
|
|
121
|
+
'claim',
|
|
122
|
+
'claims',
|
|
123
|
+
'sub',
|
|
124
|
+
'aud',
|
|
125
|
+
'iss',
|
|
126
|
+
'scope',
|
|
127
|
+
'roles',
|
|
128
|
+
'permissions',
|
|
129
|
+
]) || hasAuthTokenSignal(tokens);
|
|
130
|
+
if (authContractSignal) {
|
|
131
|
+
targets.push('auth');
|
|
132
|
+
}
|
|
133
|
+
if (hasAny([
|
|
134
|
+
'ui',
|
|
135
|
+
'ux',
|
|
136
|
+
'frontend',
|
|
137
|
+
'component',
|
|
138
|
+
'layout',
|
|
139
|
+
'design',
|
|
140
|
+
'theme',
|
|
141
|
+
'hydration',
|
|
142
|
+
'ssr',
|
|
143
|
+
'csr',
|
|
144
|
+
'a11y',
|
|
145
|
+
'accessibility',
|
|
146
|
+
'form',
|
|
147
|
+
'validation',
|
|
148
|
+
'server-component',
|
|
149
|
+
'client-component',
|
|
150
|
+
])
|
|
151
|
+
|| hasUiTokenSignal(tokens)) {
|
|
152
|
+
targets.push('ui');
|
|
153
|
+
}
|
|
154
|
+
if (hasAny([
|
|
155
|
+
'animation',
|
|
156
|
+
'animations',
|
|
157
|
+
'motion',
|
|
158
|
+
'framer',
|
|
159
|
+
'framer-motion',
|
|
160
|
+
'gsap',
|
|
161
|
+
'lottie',
|
|
162
|
+
'keyframe',
|
|
163
|
+
'keyframes',
|
|
164
|
+
'spring',
|
|
165
|
+
'tween',
|
|
166
|
+
'timeline',
|
|
167
|
+
'reduced-motion',
|
|
168
|
+
'prefers-reduced-motion',
|
|
169
|
+
])) {
|
|
170
|
+
targets.push('motion');
|
|
171
|
+
}
|
|
172
|
+
if (hasAny(['go', 'golang', 'goroutine', 'gin', 'fiber', 'echo'])) {
|
|
173
|
+
targets.push('go');
|
|
174
|
+
}
|
|
175
|
+
if (hasAny(['python', 'fastapi', 'django', 'flask', 'uvicorn', 'pydantic'])) {
|
|
176
|
+
targets.push('python');
|
|
177
|
+
}
|
|
178
|
+
if (hasAny(['next', 'nextjs', 'next.js', 'app-router', 'route-handler', 'server-component'])) {
|
|
179
|
+
targets.push('next');
|
|
180
|
+
}
|
|
181
|
+
if (hasAny(['php', 'php-fpm', 'composer'])) {
|
|
182
|
+
targets.push('php');
|
|
183
|
+
}
|
|
184
|
+
if (hasAny(['laravel', 'eloquent', 'artisan', 'sanctum', 'passport'])) {
|
|
185
|
+
targets.push('laravel');
|
|
186
|
+
}
|
|
187
|
+
return [...new Set(targets)];
|
|
188
|
+
}
|
|
189
|
+
function parseRouteOption(raw) {
|
|
190
|
+
if (!raw)
|
|
191
|
+
return [];
|
|
192
|
+
const entries = raw
|
|
193
|
+
.split(',')
|
|
194
|
+
.map((value) => value.trim().toLowerCase())
|
|
195
|
+
.filter(Boolean);
|
|
196
|
+
const routes = [];
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
if (entry === 'auth' || entry === 'db' || entry === 'ui') {
|
|
199
|
+
routes.push(entry);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return [...new Set(routes)];
|
|
203
|
+
}
|
|
204
|
+
function detectEscalationLevel(tokens, override) {
|
|
205
|
+
if (override !== undefined) {
|
|
206
|
+
const parsed = Number(override);
|
|
207
|
+
if (parsed === 1 || parsed === 2 || parsed === 3)
|
|
208
|
+
return parsed;
|
|
209
|
+
}
|
|
210
|
+
const level3Hints = [
|
|
211
|
+
'refactor',
|
|
212
|
+
'cross-module',
|
|
213
|
+
'crossmodule',
|
|
214
|
+
'migration',
|
|
215
|
+
'migrate',
|
|
216
|
+
'incident',
|
|
217
|
+
'debug',
|
|
218
|
+
'outage',
|
|
219
|
+
'hotfix',
|
|
220
|
+
];
|
|
221
|
+
if (level3Hints.some((hint) => tokens.includes(hint)))
|
|
222
|
+
return 3;
|
|
223
|
+
const level2Hints = [
|
|
224
|
+
'modify',
|
|
225
|
+
'change',
|
|
226
|
+
'behavior',
|
|
227
|
+
'invariant',
|
|
228
|
+
'contract',
|
|
229
|
+
'breaking',
|
|
230
|
+
'semantics',
|
|
231
|
+
];
|
|
232
|
+
if (level2Hints.some((hint) => tokens.includes(hint)))
|
|
233
|
+
return 2;
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
function routeCandidatesForLevel(level) {
|
|
237
|
+
const candidates = [{ level: 1, file: 'summary.md' }];
|
|
238
|
+
if (level >= 2)
|
|
239
|
+
candidates.push({ level: 2, file: 'details.md' });
|
|
240
|
+
if (level >= 3)
|
|
241
|
+
candidates.push({ level: 3, file: 'deep-dive.md' });
|
|
242
|
+
return candidates;
|
|
243
|
+
}
|
|
244
|
+
function shouldIncludeState(tokens, explicit) {
|
|
245
|
+
if (explicit === true)
|
|
246
|
+
return true;
|
|
247
|
+
const stateHints = [
|
|
248
|
+
'decision',
|
|
249
|
+
'decisions',
|
|
250
|
+
'hypothesis',
|
|
251
|
+
'hypotheses',
|
|
252
|
+
'assumption',
|
|
253
|
+
'assumptions',
|
|
254
|
+
'ruled-out',
|
|
255
|
+
'ruled',
|
|
256
|
+
'confirmed',
|
|
257
|
+
'state',
|
|
258
|
+
'context',
|
|
259
|
+
];
|
|
260
|
+
return stateHints.some((hint) => tokens.includes(hint));
|
|
261
|
+
}
|
|
262
|
+
function maxStateEntriesForLevel(level) {
|
|
263
|
+
if (level >= 3)
|
|
264
|
+
return 10;
|
|
265
|
+
if (level === 2)
|
|
266
|
+
return 6;
|
|
267
|
+
return 3;
|
|
268
|
+
}
|
|
269
|
+
function parseStateLogLines(content, kind, maxEntries) {
|
|
270
|
+
const lines = content
|
|
271
|
+
.split(/\r?\n/)
|
|
272
|
+
.map((line) => line.trim())
|
|
273
|
+
.filter(Boolean);
|
|
274
|
+
const parsed = [];
|
|
275
|
+
for (let i = lines.length - 1; i >= 0 && parsed.length < maxEntries; i -= 1) {
|
|
276
|
+
try {
|
|
277
|
+
const raw = JSON.parse(lines[i]);
|
|
278
|
+
const timestamp = typeof raw.timestamp === 'string' ? raw.timestamp : new Date(0).toISOString();
|
|
279
|
+
const textField = kind === 'decision'
|
|
280
|
+
? typeof raw.decision === 'string'
|
|
281
|
+
? raw.decision
|
|
282
|
+
: ''
|
|
283
|
+
: typeof raw.hypothesis === 'string'
|
|
284
|
+
? raw.hypothesis
|
|
285
|
+
: '';
|
|
286
|
+
if (!textField)
|
|
287
|
+
continue;
|
|
288
|
+
parsed.push({
|
|
289
|
+
kind,
|
|
290
|
+
timestamp,
|
|
291
|
+
source: typeof raw.source === 'string' ? raw.source : undefined,
|
|
292
|
+
note: typeof raw.note === 'string' ? raw.note : null,
|
|
293
|
+
text: textField,
|
|
294
|
+
status: kind === 'hypothesis' &&
|
|
295
|
+
(raw.status === 'open' || raw.status === 'ruled-out' || raw.status === 'confirmed')
|
|
296
|
+
? raw.status
|
|
297
|
+
: undefined,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Skip malformed line.
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return parsed.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
305
|
+
}
|
|
306
|
+
async function loadStateEntries(outputDir, escalationLevel) {
|
|
307
|
+
const maxEntries = maxStateEntriesForLevel(escalationLevel);
|
|
308
|
+
const entries = [];
|
|
309
|
+
try {
|
|
310
|
+
const decisionContent = await readFileSafe(path.join(outputDir, 'context', 'DECISIONS.jsonl'));
|
|
311
|
+
entries.push(...parseStateLogLines(decisionContent, 'decision', maxEntries));
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
// Optional file
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
const hypothesisContent = await readFileSafe(path.join(outputDir, 'context', 'HYPOTHESES.jsonl'));
|
|
318
|
+
entries.push(...parseStateLogLines(hypothesisContent, 'hypothesis', maxEntries));
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Optional file
|
|
322
|
+
}
|
|
323
|
+
return entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, maxEntries * 2);
|
|
324
|
+
}
|
|
325
|
+
async function loadContractContext(outputDir, contracts) {
|
|
326
|
+
const chunks = [];
|
|
327
|
+
for (const contract of contracts) {
|
|
328
|
+
const relSource = `context/contracts/${contract}.md`;
|
|
329
|
+
const fullPath = path.join(outputDir, relSource);
|
|
330
|
+
try {
|
|
331
|
+
const content = await readFileSafe(fullPath);
|
|
332
|
+
if (!content.trim())
|
|
333
|
+
continue;
|
|
334
|
+
chunks.push({
|
|
335
|
+
contract,
|
|
336
|
+
source: relSource,
|
|
337
|
+
title: `${contract.toUpperCase()} Contract`,
|
|
338
|
+
content: content.trim(),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// Optional file
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return chunks;
|
|
346
|
+
}
|
|
347
|
+
function shouldIncludeDesignSystem(tokens, routes) {
|
|
348
|
+
if (routes.includes('ui'))
|
|
349
|
+
return true;
|
|
350
|
+
const uiHints = [
|
|
351
|
+
'ui',
|
|
352
|
+
'ux',
|
|
353
|
+
'design-system',
|
|
354
|
+
'component',
|
|
355
|
+
'a11y',
|
|
356
|
+
'accessibility',
|
|
357
|
+
'hydration',
|
|
358
|
+
'ssr',
|
|
359
|
+
'csr',
|
|
360
|
+
];
|
|
361
|
+
return uiHints.some((hint) => tokens.includes(hint)) || hasUiTokenSignal(tokens);
|
|
362
|
+
}
|
|
363
|
+
async function loadDesignSystemContext(outputDir) {
|
|
364
|
+
const source = 'design-system.json';
|
|
365
|
+
const fullPath = path.join(outputDir, source);
|
|
366
|
+
let raw = '';
|
|
367
|
+
try {
|
|
368
|
+
raw = await readFileSafe(fullPath);
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
if (!raw.trim())
|
|
374
|
+
return null;
|
|
375
|
+
try {
|
|
376
|
+
const parsed = JSON.parse(raw);
|
|
377
|
+
const lines = [
|
|
378
|
+
`Design system: ${parsed.name || 'unnamed'} (v${parsed.version || 'n/a'})`,
|
|
379
|
+
`Allowed component imports: ${(parsed.allowedComponentImports || []).join(', ') || '(none)'}`,
|
|
380
|
+
`Token sources: ${(parsed.tokenSources || []).join(', ') || '(none)'}`,
|
|
381
|
+
`Required wrappers: ${(parsed.requiredWrappers || []).join(', ') || '(none)'}`,
|
|
382
|
+
`Banned rules: ${(parsed.bannedRegexRules || [])
|
|
383
|
+
.map((rule) => `${rule.id || 'rule'}${rule.message ? ` (${rule.message})` : ''}`)
|
|
384
|
+
.join('; ') || '(none)'}`,
|
|
385
|
+
`Motion config: reducedMotionRequired=${parsed.motion?.reducedMotionRequired !== false}, maxDurationMs=${parsed.motion?.maxDurationMs || 900}, forbidInfiniteAnimations=${parsed.motion?.forbidInfiniteAnimations !== false}`,
|
|
386
|
+
];
|
|
387
|
+
return {
|
|
388
|
+
source,
|
|
389
|
+
title: 'Design System Context',
|
|
390
|
+
content: lines.join('\n'),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
const snippet = raw.split(/\r?\n/).slice(0, 40).join('\n').trim();
|
|
395
|
+
return {
|
|
396
|
+
source,
|
|
397
|
+
title: 'Design System Context',
|
|
398
|
+
content: snippet || '(unreadable design-system content)',
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function shouldLoadRefactorLedger(tokens, includeState) {
|
|
403
|
+
if (includeState)
|
|
404
|
+
return true;
|
|
405
|
+
return tokens.includes('refactor') || tokens.includes('rewrite') || tokens.includes('migration');
|
|
406
|
+
}
|
|
407
|
+
async function loadRoutedContext(outputDir, routes, escalationLevel) {
|
|
408
|
+
const chunks = [];
|
|
409
|
+
for (const route of routes) {
|
|
410
|
+
const candidates = routeCandidatesForLevel(escalationLevel);
|
|
411
|
+
for (const candidate of candidates) {
|
|
412
|
+
const relSource = `context/${route}/${candidate.file}`;
|
|
413
|
+
const fullPath = path.join(outputDir, relSource);
|
|
414
|
+
try {
|
|
415
|
+
const content = await readFileSafe(fullPath);
|
|
416
|
+
if (!content.trim())
|
|
417
|
+
continue;
|
|
418
|
+
chunks.push({
|
|
419
|
+
route,
|
|
420
|
+
level: candidate.level,
|
|
421
|
+
source: relSource,
|
|
422
|
+
title: `${route.toUpperCase()} Context (level-${candidate.level})`,
|
|
423
|
+
content: content.trim(),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// Optional routed context files; skip missing/unreadable paths.
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return chunks;
|
|
432
|
+
}
|
|
29
433
|
function hashContent(content) {
|
|
30
434
|
return createHash('sha256').update(content, 'utf8').digest('hex').slice(0, 16);
|
|
31
435
|
}
|
|
@@ -38,6 +442,40 @@ function metadataScore(section, tokens) {
|
|
|
38
442
|
}
|
|
39
443
|
return score;
|
|
40
444
|
}
|
|
445
|
+
function criticalityScore(section, content, tokens) {
|
|
446
|
+
const criticalHints = [
|
|
447
|
+
'invariant',
|
|
448
|
+
'invariants',
|
|
449
|
+
'constraint',
|
|
450
|
+
'constraints',
|
|
451
|
+
'edge-case',
|
|
452
|
+
'edge-cases',
|
|
453
|
+
'edge case',
|
|
454
|
+
'migration',
|
|
455
|
+
'migrations',
|
|
456
|
+
'contract',
|
|
457
|
+
'contracts',
|
|
458
|
+
'decision',
|
|
459
|
+
'decisions',
|
|
460
|
+
'rollback',
|
|
461
|
+
];
|
|
462
|
+
const sectionSignal = `${section.title} ${section.tags.join(' ')} ${section.source} ${section.type}`.toLowerCase();
|
|
463
|
+
const contentSignal = content.toLowerCase();
|
|
464
|
+
let score = 0;
|
|
465
|
+
for (const hint of criticalHints) {
|
|
466
|
+
if (sectionSignal.includes(hint))
|
|
467
|
+
score += 4;
|
|
468
|
+
else if (contentSignal.includes(hint))
|
|
469
|
+
score += 2;
|
|
470
|
+
if (tokens.includes(hint.replace(/\s+/g, '-')) || tokens.includes(hint.replace(/\s+/g, ''))) {
|
|
471
|
+
if (sectionSignal.includes(hint) || contentSignal.includes(hint))
|
|
472
|
+
score += 2;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (section.priority === 'high')
|
|
476
|
+
score += 1;
|
|
477
|
+
return score;
|
|
478
|
+
}
|
|
41
479
|
function contentScore(content, tokens) {
|
|
42
480
|
const lc = content.toLowerCase();
|
|
43
481
|
let score = 0;
|
|
@@ -80,6 +518,12 @@ export async function retrieve(options) {
|
|
|
80
518
|
const outputDir = options.output || '.devmind';
|
|
81
519
|
const query = options.query || '';
|
|
82
520
|
const tokens = tokenize(query);
|
|
521
|
+
const routeOverride = parseRouteOption(options.route);
|
|
522
|
+
const routedTargets = routeOverride.length > 0 ? routeOverride : detectRoutes(tokens);
|
|
523
|
+
const contractTargets = detectContractTargets(tokens);
|
|
524
|
+
const escalationLevel = detectEscalationLevel(tokens, options.level);
|
|
525
|
+
const includeState = shouldIncludeState(tokens, options.state);
|
|
526
|
+
const includeLedger = shouldLoadRefactorLedger(tokens, includeState);
|
|
83
527
|
const typeFilter = options.type?.toLowerCase();
|
|
84
528
|
const tagFilter = parseTags(options.tags);
|
|
85
529
|
const limit = clampInt(options.limit, 6);
|
|
@@ -88,11 +532,27 @@ export async function retrieve(options) {
|
|
|
88
532
|
const agentsPath = path.join(outputDir, 'AGENTS.md');
|
|
89
533
|
let indexSections = [];
|
|
90
534
|
let agentsContent = '';
|
|
535
|
+
let routedChunks = [];
|
|
536
|
+
let contractChunks = [];
|
|
537
|
+
let designSystemChunk = null;
|
|
538
|
+
let stateEntries = [];
|
|
539
|
+
let refactorLedger = '';
|
|
91
540
|
try {
|
|
92
541
|
indexSections = parseIndexSections(await profiler.section('retrieve.loadIndex', async () => readFileSafe(indexPath)));
|
|
93
542
|
agentsContent = await profiler.section('retrieve.loadAgents', async () => readFileSafe(agentsPath));
|
|
543
|
+
routedChunks = await profiler.section('retrieve.loadRoutedContext', async () => loadRoutedContext(outputDir, routedTargets, escalationLevel));
|
|
544
|
+
contractChunks = await profiler.section('retrieve.loadContractContext', async () => loadContractContext(outputDir, contractTargets));
|
|
545
|
+
designSystemChunk = shouldIncludeDesignSystem(tokens, routedTargets)
|
|
546
|
+
? await profiler.section('retrieve.loadDesignSystemContext', async () => loadDesignSystemContext(outputDir))
|
|
547
|
+
: null;
|
|
548
|
+
stateEntries = includeState
|
|
549
|
+
? await profiler.section('retrieve.loadStateEntries', async () => loadStateEntries(outputDir, escalationLevel))
|
|
550
|
+
: [];
|
|
551
|
+
refactorLedger = includeLedger
|
|
552
|
+
? await profiler.section('retrieve.loadRefactorLedger', async () => readFileSafe(path.join(outputDir, 'context', 'refactor-ledger.md')).catch(() => ''))
|
|
553
|
+
: '';
|
|
94
554
|
}
|
|
95
|
-
catch
|
|
555
|
+
catch {
|
|
96
556
|
const message = `Failed to load retrieval files in ${outputDir}. Run "devmind scan" or "devmind generate --all".`;
|
|
97
557
|
if (jsonMode) {
|
|
98
558
|
jsonFail(message);
|
|
@@ -125,6 +585,16 @@ export async function retrieve(options) {
|
|
|
125
585
|
success: true,
|
|
126
586
|
query,
|
|
127
587
|
outputDir,
|
|
588
|
+
routing: {
|
|
589
|
+
routes: routedTargets,
|
|
590
|
+
contracts: contractTargets,
|
|
591
|
+
escalationLevel,
|
|
592
|
+
},
|
|
593
|
+
contracts: contractChunks,
|
|
594
|
+
designSystem: designSystemChunk,
|
|
595
|
+
routed: routedChunks,
|
|
596
|
+
state: stateEntries,
|
|
597
|
+
ledger: refactorLedger,
|
|
128
598
|
selected: [],
|
|
129
599
|
message: 'No sections matched the requested filters.',
|
|
130
600
|
timestamp: new Date().toISOString(),
|
|
@@ -134,7 +604,6 @@ export async function retrieve(options) {
|
|
|
134
604
|
logger.warn('No sections matched the requested filters.');
|
|
135
605
|
return;
|
|
136
606
|
}
|
|
137
|
-
// Stage 1: metadata filter + ranking
|
|
138
607
|
filtered = filtered
|
|
139
608
|
.map((section) => ({ section, stage1Score: metadataScore(section, tokens) }))
|
|
140
609
|
.sort((a, b) => b.stage1Score - a.stage1Score ||
|
|
@@ -142,7 +611,6 @@ export async function retrieve(options) {
|
|
|
142
611
|
a.section.startLine - b.section.startLine)
|
|
143
612
|
.slice(0, Math.min(24, filtered.length))
|
|
144
613
|
.map((item) => item.section);
|
|
145
|
-
// Stage 2: content ranking
|
|
146
614
|
const ranked = filtered.map((section) => {
|
|
147
615
|
const content = sliceByLines(agentsContent, section.startLine, section.endLine);
|
|
148
616
|
const hash = hashContent(content);
|
|
@@ -151,22 +619,67 @@ export async function retrieve(options) {
|
|
|
151
619
|
logger.warn(`Section hash mismatch for ${section.id}; consider regenerating context.`);
|
|
152
620
|
}
|
|
153
621
|
}
|
|
154
|
-
const
|
|
622
|
+
const criticalScore = criticalityScore(section, content, tokens);
|
|
623
|
+
const score = metadataScore(section, tokens) * 2 + contentScore(content, tokens) + criticalScore;
|
|
155
624
|
return {
|
|
156
625
|
section,
|
|
157
626
|
stage1Score: metadataScore(section, tokens),
|
|
158
627
|
content,
|
|
159
628
|
score,
|
|
629
|
+
criticalityScore: criticalScore,
|
|
160
630
|
};
|
|
161
631
|
});
|
|
162
632
|
ranked.sort((a, b) => b.score - a.score ||
|
|
633
|
+
b.criticalityScore - a.criticalityScore ||
|
|
163
634
|
b.stage1Score - a.stage1Score ||
|
|
164
635
|
a.section.id.localeCompare(b.section.id) ||
|
|
165
636
|
a.section.startLine - b.section.startLine);
|
|
166
637
|
const selected = ranked.slice(0, limit);
|
|
167
|
-
const
|
|
638
|
+
const routedPicked = [];
|
|
168
639
|
let wordBudget = 0;
|
|
640
|
+
const contractPicked = [];
|
|
641
|
+
for (const chunk of contractChunks) {
|
|
642
|
+
const words = chunk.content.split(/\s+/).filter(Boolean).length;
|
|
643
|
+
if (wordBudget + words > maxWords && contractPicked.length > 0)
|
|
644
|
+
continue;
|
|
645
|
+
contractPicked.push(chunk);
|
|
646
|
+
wordBudget += words;
|
|
647
|
+
}
|
|
648
|
+
for (const chunk of routedChunks) {
|
|
649
|
+
const words = chunk.content.split(/\s+/).filter(Boolean).length;
|
|
650
|
+
if (wordBudget + words > maxWords && routedPicked.length > 0)
|
|
651
|
+
continue;
|
|
652
|
+
routedPicked.push(chunk);
|
|
653
|
+
wordBudget += words;
|
|
654
|
+
}
|
|
655
|
+
const includeDesignSystem = !!designSystemChunk &&
|
|
656
|
+
(wordBudget +
|
|
657
|
+
designSystemChunk.content
|
|
658
|
+
.split(/\s+/)
|
|
659
|
+
.filter(Boolean).length <=
|
|
660
|
+
maxWords ||
|
|
661
|
+
(wordBudget === 0 && !!designSystemChunk));
|
|
662
|
+
if (includeDesignSystem && designSystemChunk) {
|
|
663
|
+
wordBudget += designSystemChunk.content.split(/\s+/).filter(Boolean).length;
|
|
664
|
+
}
|
|
665
|
+
const statePicked = [];
|
|
666
|
+
for (const entry of stateEntries) {
|
|
667
|
+
const words = entry.text.split(/\s+/).filter(Boolean).length;
|
|
668
|
+
if (wordBudget + words > maxWords && statePicked.length > 0)
|
|
669
|
+
continue;
|
|
670
|
+
statePicked.push(entry);
|
|
671
|
+
wordBudget += words;
|
|
672
|
+
}
|
|
673
|
+
const ledgerWords = refactorLedger.split(/\s+/).filter(Boolean).length;
|
|
674
|
+
const includeLedgerInOutput = !!refactorLedger && (wordBudget + ledgerWords <= maxWords);
|
|
675
|
+
if (includeLedgerInOutput) {
|
|
676
|
+
wordBudget += ledgerWords;
|
|
677
|
+
}
|
|
678
|
+
const picked = [];
|
|
679
|
+
const sectionLimit = Math.max(0, limit - routedPicked.length - contractPicked.length);
|
|
169
680
|
for (const item of selected) {
|
|
681
|
+
if (picked.length >= sectionLimit)
|
|
682
|
+
break;
|
|
170
683
|
const words = item.content.split(/\s+/).filter(Boolean).length;
|
|
171
684
|
if (wordBudget + words > maxWords && picked.length > 0)
|
|
172
685
|
continue;
|
|
@@ -179,6 +692,16 @@ export async function retrieve(options) {
|
|
|
179
692
|
? {
|
|
180
693
|
query,
|
|
181
694
|
outputDir,
|
|
695
|
+
routing: {
|
|
696
|
+
routes: routedTargets,
|
|
697
|
+
contracts: contractTargets,
|
|
698
|
+
escalationLevel,
|
|
699
|
+
},
|
|
700
|
+
contracts: contractPicked,
|
|
701
|
+
designSystem: includeDesignSystem ? designSystemChunk : null,
|
|
702
|
+
routed: routedPicked,
|
|
703
|
+
state: statePicked,
|
|
704
|
+
ledger: includeLedgerInOutput ? refactorLedger : '',
|
|
182
705
|
selected: picked.map((item) => ({
|
|
183
706
|
id: item.section.id,
|
|
184
707
|
title: item.section.title,
|
|
@@ -188,6 +711,7 @@ export async function retrieve(options) {
|
|
|
188
711
|
startLine: item.section.startLine,
|
|
189
712
|
endLine: item.section.endLine,
|
|
190
713
|
score: item.score,
|
|
714
|
+
criticalityScore: item.criticalityScore,
|
|
191
715
|
content: item.content,
|
|
192
716
|
})),
|
|
193
717
|
profile,
|
|
@@ -195,6 +719,16 @@ export async function retrieve(options) {
|
|
|
195
719
|
: {
|
|
196
720
|
query,
|
|
197
721
|
outputDir,
|
|
722
|
+
routing: {
|
|
723
|
+
routes: routedTargets,
|
|
724
|
+
contracts: contractTargets,
|
|
725
|
+
escalationLevel,
|
|
726
|
+
},
|
|
727
|
+
contracts: contractPicked,
|
|
728
|
+
designSystem: includeDesignSystem ? designSystemChunk : null,
|
|
729
|
+
routed: routedPicked,
|
|
730
|
+
state: statePicked,
|
|
731
|
+
ledger: includeLedgerInOutput ? refactorLedger : '',
|
|
198
732
|
selected: picked.map((item) => ({
|
|
199
733
|
id: item.section.id,
|
|
200
734
|
title: item.section.title,
|
|
@@ -204,6 +738,7 @@ export async function retrieve(options) {
|
|
|
204
738
|
startLine: item.section.startLine,
|
|
205
739
|
endLine: item.section.endLine,
|
|
206
740
|
score: item.score,
|
|
741
|
+
criticalityScore: item.criticalityScore,
|
|
207
742
|
content: item.content,
|
|
208
743
|
})),
|
|
209
744
|
}, null, 2));
|
|
@@ -211,7 +746,60 @@ export async function retrieve(options) {
|
|
|
211
746
|
}
|
|
212
747
|
let output = '# Retrieved Context\n\n';
|
|
213
748
|
output += `Query: ${query || '(none)'}\n`;
|
|
749
|
+
if (routedTargets.length > 0) {
|
|
750
|
+
output += `Routing: ${routedTargets.map((target) => `\`${target}\``).join(', ')}\n`;
|
|
751
|
+
}
|
|
752
|
+
if (contractTargets.length > 0) {
|
|
753
|
+
output += `Contracts: ${contractTargets.map((target) => `\`${target}\``).join(', ')}\n`;
|
|
754
|
+
}
|
|
755
|
+
if (includeDesignSystem && designSystemChunk) {
|
|
756
|
+
output += 'Design system included: yes\n';
|
|
757
|
+
}
|
|
758
|
+
output += `Escalation level: ${escalationLevel}\n`;
|
|
759
|
+
output += `State logs included: ${statePicked.length}\n`;
|
|
214
760
|
output += `Selected sections: ${picked.length}\n\n`;
|
|
761
|
+
if (routedPicked.length > 0) {
|
|
762
|
+
output += '## Routed Context\n\n';
|
|
763
|
+
for (const chunk of routedPicked) {
|
|
764
|
+
output += `### ${chunk.title}\n\n`;
|
|
765
|
+
output += `- Route: \`${chunk.route}\`\n`;
|
|
766
|
+
output += `- Source: \`${chunk.source}\`\n\n`;
|
|
767
|
+
output += `${chunk.content}\n\n`;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (contractPicked.length > 0) {
|
|
771
|
+
output += '## Contract Context\n\n';
|
|
772
|
+
for (const chunk of contractPicked) {
|
|
773
|
+
output += `### ${chunk.title}\n\n`;
|
|
774
|
+
output += `- Contract: \`${chunk.contract}\`\n`;
|
|
775
|
+
output += `- Source: \`${chunk.source}\`\n\n`;
|
|
776
|
+
output += `${chunk.content}\n\n`;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (includeDesignSystem && designSystemChunk) {
|
|
780
|
+
output += '## Design System Context\n\n';
|
|
781
|
+
output += `- Source: \`${designSystemChunk.source}\`\n\n`;
|
|
782
|
+
output += `${designSystemChunk.content}\n\n`;
|
|
783
|
+
}
|
|
784
|
+
if (statePicked.length > 0) {
|
|
785
|
+
output += '## State Log\n\n';
|
|
786
|
+
for (const entry of statePicked) {
|
|
787
|
+
output += `- [${entry.kind}] ${entry.timestamp}`;
|
|
788
|
+
if (entry.status)
|
|
789
|
+
output += ` | status=\`${entry.status}\``;
|
|
790
|
+
if (entry.source)
|
|
791
|
+
output += ` | source=\`${entry.source}\``;
|
|
792
|
+
output += `\n`;
|
|
793
|
+
output += ` ${entry.text}\n`;
|
|
794
|
+
if (entry.note)
|
|
795
|
+
output += ` note: ${entry.note}\n`;
|
|
796
|
+
output += '\n';
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (includeLedgerInOutput) {
|
|
800
|
+
output += '## Refactor Ledger\n\n';
|
|
801
|
+
output += `${refactorLedger}\n\n`;
|
|
802
|
+
}
|
|
215
803
|
for (const item of picked) {
|
|
216
804
|
output += `## ${item.section.title}\n\n`;
|
|
217
805
|
output += `- ID: \`${item.section.id}\`\n`;
|