ctxman 3.1.0 → 3.2.7
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/.ctxman/llm-profiles.json +6 -6
- package/README.md +17 -10
- package/bin/cli.js +107 -8
- package/bin/cm-ask.js +74 -70
- package/bin/cm-audit.js +80 -7
- package/bin/cm-diagnose.js +199 -0
- package/bin/cm-focus.js +322 -0
- package/bin/cm-map.js +71 -18
- package/bin/cm-pack.js +36 -5
- package/bin/cm-policy.js +107 -2
- package/bin/cm-pr.js +18 -3
- package/bin/cm-search.js +84 -19
- package/bin/cm-task.js +192 -0
- package/bin/cm-who.js +119 -0
- package/bin/cm-why.js +41 -5
- package/examples/github-actions/ctxman-check.yml +1 -1
- package/index.d.ts +931 -1
- package/index.js +105 -1
- package/lib/analyzers/performance-dashboard.js +7 -3
- package/lib/analyzers/token-calculator.js +76 -4
- package/lib/api/mcp/server.js +713 -14
- package/lib/cache/CacheManager.js +161 -47
- package/lib/core/Analyzer.js +61 -5
- package/lib/diagnose/index.js +712 -0
- package/lib/focus/index.js +790 -0
- package/lib/map/call-graph.js +570 -0
- package/lib/map/graph.js +30 -5
- package/lib/map/index.js +12 -4
- package/lib/map/rank.js +45 -7
- package/lib/map/render.js +89 -28
- package/lib/pack/index.js +20 -1
- package/lib/pack/select.js +8 -1
- package/lib/parsers/gitignore-parser.js +4 -1
- package/lib/policy/audit.js +22 -2
- package/lib/policy/index.js +6 -0
- package/lib/policy/workspace.js +254 -0
- package/lib/pr/pr-context.js +292 -31
- package/lib/pr/pr-renderer.js +25 -10
- package/lib/rag/EmbeddingProvider.js +139 -2
- package/lib/rag/VectorStoreFactory.js +10 -4
- package/lib/rag/ask.js +291 -0
- package/lib/rag/sources/FileDataSource.js +6 -3
- package/lib/rag/stores/SqliteVectorStore.js +595 -0
- package/lib/search/hybrid.js +324 -0
- package/lib/search/symbols.js +75 -11
- package/lib/task/index.js +630 -0
- package/lib/utils/file-utils.js +1 -0
- package/lib/utils/logger.js +5 -2
- package/lib/utils/multi-repo-manager.js +4 -1
- package/lib/watch/FileWatcher.js +121 -31
- package/lib/watch/IncrementalAnalyzer.js +964 -16
- package/lib/who/index.js +893 -0
- package/lib/why/analyzer-bridge.js +223 -0
- package/lib/why/discussions.js +314 -0
- package/lib/why/index.js +356 -30
- package/lib/why/render.js +154 -9
- package/lib/wizards/config-generator.js +20 -18
- package/lib/wizards/project-detector.js +2 -1
- package/lib/workspace/index.js +1274 -0
- package/package.json +2 -2
package/lib/why/render.js
CHANGED
|
@@ -12,6 +12,8 @@ export const STYLES = ['markdown', 'xml', 'json'];
|
|
|
12
12
|
/** Sections a budget can shorten */
|
|
13
13
|
export const DROP_ORDER = [
|
|
14
14
|
'patches',
|
|
15
|
+
'discussions',
|
|
16
|
+
'citations',
|
|
15
17
|
'related',
|
|
16
18
|
'tests',
|
|
17
19
|
'coupling',
|
|
@@ -23,13 +25,15 @@ export const DROP_ORDER = [
|
|
|
23
25
|
];
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
|
-
* What a budget drops, first to last, as [section, items kept at least]. The first
|
|
27
|
-
* steps follow the documented priority (patches, related text, tests,
|
|
28
|
-
* older commits, keeping the newest one); the rest only run when the
|
|
29
|
-
* than the core of the document
|
|
28
|
+
* What a budget drops, first to last, as [section, items kept at least]. The first six
|
|
29
|
+
* steps follow the documented priority (patches, remote discussions, related text, tests,
|
|
30
|
+
* coupling details, older commits, keeping the newest one); the rest only run when the
|
|
31
|
+
* budget is smaller than the core of the document
|
|
30
32
|
*/
|
|
31
33
|
const DROP_STEPS = [
|
|
32
34
|
['patches', 0],
|
|
35
|
+
['discussions', 0],
|
|
36
|
+
['citations', 0],
|
|
33
37
|
['related', 0],
|
|
34
38
|
['tests', 0],
|
|
35
39
|
['coupling', 0],
|
|
@@ -43,6 +47,8 @@ const DROP_STEPS = [
|
|
|
43
47
|
|
|
44
48
|
const OMITTED_LABELS = {
|
|
45
49
|
patches: 'patches',
|
|
50
|
+
discussions: 'remote discussions',
|
|
51
|
+
citations: 'institutional citations',
|
|
46
52
|
related: 'related text snippets',
|
|
47
53
|
tests: 'test files',
|
|
48
54
|
coupling: 'co-changed files',
|
|
@@ -106,6 +112,8 @@ function flattenReferences(references) {
|
|
|
106
112
|
export function countItems(data) {
|
|
107
113
|
return {
|
|
108
114
|
patches: data.history.commits.filter((commit) => commit.patch).length,
|
|
115
|
+
discussions: (data.discussions ?? []).length,
|
|
116
|
+
citations: (data.institutionalCitations ?? []).length,
|
|
109
117
|
related: data.related.length,
|
|
110
118
|
tests: data.tests.length,
|
|
111
119
|
coupling: data.coupling?.files.length ?? 0,
|
|
@@ -162,6 +170,8 @@ export function applyView(data, view) {
|
|
|
162
170
|
target,
|
|
163
171
|
history: { ...data.history, commits },
|
|
164
172
|
references,
|
|
173
|
+
discussions: (data.discussions ?? []).slice(0, view.discussions),
|
|
174
|
+
institutionalCitations: (data.institutionalCitations ?? []).slice(0, view.citations ?? 0),
|
|
165
175
|
ownership: data.ownership && {
|
|
166
176
|
...data.ownership,
|
|
167
177
|
authors: data.ownership.authors.slice(0, view.ownership),
|
|
@@ -293,8 +303,11 @@ function historyDescription(history) {
|
|
|
293
303
|
|
|
294
304
|
function renderMarkdown(data, budget) {
|
|
295
305
|
const out = [`# Why: ${title(data.target)}`, ''];
|
|
306
|
+
const hasDiscussions = (data.discussions?.length ?? 0) > 0 || Boolean(data.omitted?.discussions);
|
|
296
307
|
out.push(
|
|
297
|
-
|
|
308
|
+
hasDiscussions
|
|
309
|
+
? 'Context for explaining why this code looks the way it does, collected by `ctxman why` from the local git history, repository, and remote issue/PR discussions.'
|
|
310
|
+
: 'Context for explaining why this code looks the way it does, collected by `ctxman why` from the local git history and the repository (no network).',
|
|
298
311
|
''
|
|
299
312
|
);
|
|
300
313
|
for (const warning of data.warnings) out.push(`> **Warning:** ${warning}`);
|
|
@@ -307,6 +320,8 @@ function renderMarkdown(data, budget) {
|
|
|
307
320
|
markdownTarget,
|
|
308
321
|
markdownHistory,
|
|
309
322
|
markdownReferences,
|
|
323
|
+
markdownDiscussions,
|
|
324
|
+
markdownCitations,
|
|
310
325
|
markdownOwnership,
|
|
311
326
|
markdownCoupling,
|
|
312
327
|
markdownRelated,
|
|
@@ -320,8 +335,11 @@ function renderMarkdown(data, budget) {
|
|
|
320
335
|
return out.join('\n');
|
|
321
336
|
}
|
|
322
337
|
|
|
323
|
-
function markdownTarget({ target, repository, omitted }) {
|
|
338
|
+
function markdownTarget({ target, repository, repos, omitted }) {
|
|
324
339
|
const out = ['## Target', '', `- File: ${code(target.path)}`];
|
|
340
|
+
if (Array.isArray(repos) && repos.length > 0) {
|
|
341
|
+
out.push(`- Workspace repositories: ${repos.map((r) => code(r.alias)).join(', ')}`);
|
|
342
|
+
}
|
|
325
343
|
if (target.symbol) {
|
|
326
344
|
const line = target.line ? `, declared on line ${target.line}` : '';
|
|
327
345
|
out.push(`- Symbol: ${code(target.symbol)} (${target.kind}${line})`);
|
|
@@ -372,7 +390,7 @@ function markdownHistory({ history, omitted }) {
|
|
|
372
390
|
);
|
|
373
391
|
}
|
|
374
392
|
if (commit.refs?.length) meta.push(`refs: ${commit.refs.join(', ')}`);
|
|
375
|
-
out.push(meta.join('
|
|
393
|
+
out.push(meta.join(' \u00b7 '));
|
|
376
394
|
if (commit.body) {
|
|
377
395
|
out.push('', commit.body.replace(/^/gm, '> ').replace(/^> $/gm, '>'));
|
|
378
396
|
}
|
|
@@ -407,6 +425,68 @@ function markdownReferences({ references, history, omitted }) {
|
|
|
407
425
|
return out;
|
|
408
426
|
}
|
|
409
427
|
|
|
428
|
+
function markdownDiscussions({ discussions = [], omitted }) {
|
|
429
|
+
if (!discussions.length) return [];
|
|
430
|
+
const out = ['## Discussions', ''];
|
|
431
|
+
for (const item of discussions) {
|
|
432
|
+
const label = { pr: 'Pull request', mr: 'Merge request' }[item.kind] ?? 'Issue';
|
|
433
|
+
const titleText = item.title ? `: ${item.title}` : '';
|
|
434
|
+
out.push(`### ${label} ${item.id}${titleText}`, '');
|
|
435
|
+
const meta = [item.state, item.author ? `@${item.author}` : null, day(item.createdAt)].filter(
|
|
436
|
+
Boolean
|
|
437
|
+
);
|
|
438
|
+
if (item.labels?.length) meta.push(`labels: ${item.labels.join(', ')}`);
|
|
439
|
+
if (item.commits?.length) meta.push(`commits: ${item.commits.join(', ')}`);
|
|
440
|
+
if (item.url) meta.push(item.url);
|
|
441
|
+
out.push(meta.join(' \u00b7 '));
|
|
442
|
+
if (item.body) {
|
|
443
|
+
out.push('', item.body.replace(/^/gm, '> ').replace(/^> $/gm, '>'));
|
|
444
|
+
}
|
|
445
|
+
if (item.comments?.length) {
|
|
446
|
+
out.push('', 'Comments:');
|
|
447
|
+
for (const comment of item.comments) {
|
|
448
|
+
const header = `@${comment.author}${comment.createdAt ? ` (${day(comment.createdAt)})` : ''}`;
|
|
449
|
+
const singleLine = comment.body.replace(/\n+/g, ' ');
|
|
450
|
+
out.push(`- **${header}:** ${singleLine}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
out.push('');
|
|
454
|
+
}
|
|
455
|
+
if (out.at(-1) === '') out.pop();
|
|
456
|
+
if (omitted.discussions) {
|
|
457
|
+
out.push(
|
|
458
|
+
'',
|
|
459
|
+
`_${omitted.discussions} remote discussion(s) not shown to fit the token budget._`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function markdownCitations({ institutionalCitations, omitted }) {
|
|
466
|
+
if (!institutionalCitations || !institutionalCitations.length) return [];
|
|
467
|
+
const out = ['## Institutional Citations (Analyzer)', ''];
|
|
468
|
+
for (const item of institutionalCitations) {
|
|
469
|
+
const meta = [item.kind];
|
|
470
|
+
if (item.author) meta.push(`@${item.author}`);
|
|
471
|
+
if (item.timestamp) meta.push(day(item.timestamp));
|
|
472
|
+
if (item.url) meta.push(item.url);
|
|
473
|
+
out.push(`- **${item.title}** (${meta.join(' · ')}):`);
|
|
474
|
+
if (item.snippet) {
|
|
475
|
+
out.push(` > ${item.snippet.replace(/\n+/g, ' ')}`);
|
|
476
|
+
}
|
|
477
|
+
if (item.refs && item.refs.length) {
|
|
478
|
+
out.push(` *Refs: ${item.refs.join(', ')}*`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (omitted?.citations) {
|
|
482
|
+
out.push(
|
|
483
|
+
'',
|
|
484
|
+
`_${omitted.citations} institutional citation(s) not shown to fit the token budget._`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
|
|
410
490
|
function markdownOwnership({ ownership, omitted }) {
|
|
411
491
|
if (!ownership || !ownership.authors.length) return [];
|
|
412
492
|
const scope =
|
|
@@ -427,7 +507,7 @@ function markdownOwnership({ ownership, omitted }) {
|
|
|
427
507
|
`| ${name} | ${author.lines} | ${percent(author.share)} | ${author.commits} | ${day(author.lastTouched)} |`
|
|
428
508
|
);
|
|
429
509
|
}
|
|
430
|
-
if (omitted.ownership) out.push(`|
|
|
510
|
+
if (omitted.ownership) out.push(`| \u2026 ${omitted.ownership} more | | | | |`);
|
|
431
511
|
return out;
|
|
432
512
|
}
|
|
433
513
|
|
|
@@ -452,7 +532,7 @@ function markdownCoupling({ coupling, target, omitted }) {
|
|
|
452
532
|
const gone = file.exists === false ? ' (no longer in the tree)' : '';
|
|
453
533
|
out.push(`| ${code(file.path)}${gone} | ${file.count} | ${percent(file.confidence)} |`);
|
|
454
534
|
}
|
|
455
|
-
if (omitted.coupling) out.push(`|
|
|
535
|
+
if (omitted.coupling) out.push(`| \u2026 ${omitted.coupling} more | | |`);
|
|
456
536
|
return out;
|
|
457
537
|
}
|
|
458
538
|
|
|
@@ -534,6 +614,13 @@ function attributes(values) {
|
|
|
534
614
|
function renderXml(data, budget) {
|
|
535
615
|
const { target, history } = data;
|
|
536
616
|
const out = ['<why_context generator="ctxman why">'];
|
|
617
|
+
if (Array.isArray(data.repos) && data.repos.length > 0) {
|
|
618
|
+
out.push('<repositories>');
|
|
619
|
+
for (const repo of data.repos) {
|
|
620
|
+
out.push(`<repo${attributes({ alias: repo.alias, root: repo.root })}/>`);
|
|
621
|
+
}
|
|
622
|
+
out.push('</repositories>');
|
|
623
|
+
}
|
|
537
624
|
if (Object.keys(data.omitted).length) {
|
|
538
625
|
out.push(`<trimmed${attributes({ budget, ...data.omitted })}/>`);
|
|
539
626
|
}
|
|
@@ -617,6 +704,64 @@ function renderXml(data, budget) {
|
|
|
617
704
|
}
|
|
618
705
|
out.push('</references>');
|
|
619
706
|
|
|
707
|
+
if ((data.discussions?.length ?? 0) > 0) {
|
|
708
|
+
out.push('<discussions>');
|
|
709
|
+
for (const item of data.discussions) {
|
|
710
|
+
out.push(
|
|
711
|
+
`<discussion${attributes({
|
|
712
|
+
id: item.id,
|
|
713
|
+
number: item.number,
|
|
714
|
+
repo: item.repo,
|
|
715
|
+
provider: item.provider,
|
|
716
|
+
kind: item.kind,
|
|
717
|
+
state: item.state,
|
|
718
|
+
author: item.author,
|
|
719
|
+
created_at: item.createdAt,
|
|
720
|
+
url: item.url,
|
|
721
|
+
labels: item.labels,
|
|
722
|
+
commits: item.commits,
|
|
723
|
+
})}>`
|
|
724
|
+
);
|
|
725
|
+
if (item.title) out.push(`<title>${xml(item.title)}</title>`);
|
|
726
|
+
if (item.body) out.push(`<body>${xml(item.body)}</body>`);
|
|
727
|
+
for (const comment of item.comments ?? []) {
|
|
728
|
+
out.push(
|
|
729
|
+
`<comment${attributes({
|
|
730
|
+
author: comment.author,
|
|
731
|
+
created_at: comment.createdAt,
|
|
732
|
+
})}>${xml(comment.body)}</comment>`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
out.push('</discussion>');
|
|
736
|
+
}
|
|
737
|
+
out.push('</discussions>');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if ((data.institutionalCitations?.length ?? 0) > 0) {
|
|
741
|
+
out.push('<institutional_citations>');
|
|
742
|
+
for (const item of data.institutionalCitations) {
|
|
743
|
+
out.push(
|
|
744
|
+
`<citation${attributes({
|
|
745
|
+
id: item.id,
|
|
746
|
+
kind: item.kind,
|
|
747
|
+
title: item.title,
|
|
748
|
+
url: item.url,
|
|
749
|
+
author: item.author,
|
|
750
|
+
timestamp: item.timestamp,
|
|
751
|
+
score: item.score,
|
|
752
|
+
})}>`
|
|
753
|
+
);
|
|
754
|
+
if (item.snippet) out.push(`<snippet>${xml(item.snippet)}</snippet>`);
|
|
755
|
+
if (item.refs?.length) {
|
|
756
|
+
for (const ref of item.refs) {
|
|
757
|
+
out.push(`<ref>${xml(ref)}</ref>`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
out.push('</citation>');
|
|
761
|
+
}
|
|
762
|
+
out.push('</institutional_citations>');
|
|
763
|
+
}
|
|
764
|
+
|
|
620
765
|
if (data.ownership) {
|
|
621
766
|
const own = data.ownership;
|
|
622
767
|
out.push(
|
|
@@ -81,37 +81,39 @@ class ConfigGenerator {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const filePath = path.join(this.projectRoot, filename);
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
// Check if should skip
|
|
87
|
-
if (exists && !force) {
|
|
84
|
+
const skip = () =>
|
|
88
85
|
result.skipped.push({
|
|
89
86
|
type,
|
|
90
87
|
filename,
|
|
91
88
|
reason: 'File already exists (use --force to overwrite)',
|
|
92
89
|
});
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Generate file content
|
|
97
|
-
const content = this.generateFileContent(type, patterns);
|
|
98
90
|
|
|
99
91
|
if (dryRun) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
92
|
+
const exists = fs.existsSync(filePath);
|
|
93
|
+
if (exists && !force) {
|
|
94
|
+
skip();
|
|
95
|
+
continue;
|
|
104
96
|
}
|
|
97
|
+
const content = this.generateFileContent(type, patterns);
|
|
98
|
+
(exists ? result.overwritten : result.created).push({ type, filename, content });
|
|
105
99
|
continue;
|
|
106
100
|
}
|
|
107
101
|
|
|
108
|
-
|
|
102
|
+
const content = this.generateFileContent(type, patterns);
|
|
103
|
+
// Create exclusively first, so an existing file is found by the write itself rather
|
|
104
|
+
// than by a separate check that could be out of date
|
|
109
105
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
result.overwritten.push({ type, filename });
|
|
113
|
-
} else {
|
|
106
|
+
try {
|
|
107
|
+
fs.writeFileSync(filePath, content, { encoding: 'utf-8', flag: 'wx' });
|
|
114
108
|
result.created.push({ type, filename });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error.code !== 'EEXIST') throw error;
|
|
111
|
+
if (!force) {
|
|
112
|
+
skip();
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
116
|
+
result.overwritten.push({ type, filename });
|
|
115
117
|
}
|
|
116
118
|
} catch (error) {
|
|
117
119
|
result.errors.push({
|
|
@@ -514,7 +514,8 @@ class ProjectDetector {
|
|
|
514
514
|
* @returns {boolean} True if matches
|
|
515
515
|
*/
|
|
516
516
|
matchesGlob(filename, pattern) {
|
|
517
|
-
|
|
517
|
+
// Every regex character is literal except `*`, which matches any run of characters
|
|
518
|
+
const regex = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
518
519
|
return new RegExp(`^${regex}$`).test(filename);
|
|
519
520
|
}
|
|
520
521
|
|