@shrkcrft/impact-engine 0.1.0-alpha.22 → 0.1.0-alpha.24

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":"AAAA,OAAO,EAEL,aAAa,EAId,MAAM,iBAAiB,CAAC;AAMzB,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;AA8FD,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"}
@@ -1,6 +1,6 @@
1
- import { execSync } from 'node:child_process';
2
1
  import { EdgeKind, GraphQueryApi, GraphStore, NodeKind, } from '@shrkcrft/graph';
3
2
  import { BridgeStore, RuleGraphQueryApi, } from '@shrkcrft/rule-graph';
3
+ import { runGitLines } from '@shrkcrft/shared';
4
4
  import { GRAPH_IMPACT_SCHEMA, } from "../schema/impact-analysis.js";
5
5
  import { classifyRisk } from "./risk-score.js";
6
6
  /**
@@ -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,
@@ -349,18 +356,115 @@ function missingGraphPayload(input, diagnostics) {
349
356
  };
350
357
  }
351
358
  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 [];
359
+ // Shell-free + high-maxBuffer: a large changeset no longer ENOBUFS-crashes
360
+ // the gitref impact analysis. Failure [] (caller degrades cleanly).
361
+ return runGitLines(projectRoot, ['diff', '--name-only', ref]).lines;
362
+ }
363
+ /** Schema id for a {@link findDeletedOrphans} report. */
364
+ export const DELETED_ORPHANS_SCHEMA = 'sharkcraft.deleted-orphans/v1';
365
+ /** Read the import-statement line carried on an `imports-file` edge. */
366
+ function importEdgeLine(data) {
367
+ const v = data?.['line'];
368
+ return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
369
+ }
370
+ /**
371
+ * Reverse-of-impact query: given files DELETED in the working tree, find the
372
+ * surviving files — in the CURRENT index snapshot — that still import them or
373
+ * still reference a symbol they declared. Each such file is an orphaned
374
+ * importer whose build would break once the delete lands.
375
+ *
376
+ * Two edge classes are walked:
377
+ * - `imports-file` into the deleted file → direct importers (this also
378
+ * surfaces a barrel that `export … from` the deleted file).
379
+ * - references/calls to symbols the deleted file declared → consumers that
380
+ * reach the symbol through a package barrel re-export. The indexer already
381
+ * rewrites those reference edges to the REAL declaring symbol id
382
+ * (`resolveReExportedReferenceEdges`), so a cross-package consumer of a
383
+ * re-exported deleted symbol is caught here, not just the barrel.
384
+ *
385
+ * Assumption: the graph is the pre-edit snapshot. A not-yet-reindexed delete
386
+ * still carries its inbound edges, so the deleted file node + its symbols are
387
+ * present and queryable. Once `shrk graph index` re-runs, those nodes vanish
388
+ * and `unresolvedDeleted` grows instead — surface a diagnostic, never throw.
389
+ * Importers that are themselves in the deleted set are excluded (they are
390
+ * going away too).
391
+ */
392
+ export function findDeletedOrphans(graph, deletedFiles) {
393
+ const deletedSet = new Set(deletedFiles);
394
+ const resolvedDeleted = [];
395
+ const unresolvedDeleted = [];
396
+ const diagnostics = [];
397
+ const orphans = [];
398
+ const seen = new Set();
399
+ const pushOrphan = (o) => {
400
+ const key = `${o.id}|${o.via}|${o.deletedFile}|${o.symbol ?? ''}`;
401
+ if (seen.has(key))
402
+ return;
403
+ seen.add(key);
404
+ orphans.push(o);
405
+ };
406
+ for (const rel of deletedFiles) {
407
+ const fileNode = graph.findFile(rel);
408
+ if (!fileNode) {
409
+ unresolvedDeleted.push(rel);
410
+ diagnostics.push(`deleted file not in index (already reindexed?): ${rel}`);
411
+ continue;
412
+ }
413
+ resolvedDeleted.push(rel);
414
+ // 1) Direct importers of the deleted file (incl. barrels re-exporting it).
415
+ const neighbours = graph.neighbours(fileNode.id);
416
+ if (neighbours) {
417
+ for (const inEdge of neighbours.in) {
418
+ if (inEdge.edge.kind !== EdgeKind.ImportsFile)
419
+ continue;
420
+ const source = inEdge.source;
421
+ if (!('kind' in source))
422
+ continue; // unresolved placeholder — no node
423
+ if (source.path && deletedSet.has(source.path))
424
+ continue; // importer also deleted
425
+ pushOrphan({
426
+ id: source.id,
427
+ ...(source.path ? { path: source.path } : {}),
428
+ label: source.label,
429
+ ...(importEdgeLine(inEdge.edge.data) ? { line: importEdgeLine(inEdge.edge.data) } : {}),
430
+ via: 'import',
431
+ deletedFile: rel,
432
+ });
433
+ }
434
+ }
435
+ // 2) Surviving references/calls to symbols the deleted file declared.
436
+ for (const sym of graph.symbolsIn(fileNode.id)) {
437
+ for (const site of graph.referenceSitesOf(sym.id)) {
438
+ if (site.node.path && deletedSet.has(site.node.path))
439
+ continue; // caller also deleted
440
+ pushOrphan({
441
+ id: site.node.id,
442
+ ...(site.node.path ? { path: site.node.path } : {}),
443
+ label: site.node.label,
444
+ ...(typeof site.line === 'number' ? { line: site.line } : {}),
445
+ via: 'reference',
446
+ deletedFile: rel,
447
+ symbol: sym.label,
448
+ });
449
+ }
450
+ }
365
451
  }
452
+ orphans.sort((a, b) => {
453
+ const pa = a.path ?? a.id;
454
+ const pb = b.path ?? b.id;
455
+ if (pa !== pb)
456
+ return pa.localeCompare(pb);
457
+ if ((a.line ?? 0) !== (b.line ?? 0))
458
+ return (a.line ?? 0) - (b.line ?? 0);
459
+ if (a.via !== b.via)
460
+ return a.via.localeCompare(b.via);
461
+ return (a.symbol ?? '').localeCompare(b.symbol ?? '');
462
+ });
463
+ return {
464
+ schema: DELETED_ORPHANS_SCHEMA,
465
+ resolvedDeleted,
466
+ unresolvedDeleted,
467
+ orphans,
468
+ diagnostics,
469
+ };
366
470
  }
@@ -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.24",
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,10 @@
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.24",
48
+ "@shrkcrft/graph": "^0.1.0-alpha.24",
49
+ "@shrkcrft/rule-graph": "^0.1.0-alpha.24",
50
+ "@shrkcrft/shared": "^0.1.0-alpha.24"
50
51
  },
51
52
  "publishConfig": {
52
53
  "access": "public"