@titan-design/code-graph 0.9.2 → 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
@@ -271,6 +271,22 @@ snapshots and buckets each violation as new, resolved, or unchanged; unchanged m
271
271
  violations are further split into worsened and improved by value. From-side ids follow the
272
272
  alias chain, as in the ratchet.
273
273
 
274
+ `computeFootprints({ nodes, edges }, options)` gives each symbol node a footprint built from
275
+ structural inputs only, so a moved declaration or a churn window never reads as a change. It
276
+ has three parts, each a `hashText` over canonical JSON: `signature` (name, exported, signature,
277
+ purpose; no span), `consumers` (sorted distinct files with a `references` edge into the symbol),
278
+ and `coupling` (sorted co-import partners at or above `minCoImports`, default 2). `hash` combines
279
+ the three. Consumers include test files unless `ignoreConsumer(fileId)` rejects them; a rejected
280
+ file is dropped from coupling too. `symbolSetHash({ unitId, symbolIds }, footprints)` hashes a
281
+ doc unit's sorted `[symbolId, footprint.hash]` pairs, so it ignores member order and changes when
282
+ a member is added, removed or changed.
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
+
274
290
  `scripts/dag-check-self.mjs` in the repo root runs this repo's DAG check on this engine
275
291
  instead of codewatch's CLI.
276
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[];
@@ -1470,6 +1495,97 @@ interface DiffCheckResultsOptions {
1470
1495
  */
1471
1496
  declare function diffCheckResults(store: CodeGraphStore, options: DiffCheckResultsOptions): CheckDiff;
1472
1497
 
1498
+ /**
1499
+ * A symbol's documentable footprint (C-75): structural inputs only, so a moved declaration,
1500
+ * a churn window or a centrality shift never reads as a change.
1501
+ */
1502
+ interface FootprintParts {
1503
+ /** Hash of `{name, exported, signature, purpose}`; no span. */
1504
+ signature: string;
1505
+ /** Hash of the sorted distinct file ids that reference the symbol. */
1506
+ consumers: string;
1507
+ /** Hash of the sorted co-import partner symbol ids at or above `minCoImports`. */
1508
+ coupling: string;
1509
+ }
1510
+ interface SymbolFootprint {
1511
+ symbolId: string;
1512
+ parts: FootprintParts;
1513
+ /** Hash of the three part hashes. */
1514
+ hash: string;
1515
+ }
1516
+ interface FootprintGraph {
1517
+ nodes: readonly GraphNode[];
1518
+ edges: readonly GraphEdge[];
1519
+ }
1520
+ interface FootprintOptions {
1521
+ /** Drop a referencing file from consumers and co-import coupling, e.g. test files. Default keeps all. */
1522
+ ignoreConsumer?: (fileId: string) => boolean;
1523
+ /** Co-import threshold for a coupling partner. Default 2. */
1524
+ minCoImports?: number;
1525
+ }
1526
+ /** A doc target: the default unit is one file holding its symbols. */
1527
+ interface FootprintUnit {
1528
+ unitId: string;
1529
+ symbolIds: readonly string[];
1530
+ }
1531
+ /** Footprint of every symbol node in the graph, keyed by symbol id. */
1532
+ declare function computeFootprints(graph: FootprintGraph, options?: FootprintOptions): Map<string, SymbolFootprint>;
1533
+ /**
1534
+ * Order-independent hash of a unit's `[symbolId, footprint.hash]` pairs. A member with no
1535
+ * footprint hashes as null, so a symbol that disappears still changes the set.
1536
+ */
1537
+ declare function symbolSetHash(unit: FootprintUnit, footprints: ReadonlyMap<string, SymbolFootprint>): string;
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
+
1473
1589
  interface EmbeddableSymbol {
1474
1590
  id: string;
1475
1591
  name: string;
@@ -1686,4 +1802,4 @@ declare function getConventionMap(store: CodeGraphStore, snapshotId: number, mod
1686
1802
  */
1687
1803
  declare function findConventions(store: CodeGraphStore, snapshotId: number, query: string, embedder: Embedder, model: string, opts?: FindConventionsOptions): Promise<ConventionQueryResult>;
1688
1804
 
1689
- 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 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 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, 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, 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();
@@ -6207,6 +6223,176 @@ function computeDelta(from, to) {
6207
6223
  return to.value - from.value;
6208
6224
  }
6209
6225
 
6226
+ // src/diff/footprint.ts
6227
+ var DEFAULT_MIN_CO_IMPORTS2 = 2;
6228
+ function computeFootprints(graph, options = {}) {
6229
+ const edges = referenceEdges(graph.edges, options.ignoreConsumer);
6230
+ const consumers = consumersBySymbol(edges);
6231
+ const partners = partnersBySymbol(edges, options.minCoImports ?? DEFAULT_MIN_CO_IMPORTS2);
6232
+ const out = /* @__PURE__ */ new Map();
6233
+ for (const node of graph.nodes) {
6234
+ if (node.kind !== "symbol") continue;
6235
+ const parts = {
6236
+ signature: hashJson(signatureOf2(node)),
6237
+ consumers: hashJson(consumers.get(node.id) ?? []),
6238
+ coupling: hashJson(partners.get(node.id) ?? [])
6239
+ };
6240
+ const hash = hashJson([parts.signature, parts.consumers, parts.coupling]);
6241
+ out.set(node.id, { symbolId: node.id, parts, hash });
6242
+ }
6243
+ return out;
6244
+ }
6245
+ function symbolSetHash(unit, footprints) {
6246
+ const pairs = [...new Set(unit.symbolIds)].sort().map((id) => [id, footprints.get(id)?.hash ?? null]);
6247
+ return hashJson(pairs);
6248
+ }
6249
+ function hashJson(value) {
6250
+ return hashText(JSON.stringify(value));
6251
+ }
6252
+ function signatureOf2(node) {
6253
+ const attrs = node.attrs ?? {};
6254
+ return {
6255
+ name: node.name,
6256
+ exported: attrs.exported === true,
6257
+ signature: typeof attrs.signature === "string" ? attrs.signature : null,
6258
+ purpose: typeof attrs.purpose === "string" ? attrs.purpose : null
6259
+ };
6260
+ }
6261
+ function referenceEdges(edges, ignoreConsumer) {
6262
+ return edges.filter(
6263
+ (e) => canonicalEdgeKind(e.kind) === "references" && !ignoreConsumer?.(e.srcId)
6264
+ );
6265
+ }
6266
+ function consumersBySymbol(edges) {
6267
+ return new Map(computeSymbolConsumers(edges).map((c) => [c.symbolId, c.consumers]));
6268
+ }
6269
+ function partnersBySymbol(edges, minCoImports) {
6270
+ const partners = /* @__PURE__ */ new Map();
6271
+ const add = (from, to) => {
6272
+ const set = partners.get(from) ?? /* @__PURE__ */ new Set();
6273
+ set.add(to);
6274
+ partners.set(from, set);
6275
+ };
6276
+ for (const pair of computeSymbolCoupling(edges, { minCoImports })) {
6277
+ add(pair.aId, pair.bId);
6278
+ add(pair.bId, pair.aId);
6279
+ }
6280
+ return new Map([...partners].map(([id, set]) => [id, [...set].sort()]));
6281
+ }
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
+
6210
6396
  // src/embeddings/corpus.ts
6211
6397
  import { contentHashOf } from "@titan-design/store-sqlite";
6212
6398
  function buildEmbedText(signature, purpose) {
@@ -6705,6 +6891,7 @@ export {
6705
6891
  compilePatterns,
6706
6892
  computeDeadCodeMetrics,
6707
6893
  computeDeepAst,
6894
+ computeFootprints,
6708
6895
  computeGrowthRiskMetrics,
6709
6896
  computeMetrics,
6710
6897
  computePageRank,
@@ -6725,6 +6912,7 @@ export {
6725
6912
  detectGitToplevel,
6726
6913
  detectRenames,
6727
6914
  diffCheckResults,
6915
+ diffFootprints,
6728
6916
  diffSnapshots,
6729
6917
  edgeWeight,
6730
6918
  embedSnapshot,
@@ -6735,6 +6923,7 @@ export {
6735
6923
  findConventions,
6736
6924
  findSimilarCapability,
6737
6925
  findingKey,
6926
+ gateUnits,
6738
6927
  getConventionMap,
6739
6928
  getEdgeWeight,
6740
6929
  getLanguageFromPath3 as getLanguageFromPath,
@@ -6797,9 +6986,11 @@ export {
6797
6986
  structuralSignature,
6798
6987
  summarizeConventions,
6799
6988
  symbolId,
6989
+ symbolSetHash,
6800
6990
  testCoverageCountMetrics,
6801
6991
  toFindings,
6802
6992
  tryEmbedSnapshot,
6993
+ unitProvenance,
6803
6994
  validateRules,
6804
6995
  violationKey,
6805
6996
  walkSourceFiles,