@shrkcrft/impact-engine 0.1.0-alpha.22 → 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":"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"}
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"}
@@ -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: affectedCallerFiles.length,
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 { IGraphImpactAnalysis, RiskLevel } from '../schema/impact-analysis.js';
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,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"}
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.22",
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.22",
48
- "@shrkcrft/graph": "^0.1.0-alpha.22",
49
- "@shrkcrft/rule-graph": "^0.1.0-alpha.22"
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"