@nexrall/code-core 1.4.76 → 1.4.78

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 (143) hide show
  1. package/dist/agent/effortCap.d.ts +24 -0
  2. package/dist/agent/effortCap.d.ts.map +1 -0
  3. package/dist/agent/effortCap.js +66 -0
  4. package/dist/agent/goal.d.ts +55 -0
  5. package/dist/agent/goal.d.ts.map +1 -0
  6. package/dist/agent/goal.js +120 -0
  7. package/dist/agent/handoffBrief.d.ts +82 -0
  8. package/dist/agent/handoffBrief.d.ts.map +1 -0
  9. package/dist/agent/handoffBrief.js +121 -0
  10. package/dist/agent/hooks.d.ts +10 -2
  11. package/dist/agent/hooks.d.ts.map +1 -1
  12. package/dist/agent/hooks.js +9 -3
  13. package/dist/agent/modelCatalogue.d.ts +23 -0
  14. package/dist/agent/modelCatalogue.d.ts.map +1 -1
  15. package/dist/agent/modelCatalogue.js +28 -0
  16. package/dist/agent/outputStyles.d.ts +27 -0
  17. package/dist/agent/outputStyles.d.ts.map +1 -0
  18. package/dist/agent/outputStyles.js +174 -0
  19. package/dist/agent/skills.d.ts.map +1 -1
  20. package/dist/agent/skills.js +21 -1
  21. package/dist/api/billingLines.d.ts +11 -0
  22. package/dist/api/billingLines.d.ts.map +1 -0
  23. package/dist/api/billingLines.js +56 -0
  24. package/dist/api/client.d.ts +2 -0
  25. package/dist/api/client.d.ts.map +1 -1
  26. package/dist/api/scheduleLines.d.ts +53 -0
  27. package/dist/api/scheduleLines.d.ts.map +1 -0
  28. package/dist/api/scheduleLines.js +150 -0
  29. package/dist/context/contextFiles.d.ts +81 -0
  30. package/dist/context/contextFiles.d.ts.map +1 -0
  31. package/dist/context/contextFiles.js +310 -0
  32. package/dist/diff/collect.d.ts +25 -0
  33. package/dist/diff/collect.d.ts.map +1 -0
  34. package/dist/diff/collect.js +169 -0
  35. package/dist/diff/diffView.d.ts +57 -0
  36. package/dist/diff/diffView.d.ts.map +1 -0
  37. package/dist/diff/diffView.js +231 -0
  38. package/dist/diff/reviewQueue.d.ts +53 -0
  39. package/dist/diff/reviewQueue.d.ts.map +1 -0
  40. package/dist/diff/reviewQueue.js +349 -0
  41. package/dist/events/recap.d.ts +54 -0
  42. package/dist/events/recap.d.ts.map +1 -0
  43. package/dist/events/recap.js +161 -0
  44. package/dist/events/sessionEvents.d.ts +54 -0
  45. package/dist/events/sessionEvents.d.ts.map +1 -0
  46. package/dist/events/sessionEvents.js +324 -0
  47. package/dist/events/sessionStats.d.ts +89 -0
  48. package/dist/events/sessionStats.d.ts.map +1 -0
  49. package/dist/events/sessionStats.js +264 -0
  50. package/dist/import/claudeImport.d.ts +44 -0
  51. package/dist/import/claudeImport.d.ts.map +1 -0
  52. package/dist/import/claudeImport.js +337 -0
  53. package/dist/index.d.ts +38 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +38 -0
  56. package/dist/mcp/manager.d.ts +8 -0
  57. package/dist/mcp/manager.d.ts.map +1 -1
  58. package/dist/mcp/manager.js +64 -57
  59. package/dist/permissions/destructiveTokens.d.ts +1 -0
  60. package/dist/permissions/destructiveTokens.d.ts.map +1 -1
  61. package/dist/permissions/destructiveTokens.js +93 -0
  62. package/dist/permissions/rules.d.ts +12 -0
  63. package/dist/permissions/rules.d.ts.map +1 -1
  64. package/dist/permissions/rules.js +89 -7
  65. package/dist/plugins/configStore.d.ts +43 -0
  66. package/dist/plugins/configStore.d.ts.map +1 -0
  67. package/dist/plugins/configStore.js +253 -0
  68. package/dist/plugins/files.d.ts +10 -0
  69. package/dist/plugins/files.d.ts.map +1 -0
  70. package/dist/plugins/files.js +85 -0
  71. package/dist/plugins/index.d.ts +3 -22
  72. package/dist/plugins/index.d.ts.map +1 -1
  73. package/dist/plugins/index.js +30 -40
  74. package/dist/plugins/resolve.d.ts +18 -0
  75. package/dist/plugins/resolve.d.ts.map +1 -0
  76. package/dist/plugins/resolve.js +152 -0
  77. package/dist/plugins/userConfig.d.ts +90 -0
  78. package/dist/plugins/userConfig.d.ts.map +1 -0
  79. package/dist/plugins/userConfig.js +332 -0
  80. package/dist/plugins/validate.d.ts +14 -0
  81. package/dist/plugins/validate.d.ts.map +1 -0
  82. package/dist/plugins/validate.js +222 -0
  83. package/dist/project/projectCommands.d.ts +33 -0
  84. package/dist/project/projectCommands.d.ts.map +1 -0
  85. package/dist/project/projectCommands.js +246 -0
  86. package/dist/search/conversationTurns.d.ts +53 -0
  87. package/dist/search/conversationTurns.d.ts.map +1 -0
  88. package/dist/search/conversationTurns.js +170 -0
  89. package/dist/search/globalSearch.d.ts +144 -0
  90. package/dist/search/globalSearch.d.ts.map +1 -0
  91. package/dist/search/globalSearch.js +350 -0
  92. package/dist/share/sessionChannel.d.ts +136 -0
  93. package/dist/share/sessionChannel.d.ts.map +1 -0
  94. package/dist/share/sessionChannel.js +373 -0
  95. package/dist/share/sessionShare.d.ts +104 -0
  96. package/dist/share/sessionShare.d.ts.map +1 -0
  97. package/dist/share/sessionShare.js +336 -0
  98. package/dist/ui/configCmd.d.ts +48 -0
  99. package/dist/ui/configCmd.d.ts.map +1 -0
  100. package/dist/ui/configCmd.js +146 -0
  101. package/dist/ui/deployPanel.d.ts +91 -0
  102. package/dist/ui/deployPanel.d.ts.map +1 -0
  103. package/dist/ui/deployPanel.js +442 -0
  104. package/dist/ui/mergeAssistant.d.ts +120 -0
  105. package/dist/ui/mergeAssistant.d.ts.map +1 -0
  106. package/dist/ui/mergeAssistant.js +391 -0
  107. package/dist/ui/notify.d.ts +114 -0
  108. package/dist/ui/notify.d.ts.map +1 -0
  109. package/dist/ui/notify.js +235 -0
  110. package/dist/ui/promptSuggest.d.ts +56 -0
  111. package/dist/ui/promptSuggest.d.ts.map +1 -0
  112. package/dist/ui/promptSuggest.js +160 -0
  113. package/dist/ui/taskGraph.d.ts +44 -0
  114. package/dist/ui/taskGraph.d.ts.map +1 -0
  115. package/dist/ui/taskGraph.js +289 -0
  116. package/dist/util/addDir.d.ts +31 -0
  117. package/dist/util/addDir.d.ts.map +1 -0
  118. package/dist/util/addDir.js +124 -0
  119. package/dist/util/batch.d.ts +16 -0
  120. package/dist/util/batch.d.ts.map +1 -0
  121. package/dist/util/batch.js +92 -0
  122. package/dist/util/cellWidth.d.ts +12 -0
  123. package/dist/util/cellWidth.d.ts.map +1 -0
  124. package/dist/util/cellWidth.js +161 -0
  125. package/dist/util/duration.d.ts +8 -0
  126. package/dist/util/duration.d.ts.map +1 -0
  127. package/dist/util/duration.js +50 -0
  128. package/dist/util/loopJobs.d.ts +67 -0
  129. package/dist/util/loopJobs.d.ts.map +1 -0
  130. package/dist/util/loopJobs.js +169 -0
  131. package/dist/util/settingsFile.d.ts +14 -0
  132. package/dist/util/settingsFile.d.ts.map +1 -0
  133. package/dist/util/settingsFile.js +99 -0
  134. package/dist/util/toolLabel.d.ts +2 -0
  135. package/dist/util/toolLabel.d.ts.map +1 -0
  136. package/dist/util/toolLabel.js +17 -0
  137. package/dist/verify/failureTimeline.d.ts +100 -0
  138. package/dist/verify/failureTimeline.d.ts.map +1 -0
  139. package/dist/verify/failureTimeline.js +253 -0
  140. package/dist/verify/verification.d.ts +75 -0
  141. package/dist/verify/verification.d.ts.map +1 -0
  142. package/dist/verify/verification.js +315 -0
  143. package/package.json +5 -4
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ // `/diff` — the content of your uncommitted changes, not just a file list.
3
+ //
4
+ // Lives in core (moved from the CLI's ui/diffView.ts, 2026-10-04) so the VS Code
5
+ // panel renders the same parse/summary/render; the CLI module re-exports it and
6
+ // keeps only `paintLine` (the ANSI colouring is CLI-only).
7
+ //
8
+ // WHY: `/diff` used to print `git status --short` + `git diff --stat`, so after an agent turn
9
+ // you still had to leave the CLI to see WHAT changed. Claude Code shows the diff itself, and
10
+ // lets you look at one file. This module is the pure half: parse `git diff` output into
11
+ // files/hunks, pick a file by (partial) name, fold long hunks, and produce display lines.
12
+ // No git, no console, no chalk in the parse/render path, so every rule is unit-testable —
13
+ // see test/diffView.test.ts. Colouring is a separate function (`paintLine`).
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.MAX_UNTRACKED_BYTES = exports.MAX_VIEW_LINES = exports.FOLDED_HUNK_KEEP = exports.MAX_HUNK_LINES = void 0;
16
+ exports.parseUnifiedDiff = parseUnifiedDiff;
17
+ exports.newFileDiff = newFileDiff;
18
+ exports.totals = totals;
19
+ exports.findFiles = findFiles;
20
+ exports.summaryLines = summaryLines;
21
+ exports.renderFile = renderFile;
22
+ exports.renderAll = renderAll;
23
+ exports.capLines = capLines;
24
+ /** A hunk longer than this is folded in the overview (full view never folds hunks). */
25
+ exports.MAX_HUNK_LINES = 40;
26
+ /** Body lines kept from a folded hunk. */
27
+ exports.FOLDED_HUNK_KEEP = 30;
28
+ /** Total lines one rendering may emit before it points at `/diff <file>`. */
29
+ exports.MAX_VIEW_LINES = 400;
30
+ /** Untracked files larger than this are listed, not inlined. */
31
+ exports.MAX_UNTRACKED_BYTES = 200000;
32
+ /** Parse the output of `git diff` (unified, no colour) into files. */
33
+ function parseUnifiedDiff(text) {
34
+ const files = [];
35
+ let file = null;
36
+ let hunk = null;
37
+ let oldCount = 0;
38
+ let newCount = 0;
39
+ for (const raw of text.split('\n')) {
40
+ const line = raw.replace(/\r$/, '');
41
+ const head = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
42
+ // A body line always starts with + - space or \, so it can never match ^diff --git.
43
+ if (head) {
44
+ file = { path: head[2], status: 'modified', additions: 0, deletions: 0, hunks: [] };
45
+ if (head[1] !== head[2])
46
+ file.oldPath = head[1];
47
+ files.push(file);
48
+ hunk = null;
49
+ continue;
50
+ }
51
+ if (!file)
52
+ continue;
53
+ if (hunk && (oldCount > 0 || newCount > 0)) {
54
+ const c = line[0];
55
+ if (c === '+') {
56
+ file.additions++;
57
+ newCount--;
58
+ hunk.lines.push(line);
59
+ continue;
60
+ }
61
+ if (c === '-') {
62
+ file.deletions++;
63
+ oldCount--;
64
+ hunk.lines.push(line);
65
+ continue;
66
+ }
67
+ if (c === '\\') {
68
+ hunk.lines.push(line);
69
+ continue;
70
+ }
71
+ // Context line (git writes a leading space; an empty context line may lose it).
72
+ oldCount--;
73
+ newCount--;
74
+ hunk.lines.push(line === '' ? ' ' : line);
75
+ continue;
76
+ }
77
+ if (hunk && line[0] === '\\') {
78
+ hunk.lines.push(line);
79
+ continue;
80
+ }
81
+ const h = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/.exec(line);
82
+ if (h) {
83
+ oldCount = h[1] === undefined ? 1 : Number(h[1]);
84
+ newCount = h[2] === undefined ? 1 : Number(h[2]);
85
+ hunk = { header: line, lines: [] };
86
+ file.hunks.push(hunk);
87
+ continue;
88
+ }
89
+ if (line.startsWith('new file mode'))
90
+ file.status = 'added';
91
+ else if (line.startsWith('deleted file mode'))
92
+ file.status = 'deleted';
93
+ else if (line.startsWith('rename from ')) {
94
+ file.status = 'renamed';
95
+ file.oldPath = line.slice(12);
96
+ }
97
+ else if (line.startsWith('Binary files ') || line === 'GIT binary patch')
98
+ file.status = 'binary';
99
+ }
100
+ return files;
101
+ }
102
+ /** Synthesise the diff of an untracked (new) text file so `/diff` can show it too. */
103
+ function newFileDiff(path, content) {
104
+ const lines = content.split('\n');
105
+ if (lines[lines.length - 1] === '')
106
+ lines.pop();
107
+ const binary = content.includes('\u0000');
108
+ if (binary)
109
+ return { path, status: 'binary', additions: 0, deletions: 0, hunks: [] };
110
+ return {
111
+ path,
112
+ status: 'added',
113
+ additions: lines.length,
114
+ deletions: 0,
115
+ hunks: lines.length ? [{ header: `@@ -0,0 +1,${lines.length} @@`, lines: lines.map((l) => '+' + l) }] : [],
116
+ };
117
+ }
118
+ function totals(files) {
119
+ return {
120
+ files: files.length,
121
+ additions: files.reduce((n, f) => n + f.additions, 0),
122
+ deletions: files.reduce((n, f) => n + f.deletions, 0),
123
+ };
124
+ }
125
+ /**
126
+ * Find the file a user means by `/diff <name>`: exact path, then unique suffix
127
+ * ("chat.ts"), then unique substring. Returns the matches so the caller can say
128
+ * "ambiguous" instead of silently picking one.
129
+ */
130
+ function findFiles(files, query) {
131
+ const q = query.trim().replace(/^\.\//, '');
132
+ if (!q)
133
+ return [];
134
+ const exact = files.filter((f) => f.path === q);
135
+ if (exact.length)
136
+ return exact;
137
+ const suffix = files.filter((f) => f.path.endsWith('/' + q) || f.path === q);
138
+ if (suffix.length)
139
+ return suffix;
140
+ const lower = q.toLowerCase();
141
+ return files.filter((f) => f.path.toLowerCase().includes(lower));
142
+ }
143
+ const STATUS_LABEL = {
144
+ modified: 'modified',
145
+ added: 'new file',
146
+ deleted: 'deleted',
147
+ renamed: 'renamed',
148
+ binary: 'binary',
149
+ };
150
+ /** One summary row per file: ` M path +12 −3`. */
151
+ function summaryLines(files) {
152
+ const out = [];
153
+ const width = Math.min(60, Math.max(0, ...files.map((f) => f.path.length)));
154
+ for (const f of files) {
155
+ const counts = f.status === 'binary' ? 'binary' : `+${f.additions} −${f.deletions}`;
156
+ const name = f.oldPath && f.status === 'renamed' ? `${f.oldPath} → ${f.path}` : f.path;
157
+ out.push({
158
+ kind: 'meta',
159
+ text: `${name.padEnd(width)} ${counts}${f.status === 'modified' ? '' : ` (${STATUS_LABEL[f.status]})`}`,
160
+ });
161
+ }
162
+ const t = totals(files);
163
+ out.push({
164
+ kind: 'note',
165
+ text: `${t.files} file${t.files === 1 ? '' : 's'} changed, +${t.additions} −${t.deletions}`,
166
+ });
167
+ return out;
168
+ }
169
+ /** Display lines for one file: header, then each hunk, long ones folded when asked. */
170
+ function renderFile(f, opts = {}) {
171
+ const fold = opts.foldHunks !== false;
172
+ const out = [];
173
+ const title = f.oldPath && f.status === 'renamed' ? `${f.oldPath} → ${f.path}` : f.path;
174
+ out.push({
175
+ kind: 'file',
176
+ text: `${title} (${STATUS_LABEL[f.status]}${f.status === 'binary' ? '' : `, +${f.additions} −${f.deletions}`})`,
177
+ });
178
+ if (f.status === 'binary') {
179
+ out.push({ kind: 'note', text: 'binary file — not shown' });
180
+ return out;
181
+ }
182
+ if (!f.hunks.length) {
183
+ out.push({ kind: 'note', text: f.status === 'renamed' ? 'renamed, no content changes' : '(no textual changes)' });
184
+ return out;
185
+ }
186
+ for (const h of f.hunks) {
187
+ out.push({ kind: 'hunk', text: h.header });
188
+ const folded = fold && h.lines.length > exports.MAX_HUNK_LINES;
189
+ const shown = folded ? h.lines.slice(0, exports.FOLDED_HUNK_KEEP) : h.lines;
190
+ for (const l of shown) {
191
+ const c = l[0];
192
+ out.push({ kind: c === '+' ? 'add' : c === '-' ? 'del' : 'ctx', text: l });
193
+ }
194
+ if (folded)
195
+ out.push({
196
+ kind: 'note',
197
+ text: `… ${h.lines.length - exports.FOLDED_HUNK_KEEP} more lines in this hunk — /diff ${f.path} shows all`,
198
+ });
199
+ }
200
+ return out;
201
+ }
202
+ /** Whole-tree view: every file rendered, capped overall so a big refactor doesn't flood. */
203
+ function renderAll(files, opts = {}) {
204
+ const cap = opts.maxLines ?? exports.MAX_VIEW_LINES;
205
+ const out = [];
206
+ for (let i = 0; i < files.length; i++) {
207
+ const chunk = renderFile(files[i], opts);
208
+ if (out.length + chunk.length > cap && out.length > 0) {
209
+ const rest = files.slice(i);
210
+ out.push({
211
+ kind: 'note',
212
+ text: `… ${rest.length} more file${rest.length === 1 ? '' : 's'} not shown (${rest
213
+ .map((f) => f.path)
214
+ .slice(0, 3)
215
+ .join(', ')}${rest.length > 3 ? ', …' : ''}) — /diff <file> to open one`,
216
+ });
217
+ return out;
218
+ }
219
+ if (out.length)
220
+ out.push({ kind: 'ctx', text: '' });
221
+ out.push(...chunk);
222
+ }
223
+ return out;
224
+ }
225
+ /** Cap a single-file view; over the cap, tells the user how many lines were left out. */
226
+ function capLines(lines, max = exports.MAX_VIEW_LINES * 4) {
227
+ if (lines.length <= max)
228
+ return lines;
229
+ return [...lines.slice(0, max), { kind: 'note', text: `… ${lines.length - max} more lines not shown` }];
230
+ }
231
+ //# sourceMappingURL=diffView.js.map
@@ -0,0 +1,53 @@
1
+ import type { DiffFile } from './diffView';
2
+ export type Risk = 'high' | 'medium' | 'low';
3
+ export interface ReviewedFile {
4
+ path: string;
5
+ risk: Risk;
6
+ status: DiffFile['status'];
7
+ additions: number;
8
+ deletions: number;
9
+ /** Observable facts behind `risk`, most significant first. Never a bare number. */
10
+ reasons: string[];
11
+ }
12
+ export interface ReviewGroup {
13
+ risk: Risk;
14
+ files: ReviewedFile[];
15
+ /** How many files this group holds in total — `files` may be a shorter shown slice. */
16
+ total: number;
17
+ }
18
+ export interface ReviewQueue {
19
+ groups: ReviewGroup[];
20
+ totals: {
21
+ files: number;
22
+ additions: number;
23
+ deletions: number;
24
+ high: number;
25
+ };
26
+ }
27
+ export declare function isTestPath(filePath: string): boolean;
28
+ /** Churn past which a change stops being reviewable by skimming. */
29
+ export declare const LARGE_CHANGE_LINES = 200;
30
+ export interface ClassifyContext {
31
+ /** The test files in the same changeset (see `coveringTest`). */
32
+ tests: DiffFile[];
33
+ }
34
+ export declare function classifyFile(f: DiffFile, ctx: ClassifyContext): ReviewedFile;
35
+ export declare function buildReviewQueue(files: DiffFile[], maxPerGroup?: number): ReviewQueue;
36
+ export declare const REVIEW_HEADINGS: Record<Risk, string>;
37
+ /** `/review --all` lifts the per-group cap; the default keeps the answer on one screen. */
38
+ export declare const DEFAULT_PER_GROUP = 8;
39
+ /**
40
+ * Below this width the per-file +/- counts are dropped from the rows: at 40 columns the
41
+ * counts left the reason clipped to "authe…", and the reason is the half that makes the
42
+ * grouping checkable. The header still carries the totals, so nothing is lost that can't be
43
+ * had from `/diff`.
44
+ */
45
+ export declare const COUNTS_MIN_COLUMNS = 60;
46
+ /**
47
+ * Rows for the queue. `columns` is the terminal width the caller measured: lines are clipped
48
+ * here rather than left to wrap, because a wrapped row reads as a second entry.
49
+ */
50
+ export declare function formatReviewQueue(q: ReviewQueue, columns: number, perGroup?: number): string[];
51
+ /** Everything the queue knows about one file — `/review <path>`. */
52
+ export declare function formatReviewDetail(r: ReviewedFile, columns: number): string[];
53
+ //# sourceMappingURL=reviewQueue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviewQueue.d.ts","sourceRoot":"","sources":["../../src/diff/reviewQueue.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,uFAAuF;IACvF,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/E;AAsMD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEpD;AA0BD,oEAAoE;AACpE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAItC,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,eAAe,GAAG,YAAY,CAmC5E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,WAAW,SAA2B,GAAG,WAAW,CAsBvG;AAID,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAkD,CAAC;AAEpG,2FAA2F;AAC3F,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,SAAoB,GAAG,MAAM,EAAE,CAgCzG;AAED,oEAAoE;AACpE,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAS7E"}
@@ -0,0 +1,349 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.COUNTS_MIN_COLUMNS = exports.DEFAULT_PER_GROUP = exports.REVIEW_HEADINGS = exports.LARGE_CHANGE_LINES = void 0;
7
+ exports.isTestPath = isTestPath;
8
+ exports.classifyFile = classifyFile;
9
+ exports.buildReviewQueue = buildReviewQueue;
10
+ exports.formatReviewQueue = formatReviewQueue;
11
+ exports.formatReviewDetail = formatReviewDetail;
12
+ // Smart review queue (spec §63): group the uncommitted changes by how much attention they
13
+ //
14
+ // Lives in core (moved from the CLI's ui/reviewQueue.ts, 2026-10-04) so the VS Code
15
+ // panel renders the same queue; the CLI module is a re-export shim.
16
+ // deserve, so a 40-file diff does not have to be read top to bottom.
17
+ //
18
+ // The spec is explicit that the ranking must come from OBSERVABLE characteristics rather
19
+ // than an opaque score: every file here carries the facts that placed it, and those facts
20
+ // are the only inputs. Nothing consults a model, and nothing guesses at intent.
21
+ //
22
+ // Two independent questions, deliberately kept apart:
23
+ // • WHAT the path is — test, documentation, lockfile, auth code, a migration, deploy
24
+ // config. Identity decides the baseline; a test file that happens to
25
+ // mention auth is a test file, not "auth code".
26
+ // • HOW BIG the change is — churn and deletion, which apply to every file regardless.
27
+ // Plus one relational fact: whether a matching test file changed alongside it.
28
+ const node_path_1 = __importDefault(require("node:path"));
29
+ const cellWidth_1 = require("../util/cellWidth");
30
+ const RISK_ORDER = ['high', 'medium', 'low'];
31
+ /** Bands a file's identity can pin, checked before any subject is considered. */
32
+ const IDENTITY = [
33
+ {
34
+ label: 'a test file',
35
+ risk: 'low',
36
+ words: ['test', 'tests', 'spec', 'specs', '__tests__', 'e2e', 'fixture', 'fixtures', 'snapshot', 'snapshots'],
37
+ },
38
+ {
39
+ label: 'documentation',
40
+ risk: 'low',
41
+ words: ['docs', 'doc', 'readme', 'changelog', 'license', 'licence', 'contributing', 'authors', 'notes'],
42
+ exactly: ['readme', 'changelog', 'license', 'licence', 'contributing'],
43
+ },
44
+ {
45
+ // Before the manifest concept: `package-lock.json` must not be read as `package.json`.
46
+ label: 'a generated lockfile',
47
+ risk: 'low',
48
+ words: [],
49
+ exactly: [
50
+ 'package-lock.json',
51
+ 'yarn.lock',
52
+ 'pnpm-lock.yaml',
53
+ 'bun.lockb',
54
+ 'cargo.lock',
55
+ 'go.sum',
56
+ 'poetry.lock',
57
+ 'composer.lock',
58
+ 'gemfile.lock',
59
+ ],
60
+ },
61
+ ];
62
+ /** What the file is ABOUT. Only consulted for files whose identity is not already pinned. */
63
+ const SUBJECT = [
64
+ {
65
+ label: 'authentication, sessions or access control',
66
+ risk: 'high',
67
+ words: [
68
+ 'auth',
69
+ 'authentication',
70
+ 'authorize',
71
+ 'authorization',
72
+ 'login',
73
+ 'logout',
74
+ 'signin',
75
+ 'signup',
76
+ 'oauth',
77
+ 'saml',
78
+ 'sso',
79
+ 'session',
80
+ 'sessions',
81
+ 'token',
82
+ 'tokens',
83
+ 'jwt',
84
+ 'acl',
85
+ 'rbac',
86
+ 'permission',
87
+ 'permissions',
88
+ 'privilege',
89
+ 'privileges',
90
+ 'credential',
91
+ 'credentials',
92
+ 'password',
93
+ 'passwords',
94
+ 'passwd',
95
+ 'mfa',
96
+ '2fa',
97
+ 'otp',
98
+ 'apikey',
99
+ ],
100
+ },
101
+ {
102
+ label: 'payments or money handling',
103
+ risk: 'high',
104
+ words: [
105
+ 'billing',
106
+ 'payment',
107
+ 'payments',
108
+ 'pay',
109
+ 'invoice',
110
+ 'invoices',
111
+ 'charge',
112
+ 'charges',
113
+ 'checkout',
114
+ 'stripe',
115
+ 'paypal',
116
+ 'refund',
117
+ 'refunds',
118
+ 'subscription',
119
+ 'subscriptions',
120
+ 'pricing',
121
+ 'price',
122
+ 'prices',
123
+ 'ledger',
124
+ 'wallet',
125
+ 'payout',
126
+ 'payouts',
127
+ 'tax',
128
+ 'taxes',
129
+ ],
130
+ },
131
+ {
132
+ label: 'secrets or key material',
133
+ risk: 'high',
134
+ words: ['secrets'],
135
+ exactly: ['.npmrc', '.pypirc', '.netrc', 'id_rsa', 'id_ed25519', '.htpasswd', 'credentials'],
136
+ // `.env`, and every `.env.<suffix>` a project may have invented (staging, ci, local…).
137
+ prefix: ['.env'],
138
+ },
139
+ {
140
+ label: 'database schema or migration',
141
+ risk: 'high',
142
+ words: ['migration', 'migrations', 'schema', 'schemas'],
143
+ },
144
+ {
145
+ label: 'deployment or infrastructure configuration',
146
+ risk: 'high',
147
+ words: [
148
+ 'k8s',
149
+ 'kubernetes',
150
+ 'helm',
151
+ 'terraform',
152
+ 'ansible',
153
+ 'manifests',
154
+ 'argocd',
155
+ 'deploy',
156
+ 'deployment',
157
+ 'deployments',
158
+ 'cloudformation',
159
+ ],
160
+ },
161
+ {
162
+ label: 'a dependency manifest',
163
+ risk: 'medium',
164
+ words: [],
165
+ exactly: [
166
+ 'package.json',
167
+ 'pyproject.toml',
168
+ 'cargo.toml',
169
+ 'requirements.txt',
170
+ 'go.mod',
171
+ 'composer.json',
172
+ 'gemfile',
173
+ 'build.gradle',
174
+ 'pom.xml',
175
+ ],
176
+ },
177
+ ];
178
+ /** A path's segments, without the empty pieces `split` can leave behind. */
179
+ function segmentsOf(filePath) {
180
+ return filePath.split('/').filter((s) => s !== '' && s !== '.' && s !== '..');
181
+ }
182
+ /** The words a segment is spelled with: split on separators, lower-cased. */
183
+ function wordsOf(segment) {
184
+ return segment
185
+ .toLowerCase()
186
+ .split(/[^a-z0-9+#@]+/)
187
+ .filter((w) => w !== '');
188
+ }
189
+ function findConcept(filePath, concepts) {
190
+ const segments = segmentsOf(filePath);
191
+ for (const concept of concepts) {
192
+ for (const seg of segments) {
193
+ const lower = seg.toLowerCase();
194
+ if (concept.exactly?.includes(lower))
195
+ return concept;
196
+ if (concept.prefix?.some((pre) => lower.startsWith(pre)))
197
+ return concept;
198
+ if (concept.words.length && wordsOf(seg).some((w) => concept.words.includes(w)))
199
+ return concept;
200
+ }
201
+ }
202
+ return null;
203
+ }
204
+ // ── what changed ALONGSIDE it ────────────────────────────────────────────────
205
+ function isTestPath(filePath) {
206
+ return findConcept(filePath, IDENTITY)?.label === 'a test file';
207
+ }
208
+ function stemOf(filePath) {
209
+ return node_path_1.default.posix
210
+ .basename(filePath)
211
+ .toLowerCase()
212
+ .replace(/\.(test|spec)\.[a-z0-9]+$/, '')
213
+ .replace(/\.[a-z0-9]+$/, '')
214
+ .replace(/^(test_|_test)/, '');
215
+ }
216
+ /**
217
+ * The test file that covers `filePath`, if the changeset contains one. The changeset having
218
+ * ANY test file is what makes "no test changed" informative: in a repo with no tests at all
219
+ * the absence says nothing about any individual file, so it is not reported per file.
220
+ */
221
+ function coveringTest(filePath, tests) {
222
+ const stem = stemOf(filePath);
223
+ if (!stem)
224
+ return null;
225
+ const dir = node_path_1.default.posix.dirname(filePath);
226
+ const sameDir = tests.filter((t) => node_path_1.default.posix.dirname(t.path) === dir && stemOf(t.path) === stem);
227
+ return sameDir[0] ?? tests.find((t) => stemOf(t.path) === stem) ?? null;
228
+ }
229
+ // ── classifying ──────────────────────────────────────────────────────────────
230
+ /** Churn past which a change stops being reviewable by skimming. */
231
+ exports.LARGE_CHANGE_LINES = 200;
232
+ const rank = (r) => RISK_ORDER.indexOf(r);
233
+ function classifyFile(f, ctx) {
234
+ const reasons = [];
235
+ let risk = 'low';
236
+ const raise = (to, reason) => {
237
+ reasons.push(reason);
238
+ if (rank(to) < rank(risk))
239
+ risk = to;
240
+ };
241
+ // The two questions are answered one at a time on purpose: a file that IS a test or a doc
242
+ // is classified by that alone, so `test/auth.test.ts` is never reported as auth code.
243
+ const identity = findConcept(f.path, IDENTITY);
244
+ if (identity) {
245
+ reasons.push(identity.label);
246
+ }
247
+ else {
248
+ const subject = findConcept(f.path, SUBJECT);
249
+ if (subject)
250
+ raise(subject.risk, subject.label);
251
+ }
252
+ // These apply to every file, whatever it is: a 900-line doc or test change is still a
253
+ // lot to read, and an identity of "test file" must not hide a deletion.
254
+ if (f.status === 'deleted')
255
+ raise('medium', 'file deleted');
256
+ if (f.status === 'binary')
257
+ reasons.push('binary — contents are not shown in a diff');
258
+ const churn = f.additions + f.deletions;
259
+ if (churn >= exports.LARGE_CHANGE_LINES)
260
+ raise('medium', `large change (+${f.additions} −${f.deletions})`);
261
+ // Pairing a source file with its test only means something for non-test files.
262
+ if (!identity && f.status !== 'deleted') {
263
+ const test = coveringTest(f.path, ctx.tests);
264
+ if (test)
265
+ reasons.push(`covered by ${test.path}`);
266
+ else if (ctx.tests.length)
267
+ raise('medium', 'no test file changed for this');
268
+ }
269
+ if (!reasons.length)
270
+ reasons.push('no path or size signal');
271
+ return { path: f.path, risk, status: f.status, additions: f.additions, deletions: f.deletions, reasons };
272
+ }
273
+ function buildReviewQueue(files, maxPerGroup = Number.POSITIVE_INFINITY) {
274
+ const ctx = { tests: files.filter((f) => isTestPath(f.path)) };
275
+ const reviewed = files.map((f) => classifyFile(f, ctx));
276
+ const groups = RISK_ORDER.map((risk) => {
277
+ const inGroup = reviewed
278
+ .filter((r) => r.risk === risk)
279
+ // Biggest first: inside a band, the file with the most to read is the one to start
280
+ // with. Path breaks ties, so the order never depends on `git diff`'s own ordering.
281
+ .sort((a, b) => b.additions + b.deletions - (a.additions + a.deletions) || a.path.localeCompare(b.path));
282
+ return { risk, files: inGroup.slice(0, maxPerGroup), total: inGroup.length };
283
+ }).filter((g) => g.total > 0);
284
+ return {
285
+ groups,
286
+ totals: {
287
+ files: reviewed.length,
288
+ additions: reviewed.reduce((n, r) => n + r.additions, 0),
289
+ deletions: reviewed.reduce((n, r) => n + r.deletions, 0),
290
+ high: reviewed.filter((r) => r.risk === 'high').length,
291
+ },
292
+ };
293
+ }
294
+ // ── rendering ────────────────────────────────────────────────────────────────
295
+ exports.REVIEW_HEADINGS = { high: 'HIGH', medium: 'MEDIUM', low: 'LOW' };
296
+ /** `/review --all` lifts the per-group cap; the default keeps the answer on one screen. */
297
+ exports.DEFAULT_PER_GROUP = 8;
298
+ /**
299
+ * Below this width the per-file +/- counts are dropped from the rows: at 40 columns the
300
+ * counts left the reason clipped to "authe…", and the reason is the half that makes the
301
+ * grouping checkable. The header still carries the totals, so nothing is lost that can't be
302
+ * had from `/diff`.
303
+ */
304
+ exports.COUNTS_MIN_COLUMNS = 60;
305
+ /**
306
+ * Rows for the queue. `columns` is the terminal width the caller measured: lines are clipped
307
+ * here rather than left to wrap, because a wrapped row reads as a second entry.
308
+ */
309
+ function formatReviewQueue(q, columns, perGroup = exports.DEFAULT_PER_GROUP) {
310
+ const { totals } = q;
311
+ const out = [
312
+ (0, cellWidth_1.truncateCells)(`${totals.files} changed file${totals.files === 1 ? '' : 's'}, +${totals.additions} −${totals.deletions}` +
313
+ (totals.high ? ` · ${totals.high} needing care` : ''), columns),
314
+ ];
315
+ if (!q.groups.length)
316
+ return out;
317
+ // A shared name column, so the counts line up; capped so a deep path cannot eat the row.
318
+ const widest = Math.max(0, ...q.groups.flatMap((g) => g.files.map((f) => (0, cellWidth_1.visibleLen)(f.path))));
319
+ const nameWidth = Math.min(widest, Math.max(12, Math.floor(columns * 0.45)));
320
+ const withCounts = columns >= exports.COUNTS_MIN_COLUMNS;
321
+ for (const g of q.groups) {
322
+ out.push('', (0, cellWidth_1.truncateCells)(`${exports.REVIEW_HEADINGS[g.risk]} (${g.total})`, columns));
323
+ for (const f of g.files) {
324
+ const name = (0, cellWidth_1.visibleLen)(f.path) > nameWidth ? (0, cellWidth_1.truncateCells)(f.path, nameWidth) : f.path;
325
+ const counts = f.status === 'binary' ? 'binary' : `+${f.additions} −${f.deletions}`;
326
+ const left = ' ' + name + ' '.repeat(Math.max(1, nameWidth - (0, cellWidth_1.visibleLen)(name) + 2)) + (withCounts ? counts.padEnd(12) : '');
327
+ // The reason is what makes the grouping checkable, so it is the part that survives
328
+ // when the row has to be clipped.
329
+ out.push((0, cellWidth_1.truncateCells)(left + f.reasons[0], columns));
330
+ }
331
+ if (g.total > g.files.length) {
332
+ out.push((0, cellWidth_1.truncateCells)(` … ${g.total - g.files.length} more (--all to show)`, columns));
333
+ }
334
+ }
335
+ return out;
336
+ }
337
+ /** Everything the queue knows about one file — `/review <path>`. */
338
+ function formatReviewDetail(r, columns) {
339
+ const out = [
340
+ (0, cellWidth_1.truncateCells)(`${r.path} [${exports.REVIEW_HEADINGS[r.risk]}]`, columns),
341
+ (0, cellWidth_1.truncateCells)(` ${r.status === 'binary' ? 'binary' : `+${r.additions} −${r.deletions}`} · ${r.status}`, columns),
342
+ '',
343
+ ' why:',
344
+ ];
345
+ for (const reason of r.reasons)
346
+ out.push((0, cellWidth_1.truncateCells)(' · ' + reason, columns));
347
+ return out;
348
+ }
349
+ //# sourceMappingURL=reviewQueue.js.map