projscan 2.0.0 → 2.2.0

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 (120) hide show
  1. package/README.md +38 -29
  2. package/dist/analyzers/supplyChainCheck.d.ts +2 -0
  3. package/dist/analyzers/supplyChainCheck.js +400 -0
  4. package/dist/analyzers/supplyChainCheck.js.map +1 -0
  5. package/dist/cli/_shared.d.ts +1 -0
  6. package/dist/cli/_shared.js +19 -3
  7. package/dist/cli/_shared.js.map +1 -1
  8. package/dist/cli/commands/analyze.js +7 -3
  9. package/dist/cli/commands/analyze.js.map +1 -1
  10. package/dist/cli/commands/applyFix.js +2 -2
  11. package/dist/cli/commands/applyFix.js.map +1 -1
  12. package/dist/cli/commands/audit.js +2 -2
  13. package/dist/cli/commands/audit.js.map +1 -1
  14. package/dist/cli/commands/badge.js +2 -1
  15. package/dist/cli/commands/badge.js.map +1 -1
  16. package/dist/cli/commands/ci.js +3 -3
  17. package/dist/cli/commands/ci.js.map +1 -1
  18. package/dist/cli/commands/coupling.js +2 -2
  19. package/dist/cli/commands/coupling.js.map +1 -1
  20. package/dist/cli/commands/coverage.js +2 -2
  21. package/dist/cli/commands/coverage.js.map +1 -1
  22. package/dist/cli/commands/dependencies.js +2 -2
  23. package/dist/cli/commands/dependencies.js.map +1 -1
  24. package/dist/cli/commands/diagram.js +2 -2
  25. package/dist/cli/commands/diagram.js.map +1 -1
  26. package/dist/cli/commands/diff.js +2 -2
  27. package/dist/cli/commands/diff.js.map +1 -1
  28. package/dist/cli/commands/doctor.js +3 -3
  29. package/dist/cli/commands/doctor.js.map +1 -1
  30. package/dist/cli/commands/explain.js +2 -2
  31. package/dist/cli/commands/explain.js.map +1 -1
  32. package/dist/cli/commands/explainIssue.js +2 -2
  33. package/dist/cli/commands/explainIssue.js.map +1 -1
  34. package/dist/cli/commands/file.js +2 -2
  35. package/dist/cli/commands/file.js.map +1 -1
  36. package/dist/cli/commands/fix.js +2 -1
  37. package/dist/cli/commands/fix.js.map +1 -1
  38. package/dist/cli/commands/fixSuggest.js +2 -2
  39. package/dist/cli/commands/fixSuggest.js.map +1 -1
  40. package/dist/cli/commands/help.js +2 -1
  41. package/dist/cli/commands/help.js.map +1 -1
  42. package/dist/cli/commands/hotspots.js +2 -2
  43. package/dist/cli/commands/hotspots.js.map +1 -1
  44. package/dist/cli/commands/impact.js +2 -2
  45. package/dist/cli/commands/impact.js.map +1 -1
  46. package/dist/cli/commands/init.js +2 -1
  47. package/dist/cli/commands/init.js.map +1 -1
  48. package/dist/cli/commands/installHook.js +2 -1
  49. package/dist/cli/commands/installHook.js.map +1 -1
  50. package/dist/cli/commands/mcp.js +2 -1
  51. package/dist/cli/commands/mcp.js.map +1 -1
  52. package/dist/cli/commands/memory.js +5 -4
  53. package/dist/cli/commands/memory.js.map +1 -1
  54. package/dist/cli/commands/outdated.js +2 -2
  55. package/dist/cli/commands/outdated.js.map +1 -1
  56. package/dist/cli/commands/plugin.js +83 -3
  57. package/dist/cli/commands/plugin.js.map +1 -1
  58. package/dist/cli/commands/prDiff.js +2 -2
  59. package/dist/cli/commands/prDiff.js.map +1 -1
  60. package/dist/cli/commands/preflight.d.ts +1 -0
  61. package/dist/cli/commands/preflight.js +80 -0
  62. package/dist/cli/commands/preflight.js.map +1 -0
  63. package/dist/cli/commands/review.js +2 -2
  64. package/dist/cli/commands/review.js.map +1 -1
  65. package/dist/cli/commands/search.js +2 -2
  66. package/dist/cli/commands/search.js.map +1 -1
  67. package/dist/cli/commands/session.js +5 -5
  68. package/dist/cli/commands/session.js.map +1 -1
  69. package/dist/cli/commands/structure.js +2 -2
  70. package/dist/cli/commands/structure.js.map +1 -1
  71. package/dist/cli/commands/taint.js +2 -2
  72. package/dist/cli/commands/taint.js.map +1 -1
  73. package/dist/cli/commands/upgrade.js +2 -2
  74. package/dist/cli/commands/upgrade.js.map +1 -1
  75. package/dist/cli/commands/watch.js +2 -1
  76. package/dist/cli/commands/watch.js.map +1 -1
  77. package/dist/cli/commands/workspace.js +4 -2
  78. package/dist/cli/commands/workspace.js.map +1 -1
  79. package/dist/cli/commands/workspaces.js +2 -2
  80. package/dist/cli/commands/workspaces.js.map +1 -1
  81. package/dist/cli/index.js +2 -0
  82. package/dist/cli/index.js.map +1 -1
  83. package/dist/core/issueEngine.js +2 -0
  84. package/dist/core/issueEngine.js.map +1 -1
  85. package/dist/core/pluginDx.d.ts +14 -0
  86. package/dist/core/pluginDx.js +298 -0
  87. package/dist/core/pluginDx.js.map +1 -0
  88. package/dist/core/plugins.d.ts +5 -6
  89. package/dist/core/plugins.js +99 -13
  90. package/dist/core/plugins.js.map +1 -1
  91. package/dist/core/preflight.d.ts +11 -0
  92. package/dist/core/preflight.js +450 -0
  93. package/dist/core/preflight.js.map +1 -0
  94. package/dist/core/review.js +132 -3
  95. package/dist/core/review.js.map +1 -1
  96. package/dist/core/sessionResources.d.ts +6 -0
  97. package/dist/core/sessionResources.js +216 -0
  98. package/dist/core/sessionResources.js.map +1 -0
  99. package/dist/mcp/resources.js +25 -0
  100. package/dist/mcp/resources.js.map +1 -1
  101. package/dist/mcp/tools/preflight.d.ts +2 -0
  102. package/dist/mcp/tools/preflight.js +51 -0
  103. package/dist/mcp/tools/preflight.js.map +1 -0
  104. package/dist/mcp/tools.js +2 -0
  105. package/dist/mcp/tools.js.map +1 -1
  106. package/dist/projscan-sbom.cdx.json +4589 -0
  107. package/dist/reporters/htmlReporter.d.ts +2 -1
  108. package/dist/reporters/htmlReporter.js +70 -0
  109. package/dist/reporters/htmlReporter.js.map +1 -1
  110. package/dist/tool-manifest.json +33 -3
  111. package/dist/types.d.ts +141 -0
  112. package/dist/utils/banner.js +14 -4
  113. package/dist/utils/banner.js.map +1 -1
  114. package/dist/utils/fileWalker.js +4 -0
  115. package/dist/utils/fileWalker.js.map +1 -1
  116. package/dist/utils/formatSupport.d.ts +58 -0
  117. package/dist/utils/formatSupport.js +63 -0
  118. package/dist/utils/formatSupport.js.map +1 -0
  119. package/docs/PLUGIN-AUTHORING.md +30 -0
  120. package/package.json +12 -7
@@ -0,0 +1,450 @@
1
+ import { scanRepository } from './repositoryScanner.js';
2
+ import { collectIssues } from './issueEngine.js';
3
+ import { analyzeHotspots } from './hotspotAnalyzer.js';
4
+ import { computeReview } from './review.js';
5
+ import { loadSession } from './session.js';
6
+ import { PLUGIN_PREVIEW_FLAG, pluginsEnabled } from './plugins.js';
7
+ import { getChangedFiles } from '../utils/changedFiles.js';
8
+ import { loadConfig, applyConfigToIssues } from '../utils/config.js';
9
+ import { calculateScore } from '../utils/scoreCalculator.js';
10
+ const DEFAULT_MAX_CHANGED_FILES = 50;
11
+ const MAX_EVIDENCE_FILES = 40;
12
+ export async function computePreflight(rootPath, options = {}) {
13
+ const mode = options.mode ?? 'before_edit';
14
+ const configResult = await loadConfig(rootPath).catch(() => ({ config: { ignore: [] } }));
15
+ const scan = await scanRepository(rootPath, { ignore: configResult.config.ignore });
16
+ const issues = applyConfigToIssues(await collectIssuesWithPluginOption(rootPath, scan.files, options.enablePlugins), configResult.config);
17
+ const health = calculateScore(issues);
18
+ const changedFiles = await safeChangedFiles(rootPath, mode, options.baseRef);
19
+ const session = await safeSession(rootPath);
20
+ const hotspots = await safeHotspots(rootPath, scan.files, issues);
21
+ const review = await safeReview(rootPath, mode, options);
22
+ const reasons = buildPreflightReasons({
23
+ mode,
24
+ issues,
25
+ changedFiles,
26
+ health,
27
+ session,
28
+ hotspots,
29
+ review,
30
+ maxChangedFiles: options.maxChangedFiles ?? DEFAULT_MAX_CHANGED_FILES,
31
+ });
32
+ const verdict = decidePreflightVerdict(reasons);
33
+ const supplyChain = countSupplyChainIssues(issues);
34
+ const evidence = buildEvidence({
35
+ health,
36
+ changedFiles,
37
+ session,
38
+ hotspots,
39
+ issues,
40
+ pluginsEnabledForRun: options.enablePlugins === true || pluginsEnabled(),
41
+ review,
42
+ });
43
+ const truncated = evidence.session?.truncated === true ||
44
+ changedFiles.files.length > MAX_EVIDENCE_FILES ||
45
+ (evidence.hotspots?.touched.length ?? 0) > MAX_EVIDENCE_FILES;
46
+ const report = {
47
+ schemaVersion: 1,
48
+ mode,
49
+ verdict,
50
+ summary: '',
51
+ reasons,
52
+ evidence,
53
+ requiredChecks: buildRequiredChecks(mode, health, changedFiles, review, supplyChain),
54
+ suggestedNextActions: buildSuggestedActions(reasons, mode, changedFiles),
55
+ toolCalls: buildToolCalls(reasons, mode, changedFiles),
56
+ ...(truncated ? { truncated: true } : {}),
57
+ };
58
+ return { ...report, summary: summarizePreflight(report) };
59
+ }
60
+ function countSupplyChainIssues(issues) {
61
+ const supplyChainIssues = issues.filter((issue) => issue.category === 'supply-chain');
62
+ return {
63
+ errorIssues: supplyChainIssues.filter((issue) => issue.severity === 'error').length,
64
+ warningIssues: supplyChainIssues.filter((issue) => issue.severity === 'warning').length,
65
+ };
66
+ }
67
+ export function decidePreflightVerdict(reasons) {
68
+ if (reasons.some((reason) => reason.severity === 'error'))
69
+ return 'block';
70
+ if (reasons.some((reason) => reason.severity === 'warning'))
71
+ return 'caution';
72
+ return 'proceed';
73
+ }
74
+ export function summarizePreflight(report) {
75
+ if (report.reasons.length === 0) {
76
+ return `${report.verdict}: no blocking or cautionary signals found`;
77
+ }
78
+ return `${report.verdict}: ${report.reasons[0].message}`;
79
+ }
80
+ async function collectIssuesWithPluginOption(rootPath, files, enablePlugins) {
81
+ if (enablePlugins !== true)
82
+ return collectIssues(rootPath, files);
83
+ const previous = process.env[PLUGIN_PREVIEW_FLAG];
84
+ process.env[PLUGIN_PREVIEW_FLAG] = '1';
85
+ try {
86
+ return await collectIssues(rootPath, files);
87
+ }
88
+ finally {
89
+ if (previous === undefined)
90
+ delete process.env[PLUGIN_PREVIEW_FLAG];
91
+ else
92
+ process.env[PLUGIN_PREVIEW_FLAG] = previous;
93
+ }
94
+ }
95
+ async function safeChangedFiles(rootPath, mode, baseRef) {
96
+ if (mode === 'before_edit') {
97
+ return {
98
+ available: false,
99
+ count: 0,
100
+ files: [],
101
+ baseRef: null,
102
+ reason: 'changed-file detection is not required before edits',
103
+ };
104
+ }
105
+ try {
106
+ const result = await getChangedFiles(rootPath, baseRef);
107
+ return {
108
+ available: result.available,
109
+ count: result.files.length,
110
+ files: result.files,
111
+ baseRef: result.baseRef,
112
+ ...(result.reason ? { reason: result.reason } : {}),
113
+ };
114
+ }
115
+ catch (err) {
116
+ return {
117
+ available: false,
118
+ count: 0,
119
+ files: [],
120
+ baseRef: null,
121
+ reason: err instanceof Error ? err.message : String(err),
122
+ };
123
+ }
124
+ }
125
+ async function safeSession(rootPath) {
126
+ const { session } = await loadSession(rootPath);
127
+ const touchedFiles = Object.values(session.touchedFiles)
128
+ .sort((a, b) => {
129
+ const byTime = Date.parse(b.lastTouchedAt) - Date.parse(a.lastTouchedAt);
130
+ return byTime !== 0 ? byTime : a.file.localeCompare(b.file);
131
+ })
132
+ .map((touch) => touch.file);
133
+ return {
134
+ id: session.id,
135
+ touchedFiles,
136
+ eventCount: session.events.length,
137
+ };
138
+ }
139
+ async function safeHotspots(rootPath, files, issues) {
140
+ try {
141
+ return await analyzeHotspots(rootPath, files, issues, { limit: 20 });
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ }
147
+ async function safeReview(rootPath, mode, options) {
148
+ if (mode === 'before_edit') {
149
+ return { available: false, reason: 'review is not required before edits' };
150
+ }
151
+ try {
152
+ const report = await computeReview(rootPath, {
153
+ base: options.baseRef,
154
+ head: options.headRef,
155
+ });
156
+ return {
157
+ available: report.available,
158
+ verdict: report.available ? report.verdict : undefined,
159
+ summary: report.available ? report.summary.join('; ') : undefined,
160
+ reason: report.available ? undefined : report.reason,
161
+ newTaintFlows: report.available ? report.newTaintFlows.length : undefined,
162
+ };
163
+ }
164
+ catch (err) {
165
+ return {
166
+ available: false,
167
+ reason: err instanceof Error ? err.message : String(err),
168
+ };
169
+ }
170
+ }
171
+ function buildPreflightReasons(input) {
172
+ const reasons = [];
173
+ const changedSet = new Set(input.changedFiles.files);
174
+ const changedOnly = input.mode !== 'before_edit' && input.changedFiles.available;
175
+ for (const issue of input.issues) {
176
+ if (issue.category !== 'supply-chain')
177
+ continue;
178
+ reasons.push({
179
+ severity: issue.severity,
180
+ source: 'supply-chain',
181
+ issueId: issue.id,
182
+ file: firstIssueFile(issue),
183
+ message: `${issue.severity === 'error' ? 'Supply-chain gate blocks' : 'Supply-chain gate flags'} ${issue.id}: ${issue.title}`,
184
+ tool: 'projscan_doctor',
185
+ });
186
+ }
187
+ for (const issue of input.issues) {
188
+ if (!issue.id.startsWith('plugin:'))
189
+ continue;
190
+ reasons.push({
191
+ severity: issue.severity,
192
+ source: 'plugin',
193
+ issueId: issue.id,
194
+ file: firstIssueFile(issue),
195
+ message: `${issue.severity === 'error' ? 'Plugin policy blocks' : 'Plugin policy flags'} ${issue.id}: ${issue.title}`,
196
+ tool: 'projscan_plugin',
197
+ });
198
+ }
199
+ if (changedOnly) {
200
+ const changedIssues = input.issues.filter((issue) => issueTouchesChangedFile(issue, changedSet));
201
+ const error = changedIssues.find((issue) => issue.severity === 'error');
202
+ const warning = changedIssues.find((issue) => issue.severity === 'warning');
203
+ if (error) {
204
+ reasons.push({
205
+ severity: 'error',
206
+ source: 'doctor',
207
+ issueId: error.id,
208
+ file: firstIssueFile(error),
209
+ message: `Health error on changed file: ${error.title}`,
210
+ tool: 'projscan_doctor',
211
+ });
212
+ }
213
+ else if (warning) {
214
+ reasons.push({
215
+ severity: 'warning',
216
+ source: 'doctor',
217
+ issueId: warning.id,
218
+ file: firstIssueFile(warning),
219
+ message: `Health warning on changed file: ${warning.title}`,
220
+ tool: 'projscan_doctor',
221
+ });
222
+ }
223
+ }
224
+ else if (input.mode !== 'before_edit' && !input.changedFiles.available) {
225
+ reasons.push({
226
+ severity: 'warning',
227
+ source: 'changed-files',
228
+ message: `Changed files unavailable: ${input.changedFiles.reason ?? 'unknown reason'}`,
229
+ tool: 'projscan_review',
230
+ });
231
+ }
232
+ if (input.mode !== 'before_edit' &&
233
+ input.changedFiles.available &&
234
+ input.changedFiles.count > input.maxChangedFiles) {
235
+ reasons.push({
236
+ severity: 'warning',
237
+ source: 'changed-files',
238
+ message: `${input.changedFiles.count} changed files exceeds the preflight threshold of ${input.maxChangedFiles}`,
239
+ tool: 'projscan_review',
240
+ });
241
+ }
242
+ if (input.review.available) {
243
+ if ((input.review.newTaintFlows ?? 0) > 0) {
244
+ reasons.push({
245
+ severity: 'error',
246
+ source: 'taint',
247
+ message: `${input.review.newTaintFlows} new taint flow(s) found in review`,
248
+ tool: 'projscan_review',
249
+ });
250
+ }
251
+ if (input.review.verdict === 'block') {
252
+ reasons.push({
253
+ severity: 'error',
254
+ source: 'review',
255
+ message: 'Review verdict is block',
256
+ tool: 'projscan_review',
257
+ });
258
+ }
259
+ else if (input.review.verdict === 'review') {
260
+ reasons.push({
261
+ severity: 'warning',
262
+ source: 'review',
263
+ message: 'Review verdict requires careful review',
264
+ tool: 'projscan_review',
265
+ });
266
+ }
267
+ }
268
+ else if (input.mode !== 'before_edit') {
269
+ reasons.push({
270
+ severity: 'warning',
271
+ source: 'review',
272
+ message: `Review unavailable: ${input.review.reason ?? 'unknown reason'}`,
273
+ tool: 'projscan_review',
274
+ });
275
+ }
276
+ const touched = new Set(input.session.touchedFiles);
277
+ const hotspotTouches = input.hotspots?.available === true
278
+ ? input.hotspots.hotspots.filter((hotspot) => touched.has(hotspot.relativePath) && hotspot.riskScore >= 40)
279
+ : [];
280
+ for (const hotspot of hotspotTouches.slice(0, 3)) {
281
+ reasons.push({
282
+ severity: 'warning',
283
+ source: 'hotspots',
284
+ file: hotspot.relativePath,
285
+ message: `Touched file overlaps high-risk hotspot ${hotspot.relativePath} (risk ${hotspot.riskScore})`,
286
+ tool: 'projscan_hotspots',
287
+ });
288
+ }
289
+ if (input.health.errors > 0 && input.mode === 'before_merge' && !changedOnly) {
290
+ reasons.push({
291
+ severity: 'warning',
292
+ source: 'doctor',
293
+ message: `${input.health.errors} project health error(s) exist; changed-file scoping was unavailable`,
294
+ tool: 'projscan_doctor',
295
+ });
296
+ }
297
+ return reasons;
298
+ }
299
+ function buildEvidence(input) {
300
+ const pluginIssues = input.issues.filter((issue) => issue.id.startsWith('plugin:'));
301
+ const supplyChainIssues = input.issues.filter((issue) => issue.category === 'supply-chain');
302
+ const touched = input.hotspots?.available === true
303
+ ? input.hotspots.hotspots
304
+ .filter((hotspot) => input.session.touchedFiles.includes(hotspot.relativePath))
305
+ .slice(0, MAX_EVIDENCE_FILES)
306
+ .map((hotspot) => ({ file: hotspot.relativePath, riskScore: hotspot.riskScore }))
307
+ : [];
308
+ const sessionTouchedFiles = input.session.touchedFiles.slice(0, MAX_EVIDENCE_FILES);
309
+ const changedEvidenceFiles = input.changedFiles.files.slice(0, MAX_EVIDENCE_FILES);
310
+ return {
311
+ health: {
312
+ score: input.health.score,
313
+ grade: input.health.grade,
314
+ errors: input.health.errors,
315
+ warnings: input.health.warnings,
316
+ infos: input.health.infos,
317
+ },
318
+ changedFiles: {
319
+ available: input.changedFiles.available,
320
+ count: input.changedFiles.count,
321
+ files: changedEvidenceFiles,
322
+ ...(input.changedFiles.reason ? { reason: input.changedFiles.reason } : {}),
323
+ },
324
+ review: {
325
+ available: input.review.available,
326
+ ...(input.review.verdict ? { verdict: input.review.verdict } : {}),
327
+ ...(input.review.summary ? { summary: input.review.summary } : {}),
328
+ ...(input.review.reason ? { reason: input.review.reason } : {}),
329
+ },
330
+ session: {
331
+ id: input.session.id,
332
+ touchedFiles: sessionTouchedFiles,
333
+ totalTouchedFiles: input.session.touchedFiles.length,
334
+ eventCount: input.session.eventCount,
335
+ ...(input.session.touchedFiles.length > MAX_EVIDENCE_FILES ? { truncated: true } : {}),
336
+ },
337
+ hotspots: { touched },
338
+ plugins: {
339
+ enabled: input.pluginsEnabledForRun,
340
+ errorIssues: pluginIssues.filter((issue) => issue.severity === 'error').length,
341
+ warningIssues: pluginIssues.filter((issue) => issue.severity === 'warning').length,
342
+ },
343
+ supplyChain: {
344
+ errorIssues: supplyChainIssues.filter((issue) => issue.severity === 'error').length,
345
+ warningIssues: supplyChainIssues.filter((issue) => issue.severity === 'warning').length,
346
+ },
347
+ };
348
+ }
349
+ function buildRequiredChecks(mode, health, changedFiles, review, supplyChain) {
350
+ const checks = [
351
+ {
352
+ name: 'health',
353
+ status: health.errors > 0 ? 'fail' : health.warnings > 0 ? 'warn' : 'pass',
354
+ reason: `${health.errors} error(s), ${health.warnings} warning(s), ${health.infos} info`,
355
+ },
356
+ ];
357
+ checks.push({
358
+ name: 'supply-chain',
359
+ status: (supplyChain?.errorIssues ?? 0) > 0
360
+ ? 'fail'
361
+ : (supplyChain?.warningIssues ?? 0) > 0
362
+ ? 'warn'
363
+ : 'pass',
364
+ reason: `${supplyChain?.errorIssues ?? 0} error(s), ${supplyChain?.warningIssues ?? 0} warning(s)`,
365
+ });
366
+ checks.push({
367
+ name: 'changed-files',
368
+ status: changedFiles.available ? 'pass' : 'unavailable',
369
+ reason: changedFiles.available
370
+ ? `${changedFiles.count} changed file(s)`
371
+ : changedFiles.reason ?? 'changed-file detection unavailable',
372
+ });
373
+ checks.push({
374
+ name: 'review',
375
+ status: mode === 'before_edit'
376
+ ? 'unavailable'
377
+ : !review.available
378
+ ? 'unavailable'
379
+ : review.verdict === 'block'
380
+ ? 'fail'
381
+ : review.verdict === 'review'
382
+ ? 'warn'
383
+ : 'pass',
384
+ reason: mode === 'before_edit'
385
+ ? 'review is not required before edits'
386
+ : review.available
387
+ ? review.summary ?? review.verdict
388
+ : review.reason ?? 'review unavailable',
389
+ });
390
+ return checks;
391
+ }
392
+ function buildSuggestedActions(reasons, mode, changedFiles) {
393
+ if (reasons.length === 0)
394
+ return [];
395
+ const actions = [];
396
+ if (reasons.some((reason) => reason.source === 'review' || reason.source === 'taint')) {
397
+ actions.push({
398
+ label: 'Inspect the full review before continuing',
399
+ command: 'projscan review --format json',
400
+ tool: 'projscan_review',
401
+ });
402
+ }
403
+ if (reasons.some((reason) => reason.source === 'doctor' || reason.source === 'plugin' || reason.source === 'supply-chain')) {
404
+ actions.push({
405
+ label: 'Inspect health, plugin policy, and supply-chain findings',
406
+ command: 'projscan doctor --format json',
407
+ tool: 'projscan_doctor',
408
+ });
409
+ }
410
+ if (reasons.some((reason) => reason.source === 'hotspots')) {
411
+ actions.push({
412
+ label: 'Inspect touched hotspots',
413
+ command: 'projscan hotspots --format json',
414
+ tool: 'projscan_hotspots',
415
+ });
416
+ }
417
+ if (mode !== 'before_edit' && !changedFiles.available) {
418
+ actions.push({
419
+ label: 'Run preflight with an explicit base ref',
420
+ command: 'projscan preflight --base-ref main --format json',
421
+ });
422
+ }
423
+ return dedupeActions(actions);
424
+ }
425
+ function buildToolCalls(reasons, mode, changedFiles) {
426
+ return buildSuggestedActions(reasons, mode, changedFiles).map((action) => ({
427
+ label: action.label,
428
+ ...(action.tool ? { tool: action.tool } : {}),
429
+ ...(action.args ? { args: action.args } : {}),
430
+ }));
431
+ }
432
+ function dedupeActions(actions) {
433
+ const seen = new Set();
434
+ const out = [];
435
+ for (const action of actions) {
436
+ const key = `${action.label}:${action.command ?? action.tool ?? ''}`;
437
+ if (seen.has(key))
438
+ continue;
439
+ seen.add(key);
440
+ out.push(action);
441
+ }
442
+ return out;
443
+ }
444
+ function issueTouchesChangedFile(issue, changedFiles) {
445
+ return (issue.locations ?? []).some((location) => location.file && changedFiles.has(location.file));
446
+ }
447
+ function firstIssueFile(issue) {
448
+ return issue.locations?.find((location) => location.file)?.file;
449
+ }
450
+ //# sourceMappingURL=preflight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preflight.js","sourceRoot":"","sources":["../../src/core/preflight.ts"],"names":[],"mappings":"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"}
@@ -40,6 +40,32 @@ export async function computeReview(rootPath, options = {}) {
40
40
  if (!baseSha) {
41
41
  return unavailable(`Could not resolve base ref "${baseRef}".`, options, baseRef, headRef, headSha);
42
42
  }
43
+ if (headSha && headSha === baseSha) {
44
+ const report = {
45
+ available: true,
46
+ base: { ref: baseRef, resolvedSha: baseSha },
47
+ head: { ref: headRef, resolvedSha: headSha },
48
+ prDiff: {
49
+ available: true,
50
+ base: { ref: baseRef, resolvedSha: baseSha },
51
+ head: { ref: headRef, resolvedSha: headSha },
52
+ filesAdded: [],
53
+ filesRemoved: [],
54
+ filesModified: [],
55
+ totalFilesChanged: 0,
56
+ },
57
+ changedFiles: [],
58
+ newCycles: [],
59
+ riskyFunctions: [],
60
+ dependencyChanges: [],
61
+ contractChanges: [],
62
+ newTaintFlows: [],
63
+ verdict: 'ok',
64
+ summary: ['No structural changes detected between base and head.'],
65
+ };
66
+ applyIntent(report, options.intent);
67
+ return report;
68
+ }
43
69
  // Head-side data: scan + graph + issues + hotspots.
44
70
  const headScan = await scanRepository(rootPath);
45
71
  const headGraph = await buildCodeGraph(rootPath, headScan.files);
@@ -126,6 +152,7 @@ export async function computeReview(rootPath, options = {}) {
126
152
  const riskyFunctions = findRiskyFunctions(baseGraph, headGraph, prDiff);
127
153
  // Dependency changes across root + workspaces.
128
154
  const dependencyChanges = diffManifests(basePackageManifests, headPackageManifests);
155
+ const contractChanges = buildContractChanges(prDiff, baseGraph, headGraph, basePackageManifests, headPackageManifests);
129
156
  // 1.6+ — taint flows newly introduced at head. A flow is "new" iff
130
157
  // (a) the (sourceFn, sinkFn) pair didn't exist at base, AND
131
158
  // (b) at least one file along the flow's path is in the PR diff.
@@ -149,6 +176,7 @@ export async function computeReview(rootPath, options = {}) {
149
176
  newCycles,
150
177
  riskyFunctions,
151
178
  dependencyChanges,
179
+ contractChanges,
152
180
  newTaintFlows,
153
181
  verdict,
154
182
  summary,
@@ -157,7 +185,11 @@ export async function computeReview(rootPath, options = {}) {
157
185
  // annotate each finding with an alignment label, and append a
158
186
  // small intent summary to the verdict bullets. Does NOT change the
159
187
  // verdict — verdict stays structural.
160
- const intent = parseIntent(options.intent);
188
+ applyIntent(report, options.intent);
189
+ return report;
190
+ }
191
+ function applyIntent(report, rawIntent) {
192
+ const intent = parseIntent(rawIntent);
161
193
  if (intent) {
162
194
  const analysis = annotateReviewWithIntent(report, intent);
163
195
  report.intent = {
@@ -171,7 +203,6 @@ export async function computeReview(rootPath, options = {}) {
171
203
  };
172
204
  appendIntentToSummary(report.summary, analysis);
173
205
  }
174
- return report;
175
206
  }
176
207
  async function computeNewTaintFlows(rootPath, baseGraph, headGraph, touchedFiles) {
177
208
  const { config } = await loadConfig(rootPath);
@@ -399,8 +430,22 @@ async function readOneManifest(dir, manifestFile, workspaceName) {
399
430
  manifestFile,
400
431
  dependencies: parsed.dependencies ?? {},
401
432
  devDependencies: parsed.devDependencies ?? {},
433
+ entrypoints: readEntrypoints(parsed),
402
434
  };
403
435
  }
436
+ function readEntrypoints(parsed) {
437
+ const out = {};
438
+ for (const field of ['main', 'module', 'types', 'exports', 'bin']) {
439
+ const value = parsed[field];
440
+ if (value === undefined)
441
+ continue;
442
+ out[field] = entrypointValue(value);
443
+ }
444
+ return out;
445
+ }
446
+ function entrypointValue(value) {
447
+ return typeof value === 'string' ? value : JSON.stringify(value);
448
+ }
404
449
  function diffManifests(base, head) {
405
450
  const out = [];
406
451
  const allManifests = new Set([...base.keys(), ...head.keys()]);
@@ -417,6 +462,81 @@ function diffManifests(base, head) {
417
462
  out.sort((a, b) => a.manifestFile.localeCompare(b.manifestFile));
418
463
  return out;
419
464
  }
465
+ function buildContractChanges(prDiff, baseGraph, headGraph, baseManifests, headManifests) {
466
+ const changes = [];
467
+ for (const file of prDiff.filesAdded) {
468
+ const entry = headGraph.files.get(file);
469
+ for (const exp of entry?.exports ?? []) {
470
+ changes.push(exportContractChange('export-added', file, exp.name));
471
+ }
472
+ }
473
+ for (const file of prDiff.filesRemoved) {
474
+ const entry = baseGraph.files.get(file);
475
+ for (const exp of entry?.exports ?? []) {
476
+ changes.push(exportContractChange('export-removed', file, exp.name));
477
+ }
478
+ }
479
+ for (const file of prDiff.filesModified) {
480
+ for (const symbol of file.exportsAdded) {
481
+ changes.push(exportContractChange('export-added', file.relativePath, symbol));
482
+ }
483
+ for (const symbol of file.exportsRemoved) {
484
+ changes.push(exportContractChange('export-removed', file.relativePath, symbol));
485
+ }
486
+ for (const rename of file.exportsRenamed) {
487
+ changes.push({
488
+ kind: 'export-renamed',
489
+ file: file.relativePath,
490
+ symbol: rename.to,
491
+ before: rename.from,
492
+ after: rename.to,
493
+ confidence: 'high',
494
+ why: `Export "${rename.from}" was renamed to "${rename.to}" in ${file.relativePath}; downstream imports of the old name can fail at compile time or runtime.`,
495
+ });
496
+ }
497
+ }
498
+ changes.push(...entrypointContractChanges(baseManifests, headManifests));
499
+ return changes;
500
+ }
501
+ function exportContractChange(kind, file, symbol) {
502
+ return {
503
+ kind,
504
+ file,
505
+ symbol,
506
+ confidence: 'high',
507
+ why: kind === 'export-added'
508
+ ? `Export "${symbol}" was added in ${file}; downstream code may start depending on a new public API.`
509
+ : `Export "${symbol}" was removed from ${file}; downstream imports can fail at compile time or runtime.`,
510
+ };
511
+ }
512
+ function entrypointContractChanges(base, head) {
513
+ const out = [];
514
+ const allManifests = new Set([...base.keys(), ...head.keys()]);
515
+ for (const manifestFile of allManifests) {
516
+ const baseEntrypoints = base.get(manifestFile)?.entrypoints ?? {};
517
+ const headEntrypoints = head.get(manifestFile)?.entrypoints ?? {};
518
+ const fields = new Set([...Object.keys(baseEntrypoints), ...Object.keys(headEntrypoints)]);
519
+ for (const field of fields) {
520
+ const before = baseEntrypoints[field];
521
+ const after = headEntrypoints[field];
522
+ if (before === after)
523
+ continue;
524
+ const kind = field === 'exports' ? 'public-export-changed' : 'entrypoint-changed';
525
+ out.push({
526
+ kind,
527
+ file: manifestFile,
528
+ symbol: field,
529
+ ...(before !== undefined ? { before } : {}),
530
+ ...(after !== undefined ? { after } : {}),
531
+ confidence: 'high',
532
+ why: kind === 'public-export-changed'
533
+ ? `${manifestFile} package "exports" changed; consumers may resolve different public modules.`
534
+ : `${manifestFile} package "${field}" changed from ${before ?? '<unset>'} to ${after ?? '<unset>'}; package consumers may load a different entrypoint.`,
535
+ });
536
+ }
537
+ }
538
+ return out.sort((a, b) => `${a.file}:${a.symbol ?? ''}`.localeCompare(`${b.file}:${b.symbol ?? ''}`));
539
+ }
420
540
  function diffOneManifest(base, head, manifestFile) {
421
541
  const workspace = head?.workspace ?? base?.workspace ?? '';
422
542
  const baseDeps = base?.dependencies ?? {};
@@ -645,7 +765,15 @@ export function shapeReviewForTier(report, tier) {
645
765
  const riskyFunctionsAdded = report.riskyFunctions.length;
646
766
  const depsChanged = report.dependencyChanges.length;
647
767
  const taintFlowsAdded = report.newTaintFlows?.length ?? 0;
648
- const totals = { filesChanged, cyclesAdded, riskyFunctionsAdded, depsChanged, taintFlowsAdded };
768
+ const contractChanges = report.contractChanges?.length ?? 0;
769
+ const totals = {
770
+ filesChanged,
771
+ cyclesAdded,
772
+ riskyFunctionsAdded,
773
+ depsChanged,
774
+ taintFlowsAdded,
775
+ contractChanges,
776
+ };
649
777
  if (tier === 'verdict-only') {
650
778
  return {
651
779
  available: report.available,
@@ -683,6 +811,7 @@ export function shapeReviewForTier(report, tier) {
683
811
  newCycles: report.newCycles.slice(0, 3),
684
812
  riskyFunctions: report.riskyFunctions.slice(0, 3),
685
813
  dependencyChanges: report.dependencyChanges.slice(0, 3),
814
+ contractChanges: report.contractChanges?.slice(0, TOP) ?? [],
686
815
  newTaintFlows: report.newTaintFlows?.slice(0, 5) ?? [],
687
816
  verdict: report.verdict,
688
817
  summary: report.summary,