@titan-design/code-graph 0.10.0 → 0.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 CHANGED
@@ -281,6 +281,12 @@ file is dropped from coupling too. `symbolSetHash({ unitId, symbolIds }, footpri
281
281
  doc unit's sorted `[symbolId, footprint.hash]` pairs, so it ignores member order and changes when
282
282
  a member is added, removed or changed.
283
283
 
284
+ `diffFootprints(store, { fromSnapshotId, toSnapshotId, ...options })` compares the footprints of
285
+ two snapshots. From-side ids follow the alias chain first, and a symbol under a moved file follows
286
+ its file, so a move reads as `changed` with `reasons: ["renamed"]` alone when its footprint held.
287
+ Each other change is `added`, `removed`, or `changed` with the differing parts (`signature`,
288
+ `consumers`, `coupling`). `files` rolls the changes up to their declaring files, never to consumers.
289
+
284
290
  `scripts/dag-check-self.mjs` in the repo root runs this repo's DAG check on this engine
285
291
  instead of codewatch's CLI.
286
292
 
package/dist/index.d.ts CHANGED
@@ -307,8 +307,12 @@ declare class TsMorphGraphExtractor implements Extractor<GraphFragment> {
307
307
  private readonly repoRoot;
308
308
  private readonly tsConfigPath?;
309
309
  private project?;
310
+ private readonly ownsProject;
311
+ private readonly retired;
310
312
  constructor(options: TsMorphGraphExtractorOptions);
311
313
  extract(file: ParsedFile): GraphFragment[];
314
+ private retire;
315
+ private extractFrom;
312
316
  private ensureProject;
313
317
  private loadSourceFile;
314
318
  private buildFileAndModuleNodes;
@@ -1423,6 +1427,27 @@ interface GraphDiffSummary {
1423
1427
  removedEdges: number;
1424
1428
  metricChanges: number;
1425
1429
  }
1430
+ /** Why a symbol present in both snapshots changed; `renamed` alone means its footprint held. */
1431
+ type FootprintChangeReason = "signature" | "consumers" | "coupling" | "renamed";
1432
+ interface FootprintChange {
1433
+ /** The to-snapshot id; a removed symbol keeps its from-snapshot id. */
1434
+ symbolId: string;
1435
+ /** The from-snapshot id, set only when it differs from `symbolId`. */
1436
+ previousId?: string;
1437
+ status: "added" | "removed" | "changed";
1438
+ /** Empty for an added or removed symbol. */
1439
+ reasons: FootprintChangeReason[];
1440
+ /** The declaring file, in the to-snapshot's id space where it still exists there. */
1441
+ fileId: string;
1442
+ }
1443
+ interface FootprintDiff {
1444
+ fromSnapshotId: number;
1445
+ toSnapshotId: number;
1446
+ /** Sorted by `symbolId`; an unchanged symbol is absent. */
1447
+ changes: FootprintChange[];
1448
+ /** Sorted distinct declaring files of `changes`; consumer files never appear here. */
1449
+ files: string[];
1450
+ }
1426
1451
  interface GraphDiff {
1427
1452
  summary: GraphDiffSummary;
1428
1453
  addedNodes: GraphNode[];
@@ -1511,6 +1536,56 @@ declare function computeFootprints(graph: FootprintGraph, options?: FootprintOpt
1511
1536
  */
1512
1537
  declare function symbolSetHash(unit: FootprintUnit, footprints: ReadonlyMap<string, SymbolFootprint>): string;
1513
1538
 
1539
+ interface DiffFootprintsOptions extends FootprintOptions {
1540
+ fromSnapshotId: number;
1541
+ toSnapshotId: number;
1542
+ }
1543
+ /**
1544
+ * Symbols whose documentable footprint differs between two snapshots. From-snapshot ids
1545
+ * follow the alias chain first, so a moved file reads as `renamed`, not a remove plus an add.
1546
+ */
1547
+ declare function diffFootprints(store: CodeGraphStore, options: DiffFootprintsOptions): FootprintDiff;
1548
+
1549
+ /**
1550
+ * What a consumer generated a unit's doc from. Provenance lives with the consumer, not the
1551
+ * store, so it survives snapshot prune.
1552
+ */
1553
+ interface UnitProvenance {
1554
+ unitId: string;
1555
+ /** The to-snapshot's commit hash; null for a working-tree snapshot. */
1556
+ commit: string | null;
1557
+ symbolSetHash: string;
1558
+ /** The generator the consumer used; the gate records it but never calls one. */
1559
+ model: string | null;
1560
+ }
1561
+ interface UnitProvenanceInput {
1562
+ unit: FootprintUnit;
1563
+ footprints: ReadonlyMap<string, SymbolFootprint>;
1564
+ snapshot: Pick<SnapshotRow, "commitHash">;
1565
+ model?: string | null;
1566
+ }
1567
+ type RegenerateReason = "new" | "changed";
1568
+ interface UnitRegeneration {
1569
+ unitId: string;
1570
+ reason: RegenerateReason;
1571
+ }
1572
+ interface GateUnitsInput {
1573
+ prior: readonly UnitProvenance[];
1574
+ units: readonly FootprintUnit[];
1575
+ footprints: ReadonlyMap<string, SymbolFootprint>;
1576
+ }
1577
+ /** An empty `regenerate` means the caller makes no LLM call. */
1578
+ interface GateResult {
1579
+ regenerate: UnitRegeneration[];
1580
+ skip: string[];
1581
+ /** Prior units that no longer exist. */
1582
+ orphaned: string[];
1583
+ }
1584
+ /** A fresh provenance record for one unit at the given snapshot. */
1585
+ declare function unitProvenance(input: UnitProvenanceInput): UnitProvenance;
1586
+ /** Splits units into those whose symbol-set hash moved since the prior record and those that did not. */
1587
+ declare function gateUnits(input: GateUnitsInput): GateResult;
1588
+
1514
1589
  interface EmbeddableSymbol {
1515
1590
  id: string;
1516
1591
  name: string;
@@ -1727,4 +1802,4 @@ declare function getConventionMap(store: CodeGraphStore, snapshotId: number, mod
1727
1802
  */
1728
1803
  declare function findConventions(store: CodeGraphStore, snapshotId: number, query: string, embedder: Embedder, model: string, opts?: FindConventionsOptions): Promise<ConventionQueryResult>;
1729
1804
 
1730
- export { ALIAS_BASE_ATTR, ALL_ROLES, type AliasChain, type AliasChainInput, type AliasChainOptions, type AliasLoader, type AliasResolution, COMMUNITY_SUMMARY_NAMESPACE, COVERAGE_METRIC_NAME, type CachedEmbedResult, type CheckDiff, type CheckResult, type CheckRule, type CheckRulesFile, type CheckSnapshotOptions, type CheckSnapshotResult, type CheckViolation, CodeGraphStore, type ConventionArea, type ConventionCorpus, type ConventionCoverage, type ConventionMap, type ConventionMatch, type ConventionOptions, type ConventionQueryResult, type ConventionSymbol, DEAD_CODE_METRIC_NAMES, DEFAULT_CHURN_WINDOWS, DEFAULT_OUTLIER_MIN_SAMPLE, DOMAIN_DDL, type DeepAst, type DeepAstInput, type DiffCheckResultsOptions, type DiffSnapshotsOptions, EXCEPTION_METRIC_NAMES, type EdgeKind, type EmbedAttempt, type EmbedCoverage, type EmbedSnapshotResult, type EmbeddableSymbol, type ExternalDiagnostic, FINDING_DDL, type FileFingerprint, type FindConventionsOptions, type FindSimilarOptions, type Finding, type FindingKeyInput, type FootprintGraph, type FootprintOptions, type FootprintParts, type FootprintUnit, type ForbidImportRule, GROWTH_RISK_METRIC_NAMES, type GraphDiff, type GraphDiffSummary, type GraphEdge, type GraphFragment, type GraphMetric, type GraphNode, type HistoryMetricsOptions, INDEX_VERSION, type IdAlias, type IdAliasReason, type IndexOptions, type IndexResult, type IstanbulCoverage, KIT, LanguageExtractor, type LanguageExtractorOptions, type LayeredDepsRule, type LineSpan, type Lineage, type LineageSnapshot, type LineageStep, type LinkMethod, type LinkTestsOptions, type LoadedHistory, METRIC_CATALOGUE, MIGRATIONS, type MemberInfo, type MetricAbsence, type MetricAggregate, type MetricDelta, type MetricDescriptor, type MetricDirection, type MetricMaxRule, type MetricMinRule, type MetricOutlierRule, type MetricProductMaxRule, type MetricRollup, type MetricSource, type MetricUnit, type NoInternalOnlyBarrelsRule, type NodeKind, type NodeRename, type NodeRole, type PackageFlag, type PackageLayer, type PackageStats, type PageRankOptions, type PageRankResult, type PageRankRow, type PairCoupling, type PairFlag, type ParamInfo, type PartitionQualityInput, type PartitionQualityResult, type PriorSnapshotOptions, type PruneOptions, type PrunePlan, type PruneResult, PythonGraphExtractor, type ReadFile, type ReferenceEdgeLite, type RelevanceOptions, type ResolveAliasOptions, type ReuseBasis, type RuleStore, type RunChecksOptions, SCHEMA_VERSION, SNAPSHOT_SCOPED_TABLES, SOURCE_METRIC_NAMES, SYMBOL_EMBEDDING_NAMESPACE, SYMBOL_ID_SEP, SYMBOL_METRIC_NAMES, type Severity, type SimilarCandidate, type SimilarResult, type SnapshotInsert, type SnapshotRow, type SnapshotSpec, type SourceLanguage, type StoredFinding, type StoredVerdict, type SummarizeConventionsResult, type Summarizer, type SymbolConsumers, type SymbolCouplingOptions, type SymbolCouplingPair, type SymbolFootprint, type SymbolSpan, type TestCoverageOwnershipOptions, type TestSourceLink, type TopMetricRow, TsMorphGraphExtractor, type TsMorphGraphExtractorOptions, type UnchangedViolation, type ValidateRulesOptions, type VerdictCitation, type VerdictLabel, aggregateMetrics, aliasChain, annotateRoles, attributeCoverage, buildAliases, buildConventionAreas, buildEmbedText, buildFileModuleNodes, buildIndexerMetrics, buildLineage, canonicalEdgeKind, canonicalMetricName, canonicalRole, carryForwardVerdicts, checkSnapshot, classifyRole, collectDeclaredNames, collectDeclaredSpans, compilePatterns, computeDeadCodeMetrics, computeDeepAst, computeFootprints, computeGrowthRiskMetrics, computeMetrics, computePageRank, computePartitionQuality, computeRecencyWindows, computeRelevance, computeRoleHints, computeSourceMetrics, computeSymbolConsumers, computeSymbolCoupling, computeTestCoverageOwnership, createAliasChain, defaultTargetCount, describeMetric, describeMetrics, detectCommunities, detectGitHead, detectGitToplevel, detectRenames, diffCheckResults, diffSnapshots, edgeWeight, embedSnapshot, embedTextsCached, externalId, externalToFinding, fileId, findConventions, findSimilarCapability, findingKey, getConventionMap, getEdgeWeight, groupTestsBySource, hashContent, hashEmbedText, hashText, indexPaths, invertBuckets, isGeneratedByHeuristic, isGeneratedFile, isInsideGitRepo, keyFindings, lineagePath, linkTestsToSources, listEdges, listEdgesTouching, listEmbeddableSymbols, listFindings, listMetrics, listMetricsForNode, listNodes, listVerdicts, loadCheckRules, loadGeneratedPatterns, loadHistoryMetrics, loadLineage, matchesAny, moduleId, normalizeFlaggedText, openCodeGraph, packageId, parentModuleId, parseSymbolId, patternToRegex, percentileOf, planPrune, priorSnapshotForRef, pruneDanglingReferences, readSourceFiles, rebasedViolationKey, resolveAlias, resolveBarrelEdges, resolveChurnWindows, resolveGitRef, resolveSnapshot, runChecks, runPrune, saveFindings, saveVerdicts, snapshotPageRank, snapshotReferenceEdges, snapshotRelevance, snapshotSymbolConsumers, snapshotSymbolCoupling, snapshotViolations, structuralSignature, summarizeConventions, symbolId, symbolSetHash, testCoverageCountMetrics, toFindings, tryEmbedSnapshot, validateRules, violationKey, walkSourceFiles, windowSuffix };
1805
+ export { ALIAS_BASE_ATTR, ALL_ROLES, type AliasChain, type AliasChainInput, type AliasChainOptions, type AliasLoader, type AliasResolution, COMMUNITY_SUMMARY_NAMESPACE, COVERAGE_METRIC_NAME, type CachedEmbedResult, type CheckDiff, type CheckResult, type CheckRule, type CheckRulesFile, type CheckSnapshotOptions, type CheckSnapshotResult, type CheckViolation, CodeGraphStore, type ConventionArea, type ConventionCorpus, type ConventionCoverage, type ConventionMap, type ConventionMatch, type ConventionOptions, type ConventionQueryResult, type ConventionSymbol, DEAD_CODE_METRIC_NAMES, DEFAULT_CHURN_WINDOWS, DEFAULT_OUTLIER_MIN_SAMPLE, DOMAIN_DDL, type DeepAst, type DeepAstInput, type DiffCheckResultsOptions, type DiffFootprintsOptions, type DiffSnapshotsOptions, EXCEPTION_METRIC_NAMES, type EdgeKind, type EmbedAttempt, type EmbedCoverage, type EmbedSnapshotResult, type EmbeddableSymbol, type ExternalDiagnostic, FINDING_DDL, type FileFingerprint, type FindConventionsOptions, type FindSimilarOptions, type Finding, type FindingKeyInput, type FootprintChange, type FootprintChangeReason, type FootprintDiff, type FootprintGraph, type FootprintOptions, type FootprintParts, type FootprintUnit, type ForbidImportRule, GROWTH_RISK_METRIC_NAMES, type GateResult, type GateUnitsInput, type GraphDiff, type GraphDiffSummary, type GraphEdge, type GraphFragment, type GraphMetric, type GraphNode, type HistoryMetricsOptions, INDEX_VERSION, type IdAlias, type IdAliasReason, type IndexOptions, type IndexResult, type IstanbulCoverage, KIT, LanguageExtractor, type LanguageExtractorOptions, type LayeredDepsRule, type LineSpan, type Lineage, type LineageSnapshot, type LineageStep, type LinkMethod, type LinkTestsOptions, type LoadedHistory, METRIC_CATALOGUE, MIGRATIONS, type MemberInfo, type MetricAbsence, type MetricAggregate, type MetricDelta, type MetricDescriptor, type MetricDirection, type MetricMaxRule, type MetricMinRule, type MetricOutlierRule, type MetricProductMaxRule, type MetricRollup, type MetricSource, type MetricUnit, type NoInternalOnlyBarrelsRule, type NodeKind, type NodeRename, type NodeRole, type PackageFlag, type PackageLayer, type PackageStats, type PageRankOptions, type PageRankResult, type PageRankRow, type PairCoupling, type PairFlag, type ParamInfo, type PartitionQualityInput, type PartitionQualityResult, type PriorSnapshotOptions, type PruneOptions, type PrunePlan, type PruneResult, PythonGraphExtractor, type ReadFile, type ReferenceEdgeLite, type RegenerateReason, type RelevanceOptions, type ResolveAliasOptions, type ReuseBasis, type RuleStore, type RunChecksOptions, SCHEMA_VERSION, SNAPSHOT_SCOPED_TABLES, SOURCE_METRIC_NAMES, SYMBOL_EMBEDDING_NAMESPACE, SYMBOL_ID_SEP, SYMBOL_METRIC_NAMES, type Severity, type SimilarCandidate, type SimilarResult, type SnapshotInsert, type SnapshotRow, type SnapshotSpec, type SourceLanguage, type StoredFinding, type StoredVerdict, type SummarizeConventionsResult, type Summarizer, type SymbolConsumers, type SymbolCouplingOptions, type SymbolCouplingPair, type SymbolFootprint, type SymbolSpan, type TestCoverageOwnershipOptions, type TestSourceLink, type TopMetricRow, TsMorphGraphExtractor, type TsMorphGraphExtractorOptions, type UnchangedViolation, type UnitProvenance, type UnitProvenanceInput, type UnitRegeneration, type ValidateRulesOptions, type VerdictCitation, type VerdictLabel, aggregateMetrics, aliasChain, annotateRoles, attributeCoverage, buildAliases, buildConventionAreas, buildEmbedText, buildFileModuleNodes, buildIndexerMetrics, buildLineage, canonicalEdgeKind, canonicalMetricName, canonicalRole, carryForwardVerdicts, checkSnapshot, classifyRole, collectDeclaredNames, collectDeclaredSpans, compilePatterns, computeDeadCodeMetrics, computeDeepAst, computeFootprints, computeGrowthRiskMetrics, computeMetrics, computePageRank, computePartitionQuality, computeRecencyWindows, computeRelevance, computeRoleHints, computeSourceMetrics, computeSymbolConsumers, computeSymbolCoupling, computeTestCoverageOwnership, createAliasChain, defaultTargetCount, describeMetric, describeMetrics, detectCommunities, detectGitHead, detectGitToplevel, detectRenames, diffCheckResults, diffFootprints, diffSnapshots, edgeWeight, embedSnapshot, embedTextsCached, externalId, externalToFinding, fileId, findConventions, findSimilarCapability, findingKey, gateUnits, getConventionMap, getEdgeWeight, groupTestsBySource, hashContent, hashEmbedText, hashText, indexPaths, invertBuckets, isGeneratedByHeuristic, isGeneratedFile, isInsideGitRepo, keyFindings, lineagePath, linkTestsToSources, listEdges, listEdgesTouching, listEmbeddableSymbols, listFindings, listMetrics, listMetricsForNode, listNodes, listVerdicts, loadCheckRules, loadGeneratedPatterns, loadHistoryMetrics, loadLineage, matchesAny, moduleId, normalizeFlaggedText, openCodeGraph, packageId, parentModuleId, parseSymbolId, patternToRegex, percentileOf, planPrune, priorSnapshotForRef, pruneDanglingReferences, readSourceFiles, rebasedViolationKey, resolveAlias, resolveBarrelEdges, resolveChurnWindows, resolveGitRef, resolveSnapshot, runChecks, runPrune, saveFindings, saveVerdicts, snapshotPageRank, snapshotReferenceEdges, snapshotRelevance, snapshotSymbolConsumers, snapshotSymbolCoupling, snapshotViolations, structuralSignature, summarizeConventions, symbolId, symbolSetHash, testCoverageCountMetrics, toFindings, tryEmbedSnapshot, unitProvenance, validateRules, violationKey, walkSourceFiles, windowSuffix };
package/dist/index.js CHANGED
@@ -1223,20 +1223,36 @@ function literalText(arg) {
1223
1223
  }
1224
1224
 
1225
1225
  // src/extractors/ts-morph-extractor.ts
1226
+ var RETIRE_BATCH = 100;
1226
1227
  var TsMorphGraphExtractor = class {
1227
1228
  name = "ts-morph-graph";
1228
1229
  repoRoot;
1229
1230
  tsConfigPath;
1230
1231
  project;
1232
+ ownsProject;
1233
+ retired = [];
1231
1234
  constructor(options) {
1232
1235
  this.repoRoot = options.repoRoot;
1233
1236
  this.tsConfigPath = options.tsConfigPath;
1234
1237
  this.project = options.project;
1238
+ this.ownsProject = options.project === void 0;
1235
1239
  }
1236
1240
  extract(file) {
1237
1241
  if (!isTypeScriptFile(file)) return [];
1238
1242
  const project = this.ensureProject();
1239
1243
  const sourceFile = this.loadSourceFile(project, file);
1244
+ try {
1245
+ return this.extractFrom(sourceFile, file);
1246
+ } finally {
1247
+ if (this.ownsProject) this.retire(project, sourceFile);
1248
+ }
1249
+ }
1250
+ retire(project, sourceFile) {
1251
+ this.retired.push(sourceFile);
1252
+ if (this.retired.length < RETIRE_BATCH) return;
1253
+ for (const sf of this.retired.splice(0)) project.removeSourceFile(sf);
1254
+ }
1255
+ extractFrom(sourceFile, file) {
1240
1256
  const nodes = this.buildFileAndModuleNodes(sourceFile);
1241
1257
  const symbolNodes2 = buildSymbolNodes(this.repoRoot, sourceFile, file);
1242
1258
  const { edges, externalNodes } = this.collectEdges(sourceFile);
@@ -3461,16 +3477,16 @@ function targetFileId(edge) {
3461
3477
  }
3462
3478
  function computeDeltaAffected(basis, currentFileIds) {
3463
3479
  const affected = /* @__PURE__ */ new Set();
3464
- const removed = /* @__PURE__ */ new Set();
3480
+ const removed2 = /* @__PURE__ */ new Set();
3465
3481
  for (const id of basis.fingerprints.keys()) {
3466
- if (!currentFileIds.has(id)) removed.add(id);
3482
+ if (!currentFileIds.has(id)) removed2.add(id);
3467
3483
  }
3468
- const added = currentFileIds.size !== basis.fingerprints.size || removed.size > 0;
3484
+ const added = currentFileIds.size !== basis.fingerprints.size || removed2.size > 0;
3469
3485
  if (!added) return affected;
3470
3486
  for (const [srcId, edges] of basis.edgesBySrc) {
3471
3487
  if (!currentFileIds.has(srcId)) continue;
3472
3488
  for (const edge of edges) {
3473
- if (isEdgeAffected(edge, srcId, removed, currentFileIds)) {
3489
+ if (isEdgeAffected(edge, srcId, removed2, currentFileIds)) {
3474
3490
  affected.add(srcId);
3475
3491
  break;
3476
3492
  }
@@ -3478,8 +3494,8 @@ function computeDeltaAffected(basis, currentFileIds) {
3478
3494
  }
3479
3495
  return affected;
3480
3496
  }
3481
- function isEdgeAffected(edge, srcId, removed, currentFileIds) {
3482
- if (removed.has(targetFileId(edge))) return true;
3497
+ function isEdgeAffected(edge, srcId, removed2, currentFileIds) {
3498
+ if (removed2.has(targetFileId(edge))) return true;
3483
3499
  const specifier = edge.attrs?.specifier;
3484
3500
  if (!specifier || !isRelative(specifier)) return false;
3485
3501
  return resolveRelativeId(srcId, specifier, currentFileIds) !== targetFileId(edge);
@@ -6071,13 +6087,13 @@ function indexEdges(edges) {
6071
6087
  function diffNodes(fromNodes, toNodes, aliases) {
6072
6088
  const toIndex = new Map(toNodes.map((n) => [n.id, n]));
6073
6089
  const matchedToIds = /* @__PURE__ */ new Set();
6074
- const removed = [];
6090
+ const removed2 = [];
6075
6091
  const renamed = [];
6076
6092
  for (const fromNode of fromNodes) {
6077
6093
  const newId = resolveId(fromNode.id, aliases);
6078
6094
  const toNode = toIndex.get(newId);
6079
6095
  if (!toNode) {
6080
- removed.push(fromNode);
6096
+ removed2.push(fromNode);
6081
6097
  continue;
6082
6098
  }
6083
6099
  matchedToIds.add(newId);
@@ -6086,7 +6102,7 @@ function diffNodes(fromNodes, toNodes, aliases) {
6086
6102
  }
6087
6103
  }
6088
6104
  const added = toNodes.filter((toNode) => !matchedToIds.has(toNode.id));
6089
- return { added, removed, renamed, commonNewIds: matchedToIds };
6105
+ return { added, removed: removed2, renamed, commonNewIds: matchedToIds };
6090
6106
  }
6091
6107
  function diffEdges(fromEdges, toEdges, aliases) {
6092
6108
  const remappedKey = (e) => edgeKey2(resolveId(e.srcId, aliases), resolveId(e.dstId, aliases), e.kind);
@@ -6096,8 +6112,8 @@ function diffEdges(fromEdges, toEdges, aliases) {
6096
6112
  for (const [k, e] of toIndex) {
6097
6113
  if (!fromKeys.has(k)) added.push(e);
6098
6114
  }
6099
- const removed = fromEdges.filter((e) => !toIndex.has(remappedKey(e)));
6100
- return { added, removed };
6115
+ const removed2 = fromEdges.filter((e) => !toIndex.has(remappedKey(e)));
6116
+ return { added, removed: removed2 };
6101
6117
  }
6102
6118
  function indexFromMetrics(metrics, aliases) {
6103
6119
  const fromIndex = /* @__PURE__ */ new Map();
@@ -6264,6 +6280,119 @@ function partnersBySymbol(edges, minCoImports) {
6264
6280
  return new Map([...partners].map(([id, set]) => [id, [...set].sort()]));
6265
6281
  }
6266
6282
 
6283
+ // src/diff/footprint-diff.ts
6284
+ var PART_REASONS = ["signature", "consumers", "coupling"];
6285
+ function diffFootprints(store, options) {
6286
+ const { fromSnapshotId, toSnapshotId } = options;
6287
+ const resolve4 = symbolAwareResolver(aliasChain(store, fromSnapshotId, toSnapshotId));
6288
+ const fromGraph = loadSymbolLayer(store, fromSnapshotId);
6289
+ const toGraph = loadSymbolLayer(store, toSnapshotId);
6290
+ const { previousIds, mergedAway } = mergeSources(fromGraph.nodes, resolve4);
6291
+ const before = computeFootprints(remapGraph(fromGraph, resolve4, new Set(mergedAway)), options);
6292
+ const after = computeFootprints(toGraph, options);
6293
+ const changes = [
6294
+ ...changedOrAdded(after, before, previousIds),
6295
+ ...removed(before, after, previousIds, resolve4),
6296
+ ...mergedAway.map((id) => ({ symbolId: id, status: "removed", reasons: [], fileId: resolve4(declaringFile(id)) }))
6297
+ ].sort((a, b) => compare2(a.symbolId, b.symbolId));
6298
+ const files = [...new Set(changes.map((c) => c.fileId))].sort(compare2);
6299
+ return { fromSnapshotId, toSnapshotId, changes, files };
6300
+ }
6301
+ function loadSymbolLayer(store, snapshotId) {
6302
+ return {
6303
+ nodes: store.listNodes(snapshotId, { includeSymbols: true }).filter((n) => n.kind === "symbol"),
6304
+ edges: store.listEdges(snapshotId, { includeReferences: true })
6305
+ };
6306
+ }
6307
+ function symbolAwareResolver(aliases) {
6308
+ return (id) => {
6309
+ const direct = aliases.resolve(id);
6310
+ if (direct !== id) return direct;
6311
+ const parsed = parseSymbolId(id);
6312
+ return parsed ? symbolId(aliases.resolve(parsed.fileId), parsed.name) : id;
6313
+ };
6314
+ }
6315
+ function remapGraph(graph, resolve4, losers) {
6316
+ return {
6317
+ nodes: graph.nodes.filter((n) => !losers.has(n.id)).map((n) => ({ ...n, id: resolve4(n.id) })),
6318
+ edges: graph.edges.filter((e) => !losers.has(e.srcId) && !losers.has(e.dstId)).map((e) => ({ ...e, srcId: resolve4(e.srcId), dstId: resolve4(e.dstId) }))
6319
+ };
6320
+ }
6321
+ function mergeSources(nodes, resolve4) {
6322
+ const groups = /* @__PURE__ */ new Map();
6323
+ for (const node of nodes) {
6324
+ const id = resolve4(node.id);
6325
+ groups.set(id, [...groups.get(id) ?? [], node.id]);
6326
+ }
6327
+ const previousIds = /* @__PURE__ */ new Map();
6328
+ const mergedAway = [];
6329
+ for (const [id, sources] of groups) {
6330
+ const keeper = sources.includes(id) ? id : [...sources].sort(compare2)[0];
6331
+ if (keeper !== id) previousIds.set(id, keeper);
6332
+ mergedAway.push(...sources.filter((source) => source !== keeper));
6333
+ }
6334
+ return { previousIds, mergedAway };
6335
+ }
6336
+ function changedOrAdded(after, before, previousIds) {
6337
+ const out = [];
6338
+ for (const [id, footprint] of after) {
6339
+ const prior = before.get(id);
6340
+ const previousId = previousIds.get(id);
6341
+ if (!prior) {
6342
+ out.push({ symbolId: id, status: "added", reasons: [], fileId: declaringFile(id) });
6343
+ continue;
6344
+ }
6345
+ const reasons = reasonsFor(prior, footprint, previousId !== void 0);
6346
+ if (reasons.length === 0) continue;
6347
+ out.push({ symbolId: id, ...previousId ? { previousId } : {}, status: "changed", reasons, fileId: declaringFile(id) });
6348
+ }
6349
+ return out;
6350
+ }
6351
+ function reasonsFor(prior, current, renamed) {
6352
+ const reasons = PART_REASONS.filter((part) => prior.parts[part] !== current.parts[part]);
6353
+ if (renamed) reasons.push("renamed");
6354
+ return reasons;
6355
+ }
6356
+ function removed(before, after, previousIds, resolve4) {
6357
+ const out = [];
6358
+ for (const id of before.keys()) {
6359
+ if (after.has(id)) continue;
6360
+ const fromId = previousIds.get(id) ?? id;
6361
+ out.push({ symbolId: fromId, status: "removed", reasons: [], fileId: resolve4(declaringFile(fromId)) });
6362
+ }
6363
+ return out;
6364
+ }
6365
+ function declaringFile(id) {
6366
+ return parseSymbolId(id)?.fileId ?? id;
6367
+ }
6368
+ function compare2(a, b) {
6369
+ return a < b ? -1 : a > b ? 1 : 0;
6370
+ }
6371
+
6372
+ // src/diff/gate.ts
6373
+ function unitProvenance(input) {
6374
+ return {
6375
+ unitId: input.unit.unitId,
6376
+ commit: input.snapshot.commitHash,
6377
+ symbolSetHash: symbolSetHash(input.unit, input.footprints),
6378
+ model: input.model ?? null
6379
+ };
6380
+ }
6381
+ function gateUnits(input) {
6382
+ const priorHash = new Map(input.prior.map((p) => [p.unitId, p.symbolSetHash]));
6383
+ const current = new Set(input.units.map((u) => u.unitId));
6384
+ const regenerate = [];
6385
+ const skip = [];
6386
+ for (const unit of input.units) {
6387
+ const before = priorHash.get(unit.unitId);
6388
+ if (before === void 0) regenerate.push({ unitId: unit.unitId, reason: "new" });
6389
+ else if (before !== symbolSetHash(unit, input.footprints)) regenerate.push({ unitId: unit.unitId, reason: "changed" });
6390
+ else skip.push(unit.unitId);
6391
+ }
6392
+ const orphaned = [...priorHash.keys()].filter((id) => !current.has(id));
6393
+ return { regenerate, skip, orphaned };
6394
+ }
6395
+
6267
6396
  // src/embeddings/corpus.ts
6268
6397
  import { contentHashOf } from "@titan-design/store-sqlite";
6269
6398
  function buildEmbedText(signature, purpose) {
@@ -6783,6 +6912,7 @@ export {
6783
6912
  detectGitToplevel,
6784
6913
  detectRenames,
6785
6914
  diffCheckResults,
6915
+ diffFootprints,
6786
6916
  diffSnapshots,
6787
6917
  edgeWeight,
6788
6918
  embedSnapshot,
@@ -6793,6 +6923,7 @@ export {
6793
6923
  findConventions,
6794
6924
  findSimilarCapability,
6795
6925
  findingKey,
6926
+ gateUnits,
6796
6927
  getConventionMap,
6797
6928
  getEdgeWeight,
6798
6929
  getLanguageFromPath3 as getLanguageFromPath,
@@ -6859,6 +6990,7 @@ export {
6859
6990
  testCoverageCountMetrics,
6860
6991
  toFindings,
6861
6992
  tryEmbedSnapshot,
6993
+ unitProvenance,
6862
6994
  validateRules,
6863
6995
  violationKey,
6864
6996
  walkSourceFiles,