@shrkcrft/impact-engine 0.1.0-alpha.10
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/dist/engine/analyzer.d.ts +29 -0
- package/dist/engine/analyzer.d.ts.map +1 -0
- package/dist/engine/analyzer.js +366 -0
- package/dist/engine/risk-score.d.ts +30 -0
- package/dist/engine/risk-score.d.ts.map +1 -0
- package/dist/engine/risk-score.js +66 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/runner/impact-report-store.d.ts +59 -0
- package/dist/runner/impact-report-store.d.ts.map +1 -0
- package/dist/runner/impact-report-store.js +113 -0
- package/dist/schema/impact-analysis.d.ts +73 -0
- package/dist/schema/impact-analysis.d.ts.map +1 -0
- package/dist/schema/impact-analysis.js +13 -0
- package/package.json +53 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type IGraphImpactAnalysis } from '../schema/impact-analysis.js';
|
|
2
|
+
export type IGraphImpactInput = {
|
|
3
|
+
kind: 'files';
|
|
4
|
+
files: readonly string[];
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'symbol';
|
|
7
|
+
symbolId: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'gitref';
|
|
10
|
+
ref: string;
|
|
11
|
+
};
|
|
12
|
+
export interface IAnalyzeOptions {
|
|
13
|
+
projectRoot: string;
|
|
14
|
+
/** Cap on each list. Default 200. */
|
|
15
|
+
limit?: number;
|
|
16
|
+
/** Cap on reverse-closure depth. Default 5. */
|
|
17
|
+
maxDepth?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Compute a v3 graph-backed impact analysis.
|
|
21
|
+
*
|
|
22
|
+
* Failure modes are non-fatal:
|
|
23
|
+
* - missing code graph → diagnostic + minimal payload
|
|
24
|
+
* - missing bridge → no affectedRules/Paths/Templates; not an error
|
|
25
|
+
* - input file unknown to the graph → kept in `normalizedTargets`
|
|
26
|
+
* with a diagnostic, treated as zero-dependent
|
|
27
|
+
*/
|
|
28
|
+
export declare function analyzeGraphImpact(input: IGraphImpactInput, options: IAnalyzeOptions): IGraphImpactAnalysis;
|
|
29
|
+
//# sourceMappingURL=analyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../src/engine/analyzer.ts"],"names":[],"mappings":"AAYA,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,8BAA8B,CAAC;AAGtC,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpC,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,iBAAiB,EACxB,OAAO,EAAE,eAAe,GACvB,oBAAoB,CA+OtB"}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { EdgeKind, GraphQueryApi, GraphStore, NodeKind, } from '@shrkcrft/graph';
|
|
3
|
+
import { BridgeStore, RuleGraphQueryApi, } from '@shrkcrft/rule-graph';
|
|
4
|
+
import { GRAPH_IMPACT_SCHEMA, } from "../schema/impact-analysis.js";
|
|
5
|
+
import { classifyRisk } from "./risk-score.js";
|
|
6
|
+
/**
|
|
7
|
+
* Compute a v3 graph-backed impact analysis.
|
|
8
|
+
*
|
|
9
|
+
* Failure modes are non-fatal:
|
|
10
|
+
* - missing code graph → diagnostic + minimal payload
|
|
11
|
+
* - missing bridge → no affectedRules/Paths/Templates; not an error
|
|
12
|
+
* - input file unknown to the graph → kept in `normalizedTargets`
|
|
13
|
+
* with a diagnostic, treated as zero-dependent
|
|
14
|
+
*/
|
|
15
|
+
export function analyzeGraphImpact(input, options) {
|
|
16
|
+
const limit = options.limit ?? 200;
|
|
17
|
+
const maxDepth = Math.max(1, Math.min(10, options.maxDepth ?? 5));
|
|
18
|
+
const graphStore = new GraphStore(options.projectRoot);
|
|
19
|
+
const diagnostics = [];
|
|
20
|
+
if (!graphStore.exists()) {
|
|
21
|
+
return missingGraphPayload(input, ['code-graph store missing — run `shrk graph index`']);
|
|
22
|
+
}
|
|
23
|
+
const api = GraphQueryApi.fromStore(options.projectRoot);
|
|
24
|
+
let bridgeApi;
|
|
25
|
+
const bridgeStore = new BridgeStore(options.projectRoot);
|
|
26
|
+
if (bridgeStore.exists()) {
|
|
27
|
+
bridgeApi = RuleGraphQueryApi.fromStores(options.projectRoot);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
diagnostics.push("bridge store missing — `shrk rule-graph index` for affectedRules/Templates");
|
|
31
|
+
}
|
|
32
|
+
// Resolve input → target node ids.
|
|
33
|
+
const targets = [];
|
|
34
|
+
const normalizedTargets = [];
|
|
35
|
+
const addFileTarget = (relPath) => {
|
|
36
|
+
const file = api.findFile(relPath);
|
|
37
|
+
if (file) {
|
|
38
|
+
targets.push({ nodeId: file.id, path: file.path, isSymbol: false });
|
|
39
|
+
normalizedTargets.push(file.id);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
diagnostics.push(`file not in graph: ${relPath}`);
|
|
43
|
+
normalizedTargets.push(`file:${relPath}`);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
if (input.kind === 'files') {
|
|
47
|
+
for (const f of input.files)
|
|
48
|
+
addFileTarget(f);
|
|
49
|
+
}
|
|
50
|
+
else if (input.kind === 'symbol') {
|
|
51
|
+
let symNode;
|
|
52
|
+
if (input.symbolId.startsWith('symbol:')) {
|
|
53
|
+
symNode = api.neighbours(input.symbolId)?.node;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const matches = api.findSymbol(input.symbolId, { exact: true, limit: 5 });
|
|
57
|
+
symNode = matches.find((s) => (s.data?.['isExported'] ?? false) === true) ?? matches[0];
|
|
58
|
+
}
|
|
59
|
+
if (symNode) {
|
|
60
|
+
targets.push({ nodeId: symNode.id, path: symNode.path, isSymbol: true });
|
|
61
|
+
normalizedTargets.push(symNode.id);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
diagnostics.push(`symbol not in graph: ${input.symbolId}`);
|
|
65
|
+
normalizedTargets.push(`symbol:${input.symbolId}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const files = changedFilesSince(options.projectRoot, input.ref);
|
|
70
|
+
if (files.length === 0)
|
|
71
|
+
diagnostics.push(`no files changed since ${input.ref}`);
|
|
72
|
+
for (const f of files)
|
|
73
|
+
addFileTarget(f);
|
|
74
|
+
}
|
|
75
|
+
// Reverse closure over imports-file edges.
|
|
76
|
+
const truncations = {};
|
|
77
|
+
const reachable = new Set();
|
|
78
|
+
for (const t of targets)
|
|
79
|
+
reachable.add(t.nodeId);
|
|
80
|
+
const directIds = new Set();
|
|
81
|
+
{
|
|
82
|
+
let frontier = [...reachable];
|
|
83
|
+
let depth = 1;
|
|
84
|
+
while (depth <= maxDepth && frontier.length > 0) {
|
|
85
|
+
const next = [];
|
|
86
|
+
let truncated = false;
|
|
87
|
+
for (const id of frontier) {
|
|
88
|
+
for (const imp of api.importersOf(id)) {
|
|
89
|
+
if (reachable.has(imp.id))
|
|
90
|
+
continue;
|
|
91
|
+
reachable.add(imp.id);
|
|
92
|
+
next.push(imp.id);
|
|
93
|
+
if (depth === 1)
|
|
94
|
+
directIds.add(imp.id);
|
|
95
|
+
if (reachable.size - targets.length >= limit) {
|
|
96
|
+
truncated = true;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (truncated)
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
if (truncated) {
|
|
104
|
+
truncations['dependents'] = (truncations['dependents'] ?? 0) + 1;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
frontier = next;
|
|
108
|
+
depth += 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const direct = [...directIds]
|
|
112
|
+
.map((id) => toRef(api, id))
|
|
113
|
+
.filter((r) => r !== undefined);
|
|
114
|
+
const transitive = [];
|
|
115
|
+
for (const id of reachable) {
|
|
116
|
+
if (id === targets.find((t) => t.nodeId === id)?.nodeId)
|
|
117
|
+
continue;
|
|
118
|
+
if (directIds.has(id))
|
|
119
|
+
continue;
|
|
120
|
+
if (targets.some((t) => t.nodeId === id))
|
|
121
|
+
continue;
|
|
122
|
+
const ref = toRef(api, id);
|
|
123
|
+
if (ref)
|
|
124
|
+
transitive.push(ref);
|
|
125
|
+
if (transitive.length >= limit) {
|
|
126
|
+
truncations['transitive'] = (truncations['transitive'] ?? 0) + 1;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Symbols declared by target files.
|
|
131
|
+
const affectedSymbols = [];
|
|
132
|
+
const callerSet = new Set();
|
|
133
|
+
for (const t of targets) {
|
|
134
|
+
if (t.isSymbol) {
|
|
135
|
+
affectedSymbols.push(toRefForce(api, t.nodeId));
|
|
136
|
+
for (const c of api.referencesOf(t.nodeId))
|
|
137
|
+
callerSet.add(c.id);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
for (const sym of api.symbolsIn(t.nodeId)) {
|
|
141
|
+
affectedSymbols.push(toRefForce(api, sym.id));
|
|
142
|
+
for (const c of api.referencesOf(sym.id))
|
|
143
|
+
callerSet.add(c.id);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const affectedCallerFiles = [...callerSet]
|
|
148
|
+
.map((id) => toRef(api, id))
|
|
149
|
+
.filter((r) => r !== undefined)
|
|
150
|
+
.slice(0, limit);
|
|
151
|
+
// Affected packages.
|
|
152
|
+
const packagesSet = new Set();
|
|
153
|
+
for (const id of reachable) {
|
|
154
|
+
const pkg = packageOf(api, id);
|
|
155
|
+
if (pkg)
|
|
156
|
+
packagesSet.add(pkg);
|
|
157
|
+
}
|
|
158
|
+
const affectedPackages = [...packagesSet].sort();
|
|
159
|
+
// Rule-graph bridge: rules / paths / templates touching any affected file.
|
|
160
|
+
const affectedRules = [];
|
|
161
|
+
const affectedPaths = [];
|
|
162
|
+
const affectedTemplates = [];
|
|
163
|
+
if (bridgeApi) {
|
|
164
|
+
const seenRule = new Set();
|
|
165
|
+
const seenPath = new Set();
|
|
166
|
+
const seenTpl = new Set();
|
|
167
|
+
for (const id of reachable) {
|
|
168
|
+
const node = api.neighbours(id)?.node;
|
|
169
|
+
if (!node?.path)
|
|
170
|
+
continue;
|
|
171
|
+
const f = bridgeApi.forFile(node.path);
|
|
172
|
+
if (!f)
|
|
173
|
+
continue;
|
|
174
|
+
for (const h of f.rules) {
|
|
175
|
+
if (seenRule.has(h.target.id))
|
|
176
|
+
continue;
|
|
177
|
+
seenRule.add(h.target.id);
|
|
178
|
+
affectedRules.push({
|
|
179
|
+
id: h.target.id,
|
|
180
|
+
label: h.target.label,
|
|
181
|
+
severity: h.edge.data?.['severity'] ?? undefined,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
for (const h of f.paths) {
|
|
185
|
+
if (seenPath.has(h.target.id))
|
|
186
|
+
continue;
|
|
187
|
+
seenPath.add(h.target.id);
|
|
188
|
+
affectedPaths.push({ id: h.target.id, label: h.target.label });
|
|
189
|
+
}
|
|
190
|
+
for (const h of f.templates) {
|
|
191
|
+
if (seenTpl.has(h.target.id))
|
|
192
|
+
continue;
|
|
193
|
+
seenTpl.add(h.target.id);
|
|
194
|
+
affectedTemplates.push({ id: h.target.id, label: h.target.label });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Likely tests: reachable files tagged `test`.
|
|
199
|
+
const likelyTests = [];
|
|
200
|
+
for (const id of reachable) {
|
|
201
|
+
const node = api.neighbours(id)?.node;
|
|
202
|
+
if (!node)
|
|
203
|
+
continue;
|
|
204
|
+
if (!(node.tags ?? []).includes('test'))
|
|
205
|
+
continue;
|
|
206
|
+
const ref = toRef(api, id);
|
|
207
|
+
if (ref)
|
|
208
|
+
likelyTests.push(ref);
|
|
209
|
+
if (likelyTests.length >= limit) {
|
|
210
|
+
truncations['likelyTests'] = (truncations['likelyTests'] ?? 0) + 1;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// publicApiTouched: any target is an index file or declares an exported symbol.
|
|
215
|
+
let publicApiTouched = false;
|
|
216
|
+
for (const t of targets) {
|
|
217
|
+
if (t.path && (/\/index\.ts$/.test(t.path) || /^index\.ts$/.test(t.path) || t.path.endsWith('.d.ts'))) {
|
|
218
|
+
publicApiTouched = true;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
if (t.isSymbol) {
|
|
222
|
+
const node = api.neighbours(t.nodeId)?.node;
|
|
223
|
+
if (node && (node.data?.['isExported'] ?? false) === true) {
|
|
224
|
+
publicApiTouched = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const syms = api.symbolsIn(t.nodeId);
|
|
230
|
+
if (syms.some((s) => (s.data?.['isExported'] ?? false) === true)) {
|
|
231
|
+
publicApiTouched = true;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Risk + validation scope.
|
|
237
|
+
const { risk, reasons } = classifyRisk({
|
|
238
|
+
directCount: direct.length,
|
|
239
|
+
transitiveCount: transitive.length,
|
|
240
|
+
packagesTouched: affectedPackages.length,
|
|
241
|
+
rulesTouched: affectedRules.length,
|
|
242
|
+
templatesTouched: affectedTemplates.length,
|
|
243
|
+
publicApiTouched,
|
|
244
|
+
callerFilesCount: affectedCallerFiles.length,
|
|
245
|
+
});
|
|
246
|
+
const validationScope = deriveValidationScope({
|
|
247
|
+
risk,
|
|
248
|
+
affectedRules: affectedRules.length,
|
|
249
|
+
affectedTemplates: affectedTemplates.length,
|
|
250
|
+
likelyTests: likelyTests.length,
|
|
251
|
+
affectedPackages,
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
schema: GRAPH_IMPACT_SCHEMA,
|
|
255
|
+
inputKind: input.kind,
|
|
256
|
+
normalizedTargets,
|
|
257
|
+
directDependents: direct,
|
|
258
|
+
transitiveDependents: transitive,
|
|
259
|
+
affectedSymbols,
|
|
260
|
+
affectedCallerFiles,
|
|
261
|
+
affectedPackages,
|
|
262
|
+
affectedRules,
|
|
263
|
+
affectedPaths,
|
|
264
|
+
affectedTemplates,
|
|
265
|
+
likelyTests,
|
|
266
|
+
publicApiTouched,
|
|
267
|
+
risk,
|
|
268
|
+
riskReasons: reasons,
|
|
269
|
+
validationScope,
|
|
270
|
+
truncations,
|
|
271
|
+
diagnostics,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function toRef(api, id) {
|
|
275
|
+
const node = api.neighbours(id)?.node;
|
|
276
|
+
if (!node)
|
|
277
|
+
return undefined;
|
|
278
|
+
return toRefFromNode(node);
|
|
279
|
+
}
|
|
280
|
+
function toRefForce(api, id) {
|
|
281
|
+
const ref = toRef(api, id);
|
|
282
|
+
return ref ?? { id, kind: 'unknown', label: id };
|
|
283
|
+
}
|
|
284
|
+
function toRefFromNode(node) {
|
|
285
|
+
return {
|
|
286
|
+
id: node.id,
|
|
287
|
+
kind: node.kind,
|
|
288
|
+
label: node.label,
|
|
289
|
+
...(node.path ? { path: node.path } : {}),
|
|
290
|
+
...(node.line ? { line: node.line } : {}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function packageOf(api, id) {
|
|
294
|
+
// A file → package via BelongsToPackage edge.
|
|
295
|
+
const neighbours = api.neighbours(id);
|
|
296
|
+
if (!neighbours)
|
|
297
|
+
return undefined;
|
|
298
|
+
for (const edge of neighbours.out) {
|
|
299
|
+
if (edge.edge.kind === EdgeKind.BelongsToPackage) {
|
|
300
|
+
const target = edge.target;
|
|
301
|
+
if ('kind' in target && target.kind === NodeKind.Package)
|
|
302
|
+
return target.label;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Symbol → owning file → package.
|
|
306
|
+
if (id.startsWith('symbol:')) {
|
|
307
|
+
const filePath = id.slice('symbol:'.length).split('#')[0];
|
|
308
|
+
if (filePath) {
|
|
309
|
+
const fileId = `file:${filePath}`;
|
|
310
|
+
return packageOf(api, fileId);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
function deriveValidationScope(input) {
|
|
316
|
+
const out = [];
|
|
317
|
+
if (input.affectedRules > 0)
|
|
318
|
+
out.push('shrk check boundaries');
|
|
319
|
+
if (input.affectedTemplates > 0)
|
|
320
|
+
out.push('shrk drift --json');
|
|
321
|
+
if (input.likelyTests > 0)
|
|
322
|
+
out.push('bun test');
|
|
323
|
+
if (input.risk === 'high' || input.risk === 'critical') {
|
|
324
|
+
out.push('shrk doctor');
|
|
325
|
+
out.push('bun x tsc -p tsconfig.base.json --noEmit');
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
function missingGraphPayload(input, diagnostics) {
|
|
330
|
+
return {
|
|
331
|
+
schema: GRAPH_IMPACT_SCHEMA,
|
|
332
|
+
inputKind: input.kind,
|
|
333
|
+
normalizedTargets: [],
|
|
334
|
+
directDependents: [],
|
|
335
|
+
transitiveDependents: [],
|
|
336
|
+
affectedSymbols: [],
|
|
337
|
+
affectedCallerFiles: [],
|
|
338
|
+
affectedPackages: [],
|
|
339
|
+
affectedRules: [],
|
|
340
|
+
affectedPaths: [],
|
|
341
|
+
affectedTemplates: [],
|
|
342
|
+
likelyTests: [],
|
|
343
|
+
publicApiTouched: false,
|
|
344
|
+
risk: 'low',
|
|
345
|
+
riskReasons: [],
|
|
346
|
+
validationScope: [],
|
|
347
|
+
truncations: {},
|
|
348
|
+
diagnostics,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function changedFilesSince(projectRoot, ref) {
|
|
352
|
+
try {
|
|
353
|
+
const raw = execSync(`git diff --name-only ${ref}`, {
|
|
354
|
+
cwd: projectRoot,
|
|
355
|
+
encoding: 'utf8',
|
|
356
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
357
|
+
});
|
|
358
|
+
return raw
|
|
359
|
+
.split('\n')
|
|
360
|
+
.map((s) => s.trim())
|
|
361
|
+
.filter((s) => s.length > 0);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IGraphImpactAnalysis, RiskLevel } from '../schema/impact-analysis.js';
|
|
2
|
+
interface IRiskInputs {
|
|
3
|
+
directCount: number;
|
|
4
|
+
transitiveCount: number;
|
|
5
|
+
packagesTouched: number;
|
|
6
|
+
rulesTouched: number;
|
|
7
|
+
templatesTouched: number;
|
|
8
|
+
publicApiTouched: boolean;
|
|
9
|
+
callerFilesCount: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Deterministic, heuristic risk score. Weights chosen to match the
|
|
13
|
+
* v2 inspector's qualitative behavior (small change → low, big graph
|
|
14
|
+
* spread or public-API touch → high+) without depending on the
|
|
15
|
+
* inspector's exact internal scoring.
|
|
16
|
+
*
|
|
17
|
+
* The thresholds are tuned for the SharkCraft monorepo and similar
|
|
18
|
+
* mid-size TS workspaces. They will need re-tuning when applied to
|
|
19
|
+
* very large or very small codebases — surface that as a future
|
|
20
|
+
* configuration knob if needed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function classifyRisk(input: IRiskInputs): {
|
|
23
|
+
risk: RiskLevel;
|
|
24
|
+
reasons: readonly string[];
|
|
25
|
+
};
|
|
26
|
+
/** Re-export so callers can read the (partial) inputs back for tests. */
|
|
27
|
+
export type { IRiskInputs };
|
|
28
|
+
/** Defensive constructor used by analyzer tests. */
|
|
29
|
+
export declare function emptyImpactAnalysis(): Partial<IGraphImpactAnalysis>;
|
|
30
|
+
//# sourceMappingURL=risk-score.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"risk-score.d.ts","sourceRoot":"","sources":["../../src/engine/risk-score.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAEpF,UAAU,WAAW;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CA2ChG;AAED,yEAAyE;AACzE,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B,oDAAoD;AACpD,wBAAgB,mBAAmB,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAOnE"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, heuristic risk score. Weights chosen to match the
|
|
3
|
+
* v2 inspector's qualitative behavior (small change → low, big graph
|
|
4
|
+
* spread or public-API touch → high+) without depending on the
|
|
5
|
+
* inspector's exact internal scoring.
|
|
6
|
+
*
|
|
7
|
+
* The thresholds are tuned for the SharkCraft monorepo and similar
|
|
8
|
+
* mid-size TS workspaces. They will need re-tuning when applied to
|
|
9
|
+
* very large or very small codebases — surface that as a future
|
|
10
|
+
* configuration knob if needed.
|
|
11
|
+
*/
|
|
12
|
+
export function classifyRisk(input) {
|
|
13
|
+
let score = 0;
|
|
14
|
+
const reasons = [];
|
|
15
|
+
if (input.directCount >= 5) {
|
|
16
|
+
score += 2;
|
|
17
|
+
reasons.push(`${input.directCount} direct dependents`);
|
|
18
|
+
}
|
|
19
|
+
else if (input.directCount > 0) {
|
|
20
|
+
score += 1;
|
|
21
|
+
reasons.push(`${input.directCount} direct dependents`);
|
|
22
|
+
}
|
|
23
|
+
if (input.transitiveCount >= 50) {
|
|
24
|
+
score += 3;
|
|
25
|
+
reasons.push(`${input.transitiveCount} transitive dependents`);
|
|
26
|
+
}
|
|
27
|
+
else if (input.transitiveCount >= 10) {
|
|
28
|
+
score += 2;
|
|
29
|
+
reasons.push(`${input.transitiveCount} transitive dependents`);
|
|
30
|
+
}
|
|
31
|
+
if (input.packagesTouched >= 5) {
|
|
32
|
+
score += 2;
|
|
33
|
+
reasons.push(`${input.packagesTouched} workspace packages spanned`);
|
|
34
|
+
}
|
|
35
|
+
else if (input.packagesTouched >= 2) {
|
|
36
|
+
score += 1;
|
|
37
|
+
reasons.push(`${input.packagesTouched} workspace packages spanned`);
|
|
38
|
+
}
|
|
39
|
+
if (input.rulesTouched > 0) {
|
|
40
|
+
score += 1;
|
|
41
|
+
reasons.push(`${input.rulesTouched} boundary rule(s) apply`);
|
|
42
|
+
}
|
|
43
|
+
if (input.templatesTouched > 0) {
|
|
44
|
+
score += 1;
|
|
45
|
+
reasons.push(`covered by ${input.templatesTouched} template(s) — verify drift`);
|
|
46
|
+
}
|
|
47
|
+
if (input.publicApiTouched) {
|
|
48
|
+
score += 2;
|
|
49
|
+
reasons.push('public API surface touched');
|
|
50
|
+
}
|
|
51
|
+
if (input.callerFilesCount >= 10) {
|
|
52
|
+
score += 1;
|
|
53
|
+
reasons.push(`${input.callerFilesCount} caller files`);
|
|
54
|
+
}
|
|
55
|
+
const risk = score >= 8 ? 'critical' : score >= 5 ? 'high' : score >= 2 ? 'medium' : 'low';
|
|
56
|
+
return { risk, reasons };
|
|
57
|
+
}
|
|
58
|
+
/** Defensive constructor used by analyzer tests. */
|
|
59
|
+
export function emptyImpactAnalysis() {
|
|
60
|
+
return {
|
|
61
|
+
risk: 'low',
|
|
62
|
+
riskReasons: [],
|
|
63
|
+
truncations: {},
|
|
64
|
+
diagnostics: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAC;AAC5C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,iCAAiC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { IGraphImpactAnalysis } from '../schema/impact-analysis.js';
|
|
2
|
+
export declare const IMPACT_RUN_SCHEMA: "sharkcraft.impact-run/v1";
|
|
3
|
+
/**
|
|
4
|
+
* Compact, doctor-friendly snapshot of the most recent `shrk impact`
|
|
5
|
+
* run. The full v3 payload can carry hundreds of `IAffectedNodeRef`
|
|
6
|
+
* entries — we keep counts + a representative `inputSummary` so the
|
|
7
|
+
* dashboard / doctor can answer "what was the last analysis" without
|
|
8
|
+
* loading every dependent.
|
|
9
|
+
*/
|
|
10
|
+
export interface IImpactRunReport {
|
|
11
|
+
schema: typeof IMPACT_RUN_SCHEMA;
|
|
12
|
+
generatedAt: string;
|
|
13
|
+
inputKind: 'files' | 'symbol' | 'gitref';
|
|
14
|
+
/** Short, human-readable summary of the request (file list, ref, …). */
|
|
15
|
+
inputSummary: string;
|
|
16
|
+
risk: 'low' | 'medium' | 'high' | 'critical';
|
|
17
|
+
directDependentCount: number;
|
|
18
|
+
transitiveDependentCount: number;
|
|
19
|
+
affectedPackageCount: number;
|
|
20
|
+
affectedSymbolCount: number;
|
|
21
|
+
affectedCallerFileCount: number;
|
|
22
|
+
affectedRuleCount: number;
|
|
23
|
+
affectedTemplateCount: number;
|
|
24
|
+
likelyTestCount: number;
|
|
25
|
+
publicApiTouched: boolean;
|
|
26
|
+
riskReasons: readonly string[];
|
|
27
|
+
validationScope: readonly string[];
|
|
28
|
+
diagnostics: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
export declare class ImpactReportStore {
|
|
31
|
+
private readonly projectRoot;
|
|
32
|
+
readonly absPath: string;
|
|
33
|
+
readonly baselinePath: string;
|
|
34
|
+
constructor(projectRoot: string);
|
|
35
|
+
exists(): boolean;
|
|
36
|
+
baselineExists(): boolean;
|
|
37
|
+
read(): IImpactRunReport | undefined;
|
|
38
|
+
write(report: IImpactRunReport): void;
|
|
39
|
+
readBaseline(): IImpactRunReport | undefined;
|
|
40
|
+
writeBaseline(report: IImpactRunReport): void;
|
|
41
|
+
clearBaseline(): boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface IImpactDelta {
|
|
44
|
+
/** last.dependents (direct + transitive) − baseline.dependents. */
|
|
45
|
+
dependentDelta: number;
|
|
46
|
+
/** last.packageCount − baseline.packageCount. */
|
|
47
|
+
packageDelta: number;
|
|
48
|
+
/** Risk drift summary, e.g. "low → high". */
|
|
49
|
+
riskDrift?: string;
|
|
50
|
+
/** Is `last` strictly worse than baseline along any axis? */
|
|
51
|
+
worsened: boolean;
|
|
52
|
+
}
|
|
53
|
+
export declare function diffImpactReports(baseline: IImpactRunReport, last: IImpactRunReport): IImpactDelta;
|
|
54
|
+
/**
|
|
55
|
+
* Build a compact `IImpactRunReport` from the full v3 payload + the
|
|
56
|
+
* request summary string. Pure; callers persist via `ImpactReportStore`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function snapshotImpactAnalysis(analysis: IGraphImpactAnalysis, inputSummary: string): IImpactRunReport;
|
|
59
|
+
//# sourceMappingURL=impact-report-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"impact-report-store.d.ts","sourceRoot":"","sources":["../../src/runner/impact-report-store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAEzE,eAAO,MAAM,iBAAiB,EAAG,0BAAmC,CAAC;AAIrE;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,iBAAiB,CAAC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACzC,wEAAwE;IACxE,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;IAC7C,oBAAoB,EAAE,MAAM,CAAC;IAC7B,wBAAwB,EAAE,MAAM,CAAC;IACjC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,uBAAuB,EAAE,MAAM,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,qBAAa,iBAAiB;IAIhB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAHxC,SAAgB,OAAO,EAAE,MAAM,CAAC;IAChC,SAAgB,YAAY,EAAE,MAAM,CAAC;gBAER,WAAW,EAAE,MAAM;IAKhD,MAAM,IAAI,OAAO;IAIjB,cAAc,IAAI,OAAO;IAIzB,IAAI,IAAI,gBAAgB,GAAG,SAAS;IAWpC,KAAK,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI;IAKrC,YAAY,IAAI,gBAAgB,GAAG,SAAS;IAW5C,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI;IAK7C,aAAa,IAAI,OAAO;CAKzB;AAED,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,cAAc,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,gBAAgB,GACrB,YAAY,CAcd;AAeD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,oBAAoB,EAC9B,YAAY,EAAE,MAAM,GACnB,gBAAgB,CAoBlB"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
export const IMPACT_RUN_SCHEMA = 'sharkcraft.impact-run/v1';
|
|
4
|
+
const REPORT_REL = '.sharkcraft/impact/last.json';
|
|
5
|
+
const BASELINE_REL = '.sharkcraft/impact/baseline.json';
|
|
6
|
+
export class ImpactReportStore {
|
|
7
|
+
projectRoot;
|
|
8
|
+
absPath;
|
|
9
|
+
baselinePath;
|
|
10
|
+
constructor(projectRoot) {
|
|
11
|
+
this.projectRoot = projectRoot;
|
|
12
|
+
this.absPath = nodePath.join(projectRoot, REPORT_REL);
|
|
13
|
+
this.baselinePath = nodePath.join(projectRoot, BASELINE_REL);
|
|
14
|
+
}
|
|
15
|
+
exists() {
|
|
16
|
+
return existsSync(this.absPath);
|
|
17
|
+
}
|
|
18
|
+
baselineExists() {
|
|
19
|
+
return existsSync(this.baselinePath);
|
|
20
|
+
}
|
|
21
|
+
read() {
|
|
22
|
+
if (!this.exists())
|
|
23
|
+
return undefined;
|
|
24
|
+
try {
|
|
25
|
+
const raw = JSON.parse(readFileSync(this.absPath, 'utf8'));
|
|
26
|
+
if (raw.schema !== IMPACT_RUN_SCHEMA)
|
|
27
|
+
return undefined;
|
|
28
|
+
return raw;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
write(report) {
|
|
35
|
+
mkdirSync(nodePath.dirname(this.absPath), { recursive: true });
|
|
36
|
+
writeFileSync(this.absPath, JSON.stringify(report, null, 2), 'utf8');
|
|
37
|
+
}
|
|
38
|
+
readBaseline() {
|
|
39
|
+
if (!this.baselineExists())
|
|
40
|
+
return undefined;
|
|
41
|
+
try {
|
|
42
|
+
const raw = JSON.parse(readFileSync(this.baselinePath, 'utf8'));
|
|
43
|
+
if (raw.schema !== IMPACT_RUN_SCHEMA)
|
|
44
|
+
return undefined;
|
|
45
|
+
return raw;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
writeBaseline(report) {
|
|
52
|
+
mkdirSync(nodePath.dirname(this.baselinePath), { recursive: true });
|
|
53
|
+
writeFileSync(this.baselinePath, JSON.stringify(report, null, 2), 'utf8');
|
|
54
|
+
}
|
|
55
|
+
clearBaseline() {
|
|
56
|
+
if (!this.baselineExists())
|
|
57
|
+
return false;
|
|
58
|
+
rmSync(this.baselinePath);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function diffImpactReports(baseline, last) {
|
|
63
|
+
const baseDeps = baseline.directDependentCount + baseline.transitiveDependentCount;
|
|
64
|
+
const lastDeps = last.directDependentCount + last.transitiveDependentCount;
|
|
65
|
+
const baseRiskIdx = riskRank(baseline.risk);
|
|
66
|
+
const lastRiskIdx = riskRank(last.risk);
|
|
67
|
+
const riskWorsened = lastRiskIdx > baseRiskIdx;
|
|
68
|
+
const dependentWorsened = lastDeps > baseDeps;
|
|
69
|
+
const packageWorsened = last.affectedPackageCount > baseline.affectedPackageCount;
|
|
70
|
+
return {
|
|
71
|
+
dependentDelta: lastDeps - baseDeps,
|
|
72
|
+
packageDelta: last.affectedPackageCount - baseline.affectedPackageCount,
|
|
73
|
+
...(baseline.risk !== last.risk ? { riskDrift: `${baseline.risk} → ${last.risk}` } : {}),
|
|
74
|
+
worsened: riskWorsened || dependentWorsened || packageWorsened,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function riskRank(r) {
|
|
78
|
+
switch (r) {
|
|
79
|
+
case 'low':
|
|
80
|
+
return 0;
|
|
81
|
+
case 'medium':
|
|
82
|
+
return 1;
|
|
83
|
+
case 'high':
|
|
84
|
+
return 2;
|
|
85
|
+
case 'critical':
|
|
86
|
+
return 3;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Build a compact `IImpactRunReport` from the full v3 payload + the
|
|
91
|
+
* request summary string. Pure; callers persist via `ImpactReportStore`.
|
|
92
|
+
*/
|
|
93
|
+
export function snapshotImpactAnalysis(analysis, inputSummary) {
|
|
94
|
+
return {
|
|
95
|
+
schema: IMPACT_RUN_SCHEMA,
|
|
96
|
+
generatedAt: new Date().toISOString(),
|
|
97
|
+
inputKind: analysis.inputKind,
|
|
98
|
+
inputSummary,
|
|
99
|
+
risk: analysis.risk,
|
|
100
|
+
directDependentCount: analysis.directDependents.length,
|
|
101
|
+
transitiveDependentCount: analysis.transitiveDependents.length,
|
|
102
|
+
affectedPackageCount: analysis.affectedPackages.length,
|
|
103
|
+
affectedSymbolCount: analysis.affectedSymbols.length,
|
|
104
|
+
affectedCallerFileCount: analysis.affectedCallerFiles.length,
|
|
105
|
+
affectedRuleCount: analysis.affectedRules.length,
|
|
106
|
+
affectedTemplateCount: analysis.affectedTemplates.length,
|
|
107
|
+
likelyTestCount: analysis.likelyTests.length,
|
|
108
|
+
publicApiTouched: analysis.publicApiTouched,
|
|
109
|
+
riskReasons: [...analysis.riskReasons],
|
|
110
|
+
validationScope: [...analysis.validationScope],
|
|
111
|
+
diagnostics: [...analysis.diagnostics],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3 impact-analysis payload. Built from the persistent code graph and
|
|
3
|
+
* (when available) the rule-graph bridge. Intentionally compatible with
|
|
4
|
+
* the existing v2 payload's *core shape* (input kind, normalised
|
|
5
|
+
* targets, dependent files, packages, tests) while adding:
|
|
6
|
+
*
|
|
7
|
+
* - affectedSymbols / affectedCallerFiles — Wave 3 symbol edges.
|
|
8
|
+
* - affectedRules / affectedTemplates / affectedPaths — rule-graph bridge.
|
|
9
|
+
* - publicApiTouched — true if any normalised target is an index
|
|
10
|
+
* entrypoint or declares an exported symbol.
|
|
11
|
+
* - validationScope — exact `shrk …` commands to run.
|
|
12
|
+
*/
|
|
13
|
+
export declare const GRAPH_IMPACT_SCHEMA: "sharkcraft.graph-impact-analysis/v3";
|
|
14
|
+
export type GraphImpactSchemaVersion = typeof GRAPH_IMPACT_SCHEMA;
|
|
15
|
+
export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
|
|
16
|
+
export interface IAffectedNodeRef {
|
|
17
|
+
/** Stable graph node id, e.g. `file:packages/foo/src/bar.ts`. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** Project-relative path for File / Symbol nodes. */
|
|
20
|
+
path?: string;
|
|
21
|
+
/** Display label (symbol name, file basename, …). */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Kind tag, e.g. 'file' | 'symbol' | 'package'. */
|
|
24
|
+
kind: string;
|
|
25
|
+
/** 1-based line number for symbols. */
|
|
26
|
+
line?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface IAffectedAssetRef {
|
|
29
|
+
/** Bridge node id, e.g. `boundary:core.is-base-layer`. */
|
|
30
|
+
id: string;
|
|
31
|
+
/** Display label. */
|
|
32
|
+
label: string;
|
|
33
|
+
/** Optional severity / data carried over from the bridge edge. */
|
|
34
|
+
severity?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface IGraphImpactAnalysis {
|
|
37
|
+
schema: GraphImpactSchemaVersion;
|
|
38
|
+
/** What kind of input the report was built from. */
|
|
39
|
+
inputKind: 'files' | 'symbol' | 'gitref';
|
|
40
|
+
/** Original normalised targets (file paths or `symbol:` ids). */
|
|
41
|
+
normalizedTargets: readonly string[];
|
|
42
|
+
/** Files importing a target directly (1-hop). */
|
|
43
|
+
directDependents: readonly IAffectedNodeRef[];
|
|
44
|
+
/** Files reachable via repeated reverse-import walk (capped). */
|
|
45
|
+
transitiveDependents: readonly IAffectedNodeRef[];
|
|
46
|
+
/** Symbols *declared by* the targets. */
|
|
47
|
+
affectedSymbols: readonly IAffectedNodeRef[];
|
|
48
|
+
/** Files that call/reference any affected symbol. */
|
|
49
|
+
affectedCallerFiles: readonly IAffectedNodeRef[];
|
|
50
|
+
/** Workspace packages touched (union of target packages + dependents). */
|
|
51
|
+
affectedPackages: readonly string[];
|
|
52
|
+
/** Rules (boundary) that apply to a target or dependent file. */
|
|
53
|
+
affectedRules: readonly IAffectedAssetRef[];
|
|
54
|
+
/** Path conventions matching a target or dependent file. */
|
|
55
|
+
affectedPaths: readonly IAffectedAssetRef[];
|
|
56
|
+
/** Templates whose output covers a target or dependent file. */
|
|
57
|
+
affectedTemplates: readonly IAffectedAssetRef[];
|
|
58
|
+
/** Likely tests for the targets / dependents (tag=test + dependent). */
|
|
59
|
+
likelyTests: readonly IAffectedNodeRef[];
|
|
60
|
+
/** True if any normalised target is an `index.ts` or declares exports. */
|
|
61
|
+
publicApiTouched: boolean;
|
|
62
|
+
/** Risk classification. */
|
|
63
|
+
risk: RiskLevel;
|
|
64
|
+
/** Human-readable reasons that contributed to the risk score. */
|
|
65
|
+
riskReasons: readonly string[];
|
|
66
|
+
/** Exact CLI commands to run before / after merging the change. */
|
|
67
|
+
validationScope: readonly string[];
|
|
68
|
+
/** Capped-list counters (when lists were truncated). */
|
|
69
|
+
truncations: Readonly<Record<string, number>>;
|
|
70
|
+
/** Free-form diagnostics (missing index, stale graph, etc.). */
|
|
71
|
+
diagnostics: readonly string[];
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=impact-analysis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"impact-analysis.d.ts","sourceRoot":"","sources":["../../src/schema/impact-analysis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,EAAG,qCAA8C,CAAC;AAElF,MAAM,MAAM,wBAAwB,GAAG,OAAO,mBAAmB,CAAC;AAElE,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;AAE/D,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,EAAE,EAAE,MAAM,CAAC;IACX,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,wBAAwB,CAAC;IACjC,oDAAoD;IACpD,SAAS,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACzC,iEAAiE;IACjE,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,iDAAiD;IACjD,gBAAgB,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC9C,iEAAiE;IACjE,oBAAoB,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAClD,yCAAyC;IACzC,eAAe,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,qDAAqD;IACrD,mBAAmB,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACjD,0EAA0E;IAC1E,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,iEAAiE;IACjE,aAAa,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC5C,4DAA4D;IAC5D,aAAa,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC5C,gEAAgE;IAChE,iBAAiB,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAChD,wEAAwE;IACxE,WAAW,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACzC,0EAA0E;IAC1E,gBAAgB,EAAE,OAAO,CAAC;IAC1B,2BAA2B;IAC3B,IAAI,EAAE,SAAS,CAAC;IAChB,iEAAiE;IACjE,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,mEAAmE;IACnE,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,wDAAwD;IACxD,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,gEAAgE;IAChE,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3 impact-analysis payload. Built from the persistent code graph and
|
|
3
|
+
* (when available) the rule-graph bridge. Intentionally compatible with
|
|
4
|
+
* the existing v2 payload's *core shape* (input kind, normalised
|
|
5
|
+
* targets, dependent files, packages, tests) while adding:
|
|
6
|
+
*
|
|
7
|
+
* - affectedSymbols / affectedCallerFiles — Wave 3 symbol edges.
|
|
8
|
+
* - affectedRules / affectedTemplates / affectedPaths — rule-graph bridge.
|
|
9
|
+
* - publicApiTouched — true if any normalised target is an index
|
|
10
|
+
* entrypoint or declares an exported symbol.
|
|
11
|
+
* - validationScope — exact `shrk …` commands to run.
|
|
12
|
+
*/
|
|
13
|
+
export const GRAPH_IMPACT_SCHEMA = 'sharkcraft.graph-impact-analysis/v3';
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shrkcrft/impact-engine",
|
|
3
|
+
"version": "0.1.0-alpha.10",
|
|
4
|
+
"description": "SharkCraft impact engine: graph-backed change analysis. Combines code graph + rule-graph + symbol-reference edges into a v3 impact payload (affected files, symbols, rules, templates, tests, risk).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "SharkCraft contributors",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/sharkcraft/sharkcraft.git",
|
|
25
|
+
"directory": "packages/impact-engine"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/sharkcraft/sharkcraft",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/sharkcraft/sharkcraft/issues"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"sharkcraft",
|
|
33
|
+
"impact-engine",
|
|
34
|
+
"graph",
|
|
35
|
+
"code-intelligence",
|
|
36
|
+
"blast-radius"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"bun": ">=1.1.0",
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@shrkcrft/core": "^0.1.0-alpha.10",
|
|
47
|
+
"@shrkcrft/graph": "^0.1.0-alpha.10",
|
|
48
|
+
"@shrkcrft/rule-graph": "^0.1.0-alpha.10"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|