projscan 4.10.0 → 4.11.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.
- package/README.md +27 -8
- package/dist/cli/commands/assess.d.ts +3 -0
- package/dist/cli/commands/assess.js +143 -0
- package/dist/cli/commands/assess.js.map +1 -0
- package/dist/cli/commands/simulate.d.ts +3 -0
- package/dist/cli/commands/simulate.js +125 -0
- package/dist/cli/commands/simulate.js.map +1 -0
- package/dist/cli/registerCommands.js +4 -0
- package/dist/cli/registerCommands.js.map +1 -1
- package/dist/core/assess.d.ts +9 -0
- package/dist/core/assess.js +119 -0
- package/dist/core/assess.js.map +1 -0
- package/dist/core/proofCards.d.ts +10 -0
- package/dist/core/proofCards.js +222 -0
- package/dist/core/proofCards.js.map +1 -0
- package/dist/core/riskDelta.d.ts +19 -0
- package/dist/core/riskDelta.js +77 -0
- package/dist/core/riskDelta.js.map +1 -0
- package/dist/core/simulate.d.ts +6 -0
- package/dist/core/simulate.js +298 -0
- package/dist/core/simulate.js.map +1 -0
- package/dist/mcp/toolCatalog.js +4 -0
- package/dist/mcp/toolCatalog.js.map +1 -1
- package/dist/mcp/tools/assess.d.ts +2 -0
- package/dist/mcp/tools/assess.js +42 -0
- package/dist/mcp/tools/assess.js.map +1 -0
- package/dist/mcp/tools/simulate.d.ts +2 -0
- package/dist/mcp/tools/simulate.js +32 -0
- package/dist/mcp/tools/simulate.js.map +1 -0
- package/dist/projscan-sbom.cdx.json +6 -6
- package/dist/publicCore.d.ts +2 -0
- package/dist/publicCore.js +2 -0
- package/dist/publicCore.js.map +1 -1
- package/dist/tool-manifest.json +52 -3
- package/dist/types/assess.d.ts +91 -0
- package/dist/types/assess.js +2 -0
- package/dist/types/assess.js.map +1 -0
- package/dist/types/simulate.d.ts +41 -0
- package/dist/types/simulate.js +2 -0
- package/dist/types/simulate.js.map +1 -0
- package/dist/types/workplan.d.ts +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/utils/formatSupport.d.ts +2 -0
- package/dist/utils/formatSupport.js +2 -0
- package/dist/utils/formatSupport.js.map +1 -1
- package/docs/GUIDE.md +19 -1
- package/package.json +1 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { computeRiskDelta } from './riskDelta.js';
|
|
2
|
+
const DEFAULT_MAX_CARDS = 5;
|
|
3
|
+
export function buildProofCards(input) {
|
|
4
|
+
const maxCards = normalizeMaxCards(input.maxCards);
|
|
5
|
+
const cards = [
|
|
6
|
+
...input.bugHuntFindings.map(cardFromBugHuntFinding),
|
|
7
|
+
...input.qualityRisks.map(cardFromQualityRisk),
|
|
8
|
+
];
|
|
9
|
+
const deduped = dedupeCards(cards);
|
|
10
|
+
const ranked = rankCards(deduped).slice(0, maxCards);
|
|
11
|
+
return ranked.map((card) => ({
|
|
12
|
+
...card,
|
|
13
|
+
riskDelta: input.riskDelta ??
|
|
14
|
+
computeRiskDelta({
|
|
15
|
+
healthScore: 100,
|
|
16
|
+
qualityVerdict: 'needs_attention',
|
|
17
|
+
preflightVerdict: 'caution',
|
|
18
|
+
proofCards: ranked.map((entry) => ({
|
|
19
|
+
id: entry.id,
|
|
20
|
+
priority: entry.priority,
|
|
21
|
+
source: entry.source,
|
|
22
|
+
})),
|
|
23
|
+
selectedCardIds: [card.id],
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
function cardFromBugHuntFinding(finding) {
|
|
28
|
+
const id = `proof-${finding.id}`;
|
|
29
|
+
return baseCard({
|
|
30
|
+
id,
|
|
31
|
+
priority: finding.priority,
|
|
32
|
+
source: finding.source,
|
|
33
|
+
finding: finding.title,
|
|
34
|
+
whyItMatters: finding.why,
|
|
35
|
+
files: finding.files,
|
|
36
|
+
evidence: finding.evidence.map((entry) => ({
|
|
37
|
+
source: String(entry.source),
|
|
38
|
+
detail: entry.message,
|
|
39
|
+
...(entry.file ? { file: entry.file } : {}),
|
|
40
|
+
...(entry.tool ? { command: entry.tool } : {}),
|
|
41
|
+
})),
|
|
42
|
+
commands: [...new Set([...finding.verification.commands, 'projscan bug-hunt --format json'])],
|
|
43
|
+
affectedAreas: affectedAreasForSource(finding.source),
|
|
44
|
+
fixSummary: fixSummaryForSource(finding.source, finding.title),
|
|
45
|
+
safeChangeShape: safeChangeShapeForSource(finding.source),
|
|
46
|
+
expected: finding.verification.expected,
|
|
47
|
+
confidence: finding.source === 'doctor' ? 'high' : 'medium',
|
|
48
|
+
suppressionHints: suppressionHintsForFinding(finding),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function cardFromQualityRisk(risk) {
|
|
52
|
+
const id = `proof-${risk.id}`;
|
|
53
|
+
return baseCard({
|
|
54
|
+
id,
|
|
55
|
+
priority: risk.priority,
|
|
56
|
+
source: risk.source,
|
|
57
|
+
finding: risk.title,
|
|
58
|
+
whyItMatters: whyQualityRiskMatters(risk),
|
|
59
|
+
files: risk.files,
|
|
60
|
+
evidence: [
|
|
61
|
+
{
|
|
62
|
+
source: 'quality-scorecard',
|
|
63
|
+
detail: risk.title,
|
|
64
|
+
...(risk.files[0] ? { file: risk.files[0] } : {}),
|
|
65
|
+
command: risk.command,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
commands: [
|
|
69
|
+
...new Set([
|
|
70
|
+
risk.command,
|
|
71
|
+
...(risk.source === 'hotspot' ? [simulateCommandForRisk(risk)] : []),
|
|
72
|
+
'projscan quality-scorecard --format json',
|
|
73
|
+
]),
|
|
74
|
+
],
|
|
75
|
+
affectedAreas: affectedAreasForSource(risk.source),
|
|
76
|
+
fixSummary: fixSummaryForSource(risk.source, risk.title),
|
|
77
|
+
safeChangeShape: safeChangeShapeForSource(risk.source),
|
|
78
|
+
expected: 'The proof card remains explainable, the relevant risk drops, and tests pass.',
|
|
79
|
+
confidence: risk.source === 'issue' ? 'high' : 'medium',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function baseCard(input) {
|
|
83
|
+
const feedbackCommand = feedbackCommandFor(input.id);
|
|
84
|
+
return {
|
|
85
|
+
id: input.id,
|
|
86
|
+
priority: input.priority,
|
|
87
|
+
source: input.source,
|
|
88
|
+
finding: input.finding,
|
|
89
|
+
whyItMatters: input.whyItMatters,
|
|
90
|
+
files: input.files,
|
|
91
|
+
evidence: input.evidence,
|
|
92
|
+
impact: {
|
|
93
|
+
commands: input.commands,
|
|
94
|
+
affectedAreas: input.affectedAreas,
|
|
95
|
+
likelyFiles: input.files,
|
|
96
|
+
},
|
|
97
|
+
recommendedFix: {
|
|
98
|
+
summary: input.fixSummary,
|
|
99
|
+
safeChangeShape: input.safeChangeShape,
|
|
100
|
+
},
|
|
101
|
+
verification: {
|
|
102
|
+
commands: input.commands,
|
|
103
|
+
expected: input.expected,
|
|
104
|
+
},
|
|
105
|
+
confidence: input.confidence,
|
|
106
|
+
suppression: { command: feedbackCommand, ...input.suppressionHints },
|
|
107
|
+
feedback: { command: feedbackCommand },
|
|
108
|
+
riskDelta: { baselineScore: 0, projectedScore: 0, delta: 0, basis: [] },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function suppressionHintsForFinding(finding) {
|
|
112
|
+
const evidence = finding.evidence.find((entry) => entry.issueId || entry.file);
|
|
113
|
+
const ruleId = ruleIdFromEvidence(evidence);
|
|
114
|
+
const file = evidence?.file ?? finding.files[0];
|
|
115
|
+
if (!ruleId || !file)
|
|
116
|
+
return undefined;
|
|
117
|
+
return {
|
|
118
|
+
...(evidence && 'line' in evidence && typeof evidence.line === 'number'
|
|
119
|
+
? { inlineHint: `// projscan-ignore-line ${ruleId} -- reason` }
|
|
120
|
+
: {}),
|
|
121
|
+
configHint: `"suppress": { "${ruleId}": ["${file}"] }`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function ruleIdFromEvidence(evidence) {
|
|
125
|
+
const issueId = evidence?.issueId;
|
|
126
|
+
if (!issueId)
|
|
127
|
+
return undefined;
|
|
128
|
+
if (issueId === 'hardcoded-secret' || issueId.startsWith('hardcoded-secret-'))
|
|
129
|
+
return 'hardcoded-secret';
|
|
130
|
+
return issueId;
|
|
131
|
+
}
|
|
132
|
+
function dedupeCards(cards) {
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
return cards.filter((card) => {
|
|
135
|
+
const key = `${card.files[0] ?? card.id}:${card.finding}`;
|
|
136
|
+
if (seen.has(key))
|
|
137
|
+
return false;
|
|
138
|
+
seen.add(key);
|
|
139
|
+
return true;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function rankCards(cards) {
|
|
143
|
+
return cards
|
|
144
|
+
.map((card, index) => ({ card, index }))
|
|
145
|
+
.sort((a, b) => priorityRank(a.card.priority) - priorityRank(b.card.priority) ||
|
|
146
|
+
sourceRank(a.card.source) - sourceRank(b.card.source) ||
|
|
147
|
+
a.index - b.index)
|
|
148
|
+
.map((entry) => entry.card);
|
|
149
|
+
}
|
|
150
|
+
function affectedAreasForSource(source) {
|
|
151
|
+
if (source === 'doctor')
|
|
152
|
+
return ['health'];
|
|
153
|
+
if (source === 'hotspot')
|
|
154
|
+
return ['maintainability'];
|
|
155
|
+
if (source === 'coordination' || source === 'session')
|
|
156
|
+
return ['coordination'];
|
|
157
|
+
if (source === 'preflight')
|
|
158
|
+
return ['ship-readiness'];
|
|
159
|
+
return ['quality'];
|
|
160
|
+
}
|
|
161
|
+
function whyQualityRiskMatters(risk) {
|
|
162
|
+
if (risk.source === 'hotspot') {
|
|
163
|
+
return 'This file concentrates churn, complexity, issue, or ownership risk and deserves a bounded review before broad changes.';
|
|
164
|
+
}
|
|
165
|
+
if (risk.source === 'coordination') {
|
|
166
|
+
return 'Coordination risk can cause agents or engineers to overwrite each other or validate the wrong state.';
|
|
167
|
+
}
|
|
168
|
+
return 'This issue-backed risk is visible in the project health signal and can affect confidence in daily gates.';
|
|
169
|
+
}
|
|
170
|
+
function fixSummaryForSource(source, title) {
|
|
171
|
+
if (source === 'hotspot')
|
|
172
|
+
return `Reduce the risk behind "${title}" with one focused extraction or test first.`;
|
|
173
|
+
if (source === 'coordination')
|
|
174
|
+
return 'Resolve the coordination conflict before changing shared files.';
|
|
175
|
+
if (source === 'session')
|
|
176
|
+
return 'Review touched files and clear active session conflicts before continuing.';
|
|
177
|
+
if (source === 'preflight')
|
|
178
|
+
return 'Close the preflight blocker or document the required manual sign-off.';
|
|
179
|
+
return `Address "${title}" with the smallest change that removes the finding.`;
|
|
180
|
+
}
|
|
181
|
+
function safeChangeShapeForSource(source) {
|
|
182
|
+
if (source === 'hotspot') {
|
|
183
|
+
return 'Inspect the file first, extract one pure helper or add one missing regression test, then rerun the listed proof commands.';
|
|
184
|
+
}
|
|
185
|
+
if (source === 'coordination' || source === 'session') {
|
|
186
|
+
return 'Synchronize ownership, re-run session or coordinate evidence, then continue with one owner for the file.';
|
|
187
|
+
}
|
|
188
|
+
if (source === 'preflight') {
|
|
189
|
+
return 'Fix concrete blockers first; for review-only scale, collect sign-off before merge or release.';
|
|
190
|
+
}
|
|
191
|
+
return 'Make one narrow code or config change, avoid unrelated cleanup, and rerun the listed proof commands.';
|
|
192
|
+
}
|
|
193
|
+
function feedbackCommandFor(id) {
|
|
194
|
+
return `projscan feedback intake --text "${id}: false positive because ..." --format json`;
|
|
195
|
+
}
|
|
196
|
+
function simulateCommandForRisk(risk) {
|
|
197
|
+
return `projscan simulate --plan "Reduce the risk behind ${risk.title.replace(/["\\]/g, '\\$&')}" --format json`;
|
|
198
|
+
}
|
|
199
|
+
function priorityRank(priority) {
|
|
200
|
+
if (priority === 'p0')
|
|
201
|
+
return 0;
|
|
202
|
+
if (priority === 'p1')
|
|
203
|
+
return 1;
|
|
204
|
+
return 2;
|
|
205
|
+
}
|
|
206
|
+
function sourceRank(source) {
|
|
207
|
+
if (source === 'doctor' || source === 'issue')
|
|
208
|
+
return 0;
|
|
209
|
+
if (source === 'preflight')
|
|
210
|
+
return 1;
|
|
211
|
+
if (source === 'hotspot')
|
|
212
|
+
return 2;
|
|
213
|
+
if (source === 'coordination' || source === 'session')
|
|
214
|
+
return 3;
|
|
215
|
+
return 4;
|
|
216
|
+
}
|
|
217
|
+
function normalizeMaxCards(value) {
|
|
218
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
219
|
+
return DEFAULT_MAX_CARDS;
|
|
220
|
+
return Math.max(1, Math.min(25, Math.floor(value)));
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=proofCards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proofCards.js","sourceRoot":"","sources":["../../src/core/proofCards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAkBlD,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAE5B,MAAM,UAAU,eAAe,CAAC,KAA2B;IACzD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG;QACZ,GAAG,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC,sBAAsB,CAAC;QACpD,GAAG,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC,mBAAmB,CAAC;KAC/C,CAAC;IACF,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACrD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC3B,GAAG,IAAI;QACP,SAAS,EACP,KAAK,CAAC,SAAS;YACf,gBAAgB,CAAC;gBACf,WAAW,EAAE,GAAG;gBAChB,cAAc,EAAE,iBAAiB;gBACjC,gBAAgB,EAAE,SAAS;gBAC3B,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;oBACjC,EAAE,EAAE,KAAK,CAAC,EAAE;oBACZ,QAAQ,EAAE,KAAK,CAAC,QAAQ;oBACxB,MAAM,EAAE,KAAK,CAAC,MAAM;iBACrB,CAAC,CAAC;gBACH,eAAe,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;aAC3B,CAAC;KACL,CAAC,CAAC,CAAC;AACN,CAAC;AAED,SAAS,sBAAsB,CAAC,OAAuB;IACrD,MAAM,EAAE,GAAG,SAAS,OAAO,CAAC,EAAE,EAAE,CAAC;IACjC,OAAO,QAAQ,CAAC;QACd,EAAE;QACF,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,OAAO,EAAE,OAAO,CAAC,KAAK;QACtB,YAAY,EAAE,OAAO,CAAC,GAAG;QACzB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACzC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;YAC5B,MAAM,EAAE,KAAK,CAAC,OAAO;YACrB,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3C,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC/C,CAAC,CAAC;QACH,QAAQ,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,EAAE,iCAAiC,CAAC,CAAC,CAAC;QAC7F,aAAa,EAAE,sBAAsB,CAAC,OAAO,CAAC,MAAM,CAAC;QACrD,UAAU,EAAE,mBAAmB,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC;QAC9D,eAAe,EAAE,wBAAwB,CAAC,OAAO,CAAC,MAAM,CAAC;QACzD,QAAQ,EAAE,OAAO,CAAC,YAAY,CAAC,QAAQ;QACvC,UAAU,EAAE,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ;QAC3D,gBAAgB,EAAE,0BAA0B,CAAC,OAAO,CAAC;KACtD,CAAC,CAAC;AACL,CAAC;AAED,SAAS,mBAAmB,CAAC,IAA0B;IACrD,MAAM,EAAE,GAAG,SAAS,IAAI,CAAC,EAAE,EAAE,CAAC;IAC9B,OAAO,QAAQ,CAAC;QACd,EAAE;QACF,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,KAAK;QACnB,YAAY,EAAE,qBAAqB,CAAC,IAAI,CAAC;QACzC,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,QAAQ,EAAE;YACR;gBACE,MAAM,EAAE,mBAAmB;gBAC3B,MAAM,EAAE,IAAI,CAAC,KAAK;gBAClB,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjD,OAAO,EAAE,IAAI,CAAC,OAAO;aACtB;SACF;QACD,QAAQ,EAAE;YACR,GAAG,IAAI,GAAG,CAAC;gBACT,IAAI,CAAC,OAAO;gBACZ,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACpE,0CAA0C;aAC3C,CAAC;SACH;QACD,aAAa,EAAE,sBAAsB,CAAC,IAAI,CAAC,MAAM,CAAC;QAClD,UAAU,EAAE,mBAAmB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC;QACxD,eAAe,EAAE,wBAAwB,CAAC,IAAI,CAAC,MAAM,CAAC;QACtD,QAAQ,EAAE,8EAA8E;QACxF,UAAU,EAAE,IAAI,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ;KACxD,CAAC,CAAC;AACL,CAAC;AAED,SAAS,QAAQ,CAAC,KAkBjB;IACC,MAAM,eAAe,GAAG,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACrD,OAAO;QACL,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,MAAM,EAAE;YACN,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,aAAa,EAAE,KAAK,CAAC,aAAa;YAClC,WAAW,EAAE,KAAK,CAAC,KAAK;SACzB;QACD,cAAc,EAAE;YACd,OAAO,EAAE,KAAK,CAAC,UAAU;YACzB,eAAe,EAAE,KAAK,CAAC,eAAe;SACvC;QACD,YAAY,EAAE;YACZ,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;SACzB;QACD,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,WAAW,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,GAAG,KAAK,CAAC,gBAAgB,EAAE;QACpE,QAAQ,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE;QACtC,SAAS,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE;KACxE,CAAC;AACJ,CAAC;AAED,SAAS,0BAA0B,CACjC,OAAuB;IAEvB,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/E,MAAM,MAAM,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC5C,MAAM,IAAI,GAAG,QAAQ,EAAE,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAChD,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IACvC,OAAO;QACL,GAAG,CAAC,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ;YACrE,CAAC,CAAC,EAAE,UAAU,EAAE,2BAA2B,MAAM,YAAY,EAAE;YAC/D,CAAC,CAAC,EAAE,CAAC;QACP,UAAU,EAAE,kBAAkB,MAAM,QAAQ,IAAI,MAAM;KACvD,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB,CAAC,QAAsC;IAChE,MAAM,OAAO,GAAG,QAAQ,EAAE,OAAO,CAAC;IAClC,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAC/B,IAAI,OAAO,KAAK,kBAAkB,IAAI,OAAO,CAAC,UAAU,CAAC,mBAAmB,CAAC;QAC3E,OAAO,kBAAkB,CAAC;IAC5B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,WAAW,CAAC,KAAwB;IAC3C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QAC3B,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QAC1D,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAChC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,SAAS,CAAC,KAAwB;IACzC,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;SACvC,IAAI,CACH,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACP,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC;QAC7D,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;QACrD,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CACpB;SACA,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC;AAED,SAAS,sBAAsB,CAAC,MAAyB;IACvD,IAAI,MAAM,KAAK,QAAQ;QAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACrD,IAAI,MAAM,KAAK,cAAc,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IAC/E,IAAI,MAAM,KAAK,WAAW;QAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACtD,OAAO,CAAC,SAAS,CAAC,CAAC;AACrB,CAAC;AAED,SAAS,qBAAqB,CAAC,IAA0B;IACvD,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,wHAAwH,CAAC;IAClI,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,cAAc,EAAE,CAAC;QACnC,OAAO,sGAAsG,CAAC;IAChH,CAAC;IACD,OAAO,0GAA0G,CAAC;AACpH,CAAC;AAED,SAAS,mBAAmB,CAAC,MAAyB,EAAE,KAAa;IACnE,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,2BAA2B,KAAK,8CAA8C,CAAC;IAChH,IAAI,MAAM,KAAK,cAAc;QAAE,OAAO,iEAAiE,CAAC;IACxG,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,4EAA4E,CAAC;IAC9G,IAAI,MAAM,KAAK,WAAW;QAAE,OAAO,uEAAuE,CAAC;IAC3G,OAAO,YAAY,KAAK,sDAAsD,CAAC;AACjF,CAAC;AAED,SAAS,wBAAwB,CAAC,MAAyB;IACzD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,2HAA2H,CAAC;IACrI,CAAC;IACD,IAAI,MAAM,KAAK,cAAc,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACtD,OAAO,0GAA0G,CAAC;IACpH,CAAC;IACD,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;QAC3B,OAAO,+FAA+F,CAAC;IACzG,CAAC;IACD,OAAO,sGAAsG,CAAC;AAChH,CAAC;AAED,SAAS,kBAAkB,CAAC,EAAU;IACpC,OAAO,oCAAoC,EAAE,6CAA6C,CAAC;AAC7F,CAAC;AAED,SAAS,sBAAsB,CAAC,IAA0B;IACxD,OAAO,oDAAoD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,iBAAiB,CAAC;AACnH,CAAC;AAED,SAAS,YAAY,CAAC,QAA0B;IAC9C,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,CAAC,CAAC;IAChC,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,CAAC,CAAC;IAChC,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,UAAU,CAAC,MAAyB;IAC3C,IAAI,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,OAAO;QAAE,OAAO,CAAC,CAAC;IACxD,IAAI,MAAM,KAAK,WAAW;QAAE,OAAO,CAAC,CAAC;IACrC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC;IACnC,IAAI,MAAM,KAAK,cAAc,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC;IAChE,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAyB;IAClD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,iBAAiB,CAAC;IACnF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PreflightVerdict, QualityScorecardVerdict, WorkplanPriority } from '../types.js';
|
|
2
|
+
import type { AssessBaselineComparison, AssessReport, RiskDeltaSnapshot } from '../types/assess.js';
|
|
3
|
+
export interface RiskDeltaInput {
|
|
4
|
+
healthScore: number;
|
|
5
|
+
qualityVerdict: QualityScorecardVerdict;
|
|
6
|
+
preflightVerdict: PreflightVerdict;
|
|
7
|
+
proofCards: Array<{
|
|
8
|
+
id: string;
|
|
9
|
+
priority: WorkplanPriority;
|
|
10
|
+
source: string;
|
|
11
|
+
}>;
|
|
12
|
+
selectedCardIds?: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function computeRiskDelta(input: RiskDeltaInput): RiskDeltaSnapshot;
|
|
15
|
+
export declare function compareRiskDeltaSnapshots(input: {
|
|
16
|
+
previous: Pick<AssessReport, 'riskDelta'>;
|
|
17
|
+
current: Pick<AssessReport, 'riskDelta'>;
|
|
18
|
+
baselinePath?: string;
|
|
19
|
+
}): AssessBaselineComparison;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export function computeRiskDelta(input) {
|
|
2
|
+
const basis = [`health score ${clamp(input.healthScore)}`];
|
|
3
|
+
const qualityPenalty = qualityVerdictPenalty(input.qualityVerdict);
|
|
4
|
+
const preflightPenalty = preflightVerdictPenalty(input.preflightVerdict);
|
|
5
|
+
const cardPenalty = input.proofCards.reduce((sum, card) => sum + priorityPenalty(card.priority), 0);
|
|
6
|
+
if (qualityPenalty > 0)
|
|
7
|
+
basis.push(`quality verdict ${input.qualityVerdict} penalty ${qualityPenalty}`);
|
|
8
|
+
if (preflightPenalty > 0)
|
|
9
|
+
basis.push(`preflight verdict ${input.preflightVerdict} penalty ${preflightPenalty}`);
|
|
10
|
+
if (cardPenalty > 0)
|
|
11
|
+
basis.push(`${input.proofCards.length} proof card risk penalty ${cardPenalty}`);
|
|
12
|
+
const baselineScore = clamp(input.healthScore - qualityPenalty - preflightPenalty - cardPenalty);
|
|
13
|
+
const selected = new Set(input.selectedCardIds ?? []);
|
|
14
|
+
const selectedCards = input.proofCards.filter((card) => selected.has(card.id));
|
|
15
|
+
const improvement = selectedCards.reduce((sum, card) => sum + expectedImprovement(card.priority), 0);
|
|
16
|
+
for (const card of selectedCards) {
|
|
17
|
+
basis.push(`${card.priority} ${card.source} improvement ${expectedImprovement(card.priority)}`);
|
|
18
|
+
}
|
|
19
|
+
const projectedScore = clamp(baselineScore + improvement);
|
|
20
|
+
return {
|
|
21
|
+
baselineScore,
|
|
22
|
+
projectedScore,
|
|
23
|
+
delta: projectedScore - baselineScore,
|
|
24
|
+
basis,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function compareRiskDeltaSnapshots(input) {
|
|
28
|
+
const previousScore = clamp(input.previous.riskDelta.projectedScore);
|
|
29
|
+
const currentScore = clamp(input.current.riskDelta.projectedScore);
|
|
30
|
+
const delta = currentScore - previousScore;
|
|
31
|
+
const suffix = input.baselinePath ? ` since ${input.baselinePath}` : '';
|
|
32
|
+
const direction = delta > 0 ? 'improved' : delta < 0 ? 'declined' : 'is unchanged';
|
|
33
|
+
const amount = delta === 0 ? '' : ` by ${Math.abs(delta)}`;
|
|
34
|
+
return {
|
|
35
|
+
previousScore,
|
|
36
|
+
currentScore,
|
|
37
|
+
delta,
|
|
38
|
+
...(input.baselinePath ? { baselinePath: input.baselinePath } : {}),
|
|
39
|
+
summary: `risk score ${direction}${amount}${suffix}`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function qualityVerdictPenalty(verdict) {
|
|
43
|
+
if (verdict === 'blocked')
|
|
44
|
+
return 30;
|
|
45
|
+
if (verdict === 'needs_attention')
|
|
46
|
+
return 15;
|
|
47
|
+
if (verdict === 'healthy')
|
|
48
|
+
return 4;
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
function preflightVerdictPenalty(verdict) {
|
|
52
|
+
if (verdict === 'block')
|
|
53
|
+
return 30;
|
|
54
|
+
if (verdict === 'caution')
|
|
55
|
+
return 12;
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
function priorityPenalty(priority) {
|
|
59
|
+
if (priority === 'p0')
|
|
60
|
+
return 12;
|
|
61
|
+
if (priority === 'p1')
|
|
62
|
+
return 8;
|
|
63
|
+
return 3;
|
|
64
|
+
}
|
|
65
|
+
function expectedImprovement(priority) {
|
|
66
|
+
if (priority === 'p0')
|
|
67
|
+
return 18;
|
|
68
|
+
if (priority === 'p1')
|
|
69
|
+
return 12;
|
|
70
|
+
return 5;
|
|
71
|
+
}
|
|
72
|
+
function clamp(value) {
|
|
73
|
+
if (!Number.isFinite(value))
|
|
74
|
+
return 0;
|
|
75
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=riskDelta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"riskDelta.js","sourceRoot":"","sources":["../../src/core/riskDelta.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { buildCodeGraph } from './codeGraph.js';
|
|
3
|
+
import { computeQualityScorecard } from './qualityScorecard.js';
|
|
4
|
+
import { scanRepository } from './repositoryScanner.js';
|
|
5
|
+
import { computeRiskDelta } from './riskDelta.js';
|
|
6
|
+
const DEFAULT_MAX_FILES = 5;
|
|
7
|
+
const NO_MATCH_WARNING = 'No repo files matched the plan. Mention a file, symbol, command, package, or module name for a stronger simulation.';
|
|
8
|
+
export async function computeSimulation(rootPath, options) {
|
|
9
|
+
const plan = normalizePlan(options.plan);
|
|
10
|
+
if (!plan)
|
|
11
|
+
throw new Error('simulate requires a non-empty plan');
|
|
12
|
+
const maxFiles = normalizeMaxFiles(options.maxFiles);
|
|
13
|
+
const [scan, quality] = await Promise.all([
|
|
14
|
+
scanRepository(rootPath),
|
|
15
|
+
computeQualityScorecard(rootPath, { maxRisks: maxFiles }),
|
|
16
|
+
]);
|
|
17
|
+
const graph = await buildCodeGraph(rootPath, scan.files).catch(() => undefined);
|
|
18
|
+
const candidateFiles = rankCandidateFiles({
|
|
19
|
+
plan,
|
|
20
|
+
files: scan.files,
|
|
21
|
+
graph,
|
|
22
|
+
risks: quality.topRisks,
|
|
23
|
+
}).slice(0, maxFiles);
|
|
24
|
+
const testsLikelyAffected = likelyTests(scan.files, candidateFiles);
|
|
25
|
+
const contractsLikelyAffected = inferContracts(plan, candidateFiles);
|
|
26
|
+
const warnings = candidateFiles.length === 0 ? [NO_MATCH_WARNING] : [];
|
|
27
|
+
const proofCards = candidateFiles.map((candidate) => ({
|
|
28
|
+
id: `simulate-${slug(candidate.path)}`,
|
|
29
|
+
priority: priorityForCandidate(candidate),
|
|
30
|
+
source: candidate.qualityRisk ? 'hotspot' : 'plan',
|
|
31
|
+
}));
|
|
32
|
+
const riskDelta = computeRiskDelta({
|
|
33
|
+
healthScore: quality.health.score,
|
|
34
|
+
qualityVerdict: quality.verdict,
|
|
35
|
+
preflightVerdict: 'proceed',
|
|
36
|
+
proofCards,
|
|
37
|
+
selectedCardIds: proofCards.slice(0, 2).map((card) => card.id),
|
|
38
|
+
});
|
|
39
|
+
const confidence = confidenceFor(candidateFiles, graph);
|
|
40
|
+
const verdict = verdictFor(confidence, riskDelta.delta);
|
|
41
|
+
const rolloutPlan = buildRolloutPlan(plan, candidateFiles, testsLikelyAffected);
|
|
42
|
+
const proofCommands = buildProofCommands(plan, candidateFiles, testsLikelyAffected);
|
|
43
|
+
const evidence = buildEvidence(candidateFiles, testsLikelyAffected, contractsLikelyAffected);
|
|
44
|
+
return {
|
|
45
|
+
schemaVersion: 1,
|
|
46
|
+
plan,
|
|
47
|
+
verdict,
|
|
48
|
+
confidence,
|
|
49
|
+
summary: summarize(verdict, confidence, candidateFiles, riskDelta.delta),
|
|
50
|
+
filesLikelyTouched: candidateFiles,
|
|
51
|
+
testsLikelyAffected,
|
|
52
|
+
contractsLikelyAffected,
|
|
53
|
+
riskDelta,
|
|
54
|
+
rolloutPlan,
|
|
55
|
+
proofCommands,
|
|
56
|
+
evidence,
|
|
57
|
+
warnings,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function rankCandidateFiles(input) {
|
|
61
|
+
const planLower = input.plan.toLowerCase();
|
|
62
|
+
const planTerms = tokenize(input.plan);
|
|
63
|
+
const riskByFile = new Map();
|
|
64
|
+
for (const risk of input.risks) {
|
|
65
|
+
for (const file of risk.files)
|
|
66
|
+
riskByFile.set(file, risk);
|
|
67
|
+
}
|
|
68
|
+
return input.files
|
|
69
|
+
.map((file) => scoreFile(file, planLower, planTerms, input.graph, riskByFile.get(file.relativePath)))
|
|
70
|
+
.filter((candidate) => candidate.score > 0)
|
|
71
|
+
.sort((a, b) => b.score - a.score ||
|
|
72
|
+
testRank(a.path) - testRank(b.path) ||
|
|
73
|
+
a.path.localeCompare(b.path));
|
|
74
|
+
}
|
|
75
|
+
function scoreFile(file, planLower, planTerms, graph, qualityRisk) {
|
|
76
|
+
const basename = path.basename(file.relativePath);
|
|
77
|
+
const basenameNoExt = basename.replace(/\.[^.]+$/, '');
|
|
78
|
+
const fileTerms = tokenize(`${file.relativePath} ${basenameNoExt}`);
|
|
79
|
+
const reasons = [];
|
|
80
|
+
let score = 0;
|
|
81
|
+
if (planLower.includes(file.relativePath.toLowerCase())) {
|
|
82
|
+
score += 80;
|
|
83
|
+
reasons.push(`plan mentions ${file.relativePath}`);
|
|
84
|
+
}
|
|
85
|
+
else if (planLower.includes(basename.toLowerCase())) {
|
|
86
|
+
score += 70;
|
|
87
|
+
reasons.push(`plan mentions ${basename}`);
|
|
88
|
+
}
|
|
89
|
+
else if (basenameNoExt && planLower.includes(basenameNoExt.toLowerCase())) {
|
|
90
|
+
score += 60;
|
|
91
|
+
reasons.push(`plan mentions ${basenameNoExt}`);
|
|
92
|
+
}
|
|
93
|
+
const overlap = [...fileTerms].filter((term) => planTerms.has(term) && term.length > 2);
|
|
94
|
+
if (overlap.length > 0) {
|
|
95
|
+
score += overlap.length * 12;
|
|
96
|
+
reasons.push(`plan shares term(s): ${overlap.slice(0, 4).join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
if (qualityRisk && score > 0) {
|
|
99
|
+
score += 8;
|
|
100
|
+
reasons.push(`quality signal: ${qualityRisk.title}`);
|
|
101
|
+
}
|
|
102
|
+
const gf = graph?.files.get(file.relativePath);
|
|
103
|
+
const fanIn = graph?.localImporters.get(file.relativePath)?.size ?? 0;
|
|
104
|
+
const fanOut = gf?.imports.length ?? 0;
|
|
105
|
+
if (score > 0 && gf && (fanIn > 0 || fanOut > 0))
|
|
106
|
+
score += Math.min(10, fanIn + fanOut);
|
|
107
|
+
const graphSummary = gf && graph ? graphFor(file.relativePath, gf, graph) : undefined;
|
|
108
|
+
return {
|
|
109
|
+
path: file.relativePath,
|
|
110
|
+
score,
|
|
111
|
+
reasons,
|
|
112
|
+
...(graphSummary ? { graph: graphSummary } : {}),
|
|
113
|
+
...(qualityRisk ? { qualityRisk: qualityRisk.title } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function graphFor(file, graphFile, graph) {
|
|
117
|
+
return {
|
|
118
|
+
fanIn: graph.localImporters.get(file)?.size ?? 0,
|
|
119
|
+
fanOut: graphFile.imports.length,
|
|
120
|
+
directImporters: [...(graph.localImporters.get(file) ?? new Set())].sort(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function likelyTests(files, candidates) {
|
|
124
|
+
const testFiles = files
|
|
125
|
+
.map((file) => file.relativePath)
|
|
126
|
+
.filter((file) => /(?:^|[./_-])(test|spec)\.[^.]+$/.test(file) || /\.(test|spec)\.[^.]+$/.test(file))
|
|
127
|
+
.sort();
|
|
128
|
+
const matches = new Map();
|
|
129
|
+
for (const candidate of candidates) {
|
|
130
|
+
const candidateBase = path.basename(candidate.path).replace(/\.[^.]+$/, '').toLowerCase();
|
|
131
|
+
for (const test of testFiles) {
|
|
132
|
+
const testLower = test.toLowerCase();
|
|
133
|
+
const score = testScore(testLower, candidateBase);
|
|
134
|
+
if (score > 0)
|
|
135
|
+
matches.set(test, Math.max(matches.get(test) ?? 0, score));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return [...matches]
|
|
139
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
140
|
+
.map(([test]) => test);
|
|
141
|
+
}
|
|
142
|
+
function testScore(testLower, candidateBase) {
|
|
143
|
+
const testBase = path
|
|
144
|
+
.basename(testLower)
|
|
145
|
+
.replace(/\.(test|spec)\.[^.]+$/, '')
|
|
146
|
+
.replace(/\.[^.]+$/, '');
|
|
147
|
+
if (testBase === candidateBase)
|
|
148
|
+
return 100;
|
|
149
|
+
return testLower.includes(candidateBase) ? 40 : 0;
|
|
150
|
+
}
|
|
151
|
+
function inferContracts(plan, candidates) {
|
|
152
|
+
const terms = tokenize(plan);
|
|
153
|
+
const contracts = new Set();
|
|
154
|
+
if (hasAny(terms, ['split', 'extract', 'module', 'modules', 'boundary']))
|
|
155
|
+
contracts.add('module boundary');
|
|
156
|
+
if (hasAny(terms, ['cli', 'command', 'commands']))
|
|
157
|
+
contracts.add('CLI command surface');
|
|
158
|
+
if (hasAny(terms, ['mcp', 'tool', 'tools']))
|
|
159
|
+
contracts.add('MCP tool surface');
|
|
160
|
+
if (hasAny(terms, ['type', 'types', 'api', 'export', 'public']))
|
|
161
|
+
contracts.add('public API/types');
|
|
162
|
+
if (candidates.some((candidate) => candidate.path.startsWith('src/cli/')))
|
|
163
|
+
contracts.add('CLI command surface');
|
|
164
|
+
if (candidates.some((candidate) => candidate.path.startsWith('src/mcp/')))
|
|
165
|
+
contracts.add('MCP tool surface');
|
|
166
|
+
if (candidates.some((candidate) => candidate.path.includes('/types') || candidate.path === 'src/types.ts'))
|
|
167
|
+
contracts.add('public API/types');
|
|
168
|
+
return [...contracts].sort();
|
|
169
|
+
}
|
|
170
|
+
function buildRolloutPlan(plan, candidates, tests) {
|
|
171
|
+
const primary = candidates[0]?.path;
|
|
172
|
+
const firstTest = tests[0];
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
title: 'Lock the current behavior',
|
|
176
|
+
detail: firstTest
|
|
177
|
+
? `Run or add the closest regression test before moving code: ${firstTest}.`
|
|
178
|
+
: 'Add one regression test around the behavior named in the plan before moving code.',
|
|
179
|
+
commands: firstTest ? [`npm test -- ${firstTest}`] : ['projscan assess --mode fix-first --format json'],
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
title: 'Extract the smallest module boundary',
|
|
183
|
+
detail: primary
|
|
184
|
+
? `Move one responsibility out of ${primary}; keep imports and public exports stable until tests pass.`
|
|
185
|
+
: 'Name the target file or symbol, then extract one responsibility without broad cleanup.',
|
|
186
|
+
commands: primary ? [`projscan file ${primary} --format json`] : ['projscan understand --intent "where should this change go?" --format json'],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
title: 'Wire callers through the existing public surface',
|
|
190
|
+
detail: 'Update direct importers first and avoid renaming public contracts unless the simulation calls them out.',
|
|
191
|
+
commands: primary ? [`projscan impact ${primary} --format json`] : ['projscan quality-scorecard --format json'],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
title: 'Run proof commands and compare risk',
|
|
195
|
+
detail: `After the change, rerun the simulator for the same plan and compare risk delta.`,
|
|
196
|
+
commands: [`projscan simulate --plan ${quotePlan(plan)} --format json`],
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
function buildProofCommands(plan, candidates, tests) {
|
|
201
|
+
const commands = [
|
|
202
|
+
`projscan simulate --plan ${quotePlan(plan)} --format json`,
|
|
203
|
+
'projscan assess --mode fix-first --format json',
|
|
204
|
+
'projscan quality-scorecard --format json',
|
|
205
|
+
];
|
|
206
|
+
for (const candidate of candidates.slice(0, 3)) {
|
|
207
|
+
commands.push(`projscan file ${candidate.path} --format json`);
|
|
208
|
+
commands.push(`projscan impact ${candidate.path} --format json`);
|
|
209
|
+
}
|
|
210
|
+
for (const test of tests.slice(0, 3))
|
|
211
|
+
commands.push(`npm test -- ${test}`);
|
|
212
|
+
return [...new Set(commands)];
|
|
213
|
+
}
|
|
214
|
+
function buildEvidence(candidates, tests, contracts) {
|
|
215
|
+
const evidence = [];
|
|
216
|
+
for (const candidate of candidates) {
|
|
217
|
+
evidence.push({
|
|
218
|
+
source: 'plan-match',
|
|
219
|
+
detail: candidate.reasons.join('; '),
|
|
220
|
+
file: candidate.path,
|
|
221
|
+
command: `projscan file ${candidate.path} --format json`,
|
|
222
|
+
});
|
|
223
|
+
if (candidate.graph) {
|
|
224
|
+
evidence.push({
|
|
225
|
+
source: 'code-graph',
|
|
226
|
+
detail: `fan-in ${candidate.graph.fanIn}, fan-out ${candidate.graph.fanOut}, direct importers ${candidate.graph.directImporters.length}`,
|
|
227
|
+
file: candidate.path,
|
|
228
|
+
command: `projscan impact ${candidate.path} --format json`,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (tests.length > 0) {
|
|
233
|
+
evidence.push({
|
|
234
|
+
source: 'test-neighbor',
|
|
235
|
+
detail: `${tests.length} likely affected test file(s)`,
|
|
236
|
+
file: tests[0],
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (contracts.length > 0) {
|
|
240
|
+
evidence.push({
|
|
241
|
+
source: 'contract-inference',
|
|
242
|
+
detail: contracts.join(', '),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return evidence;
|
|
246
|
+
}
|
|
247
|
+
function verdictFor(confidence, delta) {
|
|
248
|
+
if (confidence === 'low')
|
|
249
|
+
return 'needs-more-evidence';
|
|
250
|
+
if (delta <= 0)
|
|
251
|
+
return 'not-worth-it-yet';
|
|
252
|
+
return 'worth-doing';
|
|
253
|
+
}
|
|
254
|
+
function confidenceFor(candidates, graph) {
|
|
255
|
+
if (candidates.length === 0)
|
|
256
|
+
return 'low';
|
|
257
|
+
if (candidates[0] && candidates[0].score >= 80 && graph)
|
|
258
|
+
return 'high';
|
|
259
|
+
return 'medium';
|
|
260
|
+
}
|
|
261
|
+
function summarize(verdict, confidence, candidates, delta) {
|
|
262
|
+
if (candidates.length === 0)
|
|
263
|
+
return `${verdict}: low-confidence simulation needs a concrete target`;
|
|
264
|
+
return `${verdict}: ${confidence}-confidence plan touches ${candidates.length} likely file(s), projected risk delta +${delta}`;
|
|
265
|
+
}
|
|
266
|
+
function priorityForCandidate(candidate) {
|
|
267
|
+
if (candidate.score >= 90)
|
|
268
|
+
return 'p0';
|
|
269
|
+
if (candidate.score >= 45)
|
|
270
|
+
return 'p1';
|
|
271
|
+
return 'p2';
|
|
272
|
+
}
|
|
273
|
+
function normalizePlan(value) {
|
|
274
|
+
return value?.trim().replace(/\s+/g, ' ') ?? '';
|
|
275
|
+
}
|
|
276
|
+
function normalizeMaxFiles(value) {
|
|
277
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
278
|
+
return DEFAULT_MAX_FILES;
|
|
279
|
+
return Math.max(1, Math.min(25, Math.floor(value)));
|
|
280
|
+
}
|
|
281
|
+
function tokenize(value) {
|
|
282
|
+
const spaced = value.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase();
|
|
283
|
+
const terms = spaced.match(/[a-z0-9]+/g) ?? [];
|
|
284
|
+
return new Set(terms.filter((term) => term.length > 1));
|
|
285
|
+
}
|
|
286
|
+
function hasAny(terms, values) {
|
|
287
|
+
return values.some((value) => terms.has(value));
|
|
288
|
+
}
|
|
289
|
+
function quotePlan(plan) {
|
|
290
|
+
return `"${plan.replace(/["\\]/g, '\\$&')}"`;
|
|
291
|
+
}
|
|
292
|
+
function slug(value) {
|
|
293
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
294
|
+
}
|
|
295
|
+
function testRank(file) {
|
|
296
|
+
return /\.(test|spec)\.[^.]+$/.test(file) ? 1 : 0;
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=simulate.js.map
|