@shrkcrft/impact-engine 0.1.0-alpha.21 → 0.1.0-alpha.23
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.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { GraphQueryApi } from '@shrkcrft/graph';
|
|
1
2
|
import { type IGraphImpactAnalysis } from '../schema/impact-analysis.js';
|
|
2
3
|
export type IGraphImpactInput = {
|
|
3
4
|
kind: 'files';
|
|
@@ -26,4 +27,58 @@ export interface IAnalyzeOptions {
|
|
|
26
27
|
* with a diagnostic, treated as zero-dependent
|
|
27
28
|
*/
|
|
28
29
|
export declare function analyzeGraphImpact(input: IGraphImpactInput, options: IAnalyzeOptions): IGraphImpactAnalysis;
|
|
30
|
+
/** Schema id for a {@link findDeletedOrphans} report. */
|
|
31
|
+
export declare const DELETED_ORPHANS_SCHEMA: "sharkcraft.deleted-orphans/v1";
|
|
32
|
+
/** A surviving file that still imports / references now-deleted code. */
|
|
33
|
+
export interface IDeletedOrphan {
|
|
34
|
+
/** Graph node id of the surviving importer / caller file. */
|
|
35
|
+
id: string;
|
|
36
|
+
/** Project-relative path of the surviving file. */
|
|
37
|
+
path?: string;
|
|
38
|
+
/** Display label (basename / node label). */
|
|
39
|
+
label: string;
|
|
40
|
+
/** 1-based source line of the import statement / use site, when known. */
|
|
41
|
+
line?: number;
|
|
42
|
+
/** How the orphan still reaches the deleted code. */
|
|
43
|
+
via: 'import' | 'reference';
|
|
44
|
+
/** The deleted file path the orphan still points at. */
|
|
45
|
+
deletedFile: string;
|
|
46
|
+
/** When `via === 'reference'`, the deleted symbol that is still referenced. */
|
|
47
|
+
symbol?: string;
|
|
48
|
+
}
|
|
49
|
+
/** Result of a {@link findDeletedOrphans} query. */
|
|
50
|
+
export interface IDeletedOrphanReport {
|
|
51
|
+
schema: typeof DELETED_ORPHANS_SCHEMA;
|
|
52
|
+
/** Deleted paths that resolved to a node in the current index snapshot. */
|
|
53
|
+
resolvedDeleted: readonly string[];
|
|
54
|
+
/** Deleted paths NOT in the index (already reindexed, or never indexed). */
|
|
55
|
+
unresolvedDeleted: readonly string[];
|
|
56
|
+
/** Surviving files that still import / reference the deleted files or their symbols. */
|
|
57
|
+
orphans: readonly IDeletedOrphan[];
|
|
58
|
+
/** Free-form diagnostics. */
|
|
59
|
+
diagnostics: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Reverse-of-impact query: given files DELETED in the working tree, find the
|
|
63
|
+
* surviving files — in the CURRENT index snapshot — that still import them or
|
|
64
|
+
* still reference a symbol they declared. Each such file is an orphaned
|
|
65
|
+
* importer whose build would break once the delete lands.
|
|
66
|
+
*
|
|
67
|
+
* Two edge classes are walked:
|
|
68
|
+
* - `imports-file` into the deleted file → direct importers (this also
|
|
69
|
+
* surfaces a barrel that `export … from` the deleted file).
|
|
70
|
+
* - references/calls to symbols the deleted file declared → consumers that
|
|
71
|
+
* reach the symbol through a package barrel re-export. The indexer already
|
|
72
|
+
* rewrites those reference edges to the REAL declaring symbol id
|
|
73
|
+
* (`resolveReExportedReferenceEdges`), so a cross-package consumer of a
|
|
74
|
+
* re-exported deleted symbol is caught here, not just the barrel.
|
|
75
|
+
*
|
|
76
|
+
* Assumption: the graph is the pre-edit snapshot. A not-yet-reindexed delete
|
|
77
|
+
* still carries its inbound edges, so the deleted file node + its symbols are
|
|
78
|
+
* present and queryable. Once `shrk graph index` re-runs, those nodes vanish
|
|
79
|
+
* and `unresolvedDeleted` grows instead — surface a diagnostic, never throw.
|
|
80
|
+
* Importers that are themselves in the deleted set are excluded (they are
|
|
81
|
+
* going away too).
|
|
82
|
+
*/
|
|
83
|
+
export declare function findDeletedOrphans(graph: GraphQueryApi, deletedFiles: readonly string[]): IDeletedOrphanReport;
|
|
29
84
|
//# sourceMappingURL=analyzer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../src/engine/analyzer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../src/engine/analyzer.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,aAAa,EAId,MAAM,iBAAiB,CAAC;AAKzB,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,CAsPtB;AAwGD,yDAAyD;AACzD,eAAO,MAAM,sBAAsB,EAAG,+BAAwC,CAAC;AAE/E,yEAAyE;AACzE,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,EAAE,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,GAAG,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC5B,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,oDAAoD;AACpD,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,sBAAsB,CAAC;IACtC,2EAA2E;IAC3E,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,4EAA4E;IAC5E,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,wFAAwF;IACxF,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC,6BAA6B;IAC7B,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAQD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,aAAa,EACpB,YAAY,EAAE,SAAS,MAAM,EAAE,GAC9B,oBAAoB,CA4EtB"}
|
package/dist/engine/analyzer.js
CHANGED
|
@@ -144,10 +144,17 @@ export function analyzeGraphImpact(input, options) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
// True blast radius BEFORE the display cap. The displayed list is sliced to
|
|
148
|
+
// `limit`, but risk must reflect the uncapped caller-file count — otherwise a
|
|
149
|
+
// 5000-caller change and a 200-caller change score identically.
|
|
150
|
+
const callerFilesTotal = callerSet.size;
|
|
147
151
|
const affectedCallerFiles = [...callerSet]
|
|
148
152
|
.map((id) => toRef(api, id))
|
|
149
153
|
.filter((r) => r !== undefined)
|
|
150
154
|
.slice(0, limit);
|
|
155
|
+
if (callerFilesTotal > limit) {
|
|
156
|
+
truncations['callers'] = callerFilesTotal - limit;
|
|
157
|
+
}
|
|
151
158
|
// Affected packages.
|
|
152
159
|
const packagesSet = new Set();
|
|
153
160
|
for (const id of reachable) {
|
|
@@ -241,7 +248,7 @@ export function analyzeGraphImpact(input, options) {
|
|
|
241
248
|
rulesTouched: affectedRules.length,
|
|
242
249
|
templatesTouched: affectedTemplates.length,
|
|
243
250
|
publicApiTouched,
|
|
244
|
-
callerFilesCount:
|
|
251
|
+
callerFilesCount: callerFilesTotal,
|
|
245
252
|
});
|
|
246
253
|
const validationScope = deriveValidationScope({
|
|
247
254
|
risk,
|
|
@@ -364,3 +371,111 @@ function changedFilesSince(projectRoot, ref) {
|
|
|
364
371
|
return [];
|
|
365
372
|
}
|
|
366
373
|
}
|
|
374
|
+
/** Schema id for a {@link findDeletedOrphans} report. */
|
|
375
|
+
export const DELETED_ORPHANS_SCHEMA = 'sharkcraft.deleted-orphans/v1';
|
|
376
|
+
/** Read the import-statement line carried on an `imports-file` edge. */
|
|
377
|
+
function importEdgeLine(data) {
|
|
378
|
+
const v = data?.['line'];
|
|
379
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Reverse-of-impact query: given files DELETED in the working tree, find the
|
|
383
|
+
* surviving files — in the CURRENT index snapshot — that still import them or
|
|
384
|
+
* still reference a symbol they declared. Each such file is an orphaned
|
|
385
|
+
* importer whose build would break once the delete lands.
|
|
386
|
+
*
|
|
387
|
+
* Two edge classes are walked:
|
|
388
|
+
* - `imports-file` into the deleted file → direct importers (this also
|
|
389
|
+
* surfaces a barrel that `export … from` the deleted file).
|
|
390
|
+
* - references/calls to symbols the deleted file declared → consumers that
|
|
391
|
+
* reach the symbol through a package barrel re-export. The indexer already
|
|
392
|
+
* rewrites those reference edges to the REAL declaring symbol id
|
|
393
|
+
* (`resolveReExportedReferenceEdges`), so a cross-package consumer of a
|
|
394
|
+
* re-exported deleted symbol is caught here, not just the barrel.
|
|
395
|
+
*
|
|
396
|
+
* Assumption: the graph is the pre-edit snapshot. A not-yet-reindexed delete
|
|
397
|
+
* still carries its inbound edges, so the deleted file node + its symbols are
|
|
398
|
+
* present and queryable. Once `shrk graph index` re-runs, those nodes vanish
|
|
399
|
+
* and `unresolvedDeleted` grows instead — surface a diagnostic, never throw.
|
|
400
|
+
* Importers that are themselves in the deleted set are excluded (they are
|
|
401
|
+
* going away too).
|
|
402
|
+
*/
|
|
403
|
+
export function findDeletedOrphans(graph, deletedFiles) {
|
|
404
|
+
const deletedSet = new Set(deletedFiles);
|
|
405
|
+
const resolvedDeleted = [];
|
|
406
|
+
const unresolvedDeleted = [];
|
|
407
|
+
const diagnostics = [];
|
|
408
|
+
const orphans = [];
|
|
409
|
+
const seen = new Set();
|
|
410
|
+
const pushOrphan = (o) => {
|
|
411
|
+
const key = `${o.id}|${o.via}|${o.deletedFile}|${o.symbol ?? ''}`;
|
|
412
|
+
if (seen.has(key))
|
|
413
|
+
return;
|
|
414
|
+
seen.add(key);
|
|
415
|
+
orphans.push(o);
|
|
416
|
+
};
|
|
417
|
+
for (const rel of deletedFiles) {
|
|
418
|
+
const fileNode = graph.findFile(rel);
|
|
419
|
+
if (!fileNode) {
|
|
420
|
+
unresolvedDeleted.push(rel);
|
|
421
|
+
diagnostics.push(`deleted file not in index (already reindexed?): ${rel}`);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
resolvedDeleted.push(rel);
|
|
425
|
+
// 1) Direct importers of the deleted file (incl. barrels re-exporting it).
|
|
426
|
+
const neighbours = graph.neighbours(fileNode.id);
|
|
427
|
+
if (neighbours) {
|
|
428
|
+
for (const inEdge of neighbours.in) {
|
|
429
|
+
if (inEdge.edge.kind !== EdgeKind.ImportsFile)
|
|
430
|
+
continue;
|
|
431
|
+
const source = inEdge.source;
|
|
432
|
+
if (!('kind' in source))
|
|
433
|
+
continue; // unresolved placeholder — no node
|
|
434
|
+
if (source.path && deletedSet.has(source.path))
|
|
435
|
+
continue; // importer also deleted
|
|
436
|
+
pushOrphan({
|
|
437
|
+
id: source.id,
|
|
438
|
+
...(source.path ? { path: source.path } : {}),
|
|
439
|
+
label: source.label,
|
|
440
|
+
...(importEdgeLine(inEdge.edge.data) ? { line: importEdgeLine(inEdge.edge.data) } : {}),
|
|
441
|
+
via: 'import',
|
|
442
|
+
deletedFile: rel,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// 2) Surviving references/calls to symbols the deleted file declared.
|
|
447
|
+
for (const sym of graph.symbolsIn(fileNode.id)) {
|
|
448
|
+
for (const site of graph.referenceSitesOf(sym.id)) {
|
|
449
|
+
if (site.node.path && deletedSet.has(site.node.path))
|
|
450
|
+
continue; // caller also deleted
|
|
451
|
+
pushOrphan({
|
|
452
|
+
id: site.node.id,
|
|
453
|
+
...(site.node.path ? { path: site.node.path } : {}),
|
|
454
|
+
label: site.node.label,
|
|
455
|
+
...(typeof site.line === 'number' ? { line: site.line } : {}),
|
|
456
|
+
via: 'reference',
|
|
457
|
+
deletedFile: rel,
|
|
458
|
+
symbol: sym.label,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
orphans.sort((a, b) => {
|
|
464
|
+
const pa = a.path ?? a.id;
|
|
465
|
+
const pb = b.path ?? b.id;
|
|
466
|
+
if (pa !== pb)
|
|
467
|
+
return pa.localeCompare(pb);
|
|
468
|
+
if ((a.line ?? 0) !== (b.line ?? 0))
|
|
469
|
+
return (a.line ?? 0) - (b.line ?? 0);
|
|
470
|
+
if (a.via !== b.via)
|
|
471
|
+
return a.via.localeCompare(b.via);
|
|
472
|
+
return (a.symbol ?? '').localeCompare(b.symbol ?? '');
|
|
473
|
+
});
|
|
474
|
+
return {
|
|
475
|
+
schema: DELETED_ORPHANS_SCHEMA,
|
|
476
|
+
resolvedDeleted,
|
|
477
|
+
unresolvedDeleted,
|
|
478
|
+
orphans,
|
|
479
|
+
diagnostics,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RiskLevel } from '../schema/impact-analysis.js';
|
|
2
2
|
interface IRiskInputs {
|
|
3
3
|
directCount: number;
|
|
4
4
|
transitiveCount: number;
|
|
@@ -25,6 +25,4 @@ export declare function classifyRisk(input: IRiskInputs): {
|
|
|
25
25
|
};
|
|
26
26
|
/** Re-export so callers can read the (partial) inputs back for tests. */
|
|
27
27
|
export type { IRiskInputs };
|
|
28
|
-
/** Defensive constructor used by analyzer tests. */
|
|
29
|
-
export declare function emptyImpactAnalysis(): Partial<IGraphImpactAnalysis>;
|
|
30
28
|
//# sourceMappingURL=risk-score.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"risk-score.d.ts","sourceRoot":"","sources":["../../src/engine/risk-score.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"risk-score.d.ts","sourceRoot":"","sources":["../../src/engine/risk-score.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAE9D,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"}
|
|
@@ -55,12 +55,3 @@ export function classifyRisk(input) {
|
|
|
55
55
|
const risk = score >= 8 ? 'critical' : score >= 5 ? 'high' : score >= 2 ? 'medium' : 'low';
|
|
56
56
|
return { risk, reasons };
|
|
57
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shrkcrft/impact-engine",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.23",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"author": "SharkCraft contributors",
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@shrkcrft/core": "^0.1.0-alpha.
|
|
48
|
-
"@shrkcrft/graph": "^0.1.0-alpha.
|
|
49
|
-
"@shrkcrft/rule-graph": "^0.1.0-alpha.
|
|
47
|
+
"@shrkcrft/core": "^0.1.0-alpha.23",
|
|
48
|
+
"@shrkcrft/graph": "^0.1.0-alpha.23",
|
|
49
|
+
"@shrkcrft/rule-graph": "^0.1.0-alpha.23"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|