@titan-design/code-graph 0.8.0 → 0.9.1

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
@@ -148,7 +148,7 @@ always recomputed over the whole assembled graph, so a heavily-reused run and a
148
148
 
149
149
  ## Store layout
150
150
 
151
- `openCodeGraph` opens the database with the kit's pragmas and runs three migrations.
151
+ `openCodeGraph` opens the database with the kit's pragmas and runs four migrations.
152
152
 
153
153
  It refuses a database stamped past `SCHEMA_VERSION` with `SchemaTooNewError`: a code graph
154
154
  database belongs to one build, so a higher version means a newer build already moved the
@@ -163,7 +163,8 @@ every consumer reads directly: `node.language`, `node.role`, `snapshot.commit_ha
163
163
  Domain tables in `schema.ts`: `edge` (snapshot-scoped, keyed
164
164
  `(snapshot_id, src_id, dst_id, kind)` — the kit's own edge table is bi-temporal, which is the
165
165
  wrong time model for a population re-indexed all at once), `metric`, `id_alias`, and
166
- `file_fingerprint`.
166
+ `file_fingerprint`. Migration 4 adds `finding` and `verdict`, both keyed
167
+ `(snapshot_id, key)` on a `findingKey` and pruned with their snapshot.
167
168
 
168
169
  The symbol layer is hidden by default. `listNodes` drops `symbol` nodes and `listEdges` drops
169
170
  `references` and `calls` edges unless you ask for them (`includeReferences` covers both), so a caller reasoning about module structure sees
@@ -233,7 +234,9 @@ fail a check.
233
234
  flags every symbol strictly above the 95th percentile of `symbol_body_lines` over all symbols
234
235
  that carry it, interpolated linearly between ranks. `percentile` runs from 50 to 100. The rule
235
236
  stays silent until `minSample` nodes (default 20) carry the metric. Each violation's
236
- `threshold` is the computed percentile value.
237
+ `threshold` is the computed percentile value. For sparse metrics whose percentile sits at or
238
+ near zero, `floor` flags a node only if its value also exceeds that absolute number, and
239
+ `rankNonZero: true` ranks and gates on non-zero carriers only.
237
240
 
238
241
  ```ts
239
242
  import { checkSnapshot, loadCheckRules, openCodeGraph } from "@titan-design/code-graph";
package/dist/index.d.ts CHANGED
@@ -175,8 +175,14 @@ declare const KIT: {
175
175
  * `file_fingerprint` is the reuse basis the incremental indexer diffs against.
176
176
  */
177
177
  declare const DOMAIN_DDL = "\n CREATE TABLE IF NOT EXISTS edge (\n snapshot_id INTEGER NOT NULL,\n src_id TEXT NOT NULL,\n dst_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n attrs TEXT,\n PRIMARY KEY (snapshot_id, src_id, dst_id, kind)\n );\n CREATE INDEX IF NOT EXISTS idx_edge_dst ON edge(snapshot_id, dst_id);\n CREATE INDEX IF NOT EXISTS idx_edge_kind ON edge(snapshot_id, kind);\n\n CREATE TABLE IF NOT EXISTS metric (\n snapshot_id INTEGER NOT NULL,\n node_id TEXT NOT NULL,\n name TEXT NOT NULL,\n value REAL,\n unit TEXT,\n PRIMARY KEY (snapshot_id, node_id, name)\n );\n CREATE INDEX IF NOT EXISTS idx_metric_name ON metric(snapshot_id, name);\n\n CREATE TABLE IF NOT EXISTS id_alias (\n snapshot_id INTEGER NOT NULL,\n old_id TEXT NOT NULL,\n new_id TEXT NOT NULL,\n reason TEXT NOT NULL,\n PRIMARY KEY (snapshot_id, old_id, new_id)\n );\n\n CREATE TABLE IF NOT EXISTS file_fingerprint (\n snapshot_id INTEGER NOT NULL,\n file_id TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n structural_hash TEXT,\n PRIMARY KEY (snapshot_id, file_id)\n );\n";
178
+ /**
179
+ * Stored audit findings and model verdicts. `key` is a `findingKey`, not `Finding.id`,
180
+ * because an external tool's id embeds its line number and can repeat within one audit.
181
+ * The whole `Finding` and a verdict's citations live in `attrs`.
182
+ */
183
+ declare const FINDING_DDL = "\n CREATE TABLE IF NOT EXISTS finding (\n snapshot_id INTEGER NOT NULL,\n key TEXT NOT NULL,\n finding_id TEXT NOT NULL,\n tool TEXT NOT NULL,\n signal TEXT NOT NULL,\n path TEXT NOT NULL,\n line_start INTEGER,\n line_end INTEGER,\n severity TEXT NOT NULL,\n excerpt_hash TEXT,\n attrs TEXT NOT NULL,\n PRIMARY KEY (snapshot_id, key)\n );\n CREATE INDEX IF NOT EXISTS idx_finding_path ON finding(snapshot_id, path);\n\n CREATE TABLE IF NOT EXISTS verdict (\n snapshot_id INTEGER NOT NULL,\n key TEXT NOT NULL,\n verdict TEXT NOT NULL CHECK (verdict IN ('confirmed', 'justified', 'unclear')),\n rationale TEXT NOT NULL,\n excerpt_hash TEXT NOT NULL,\n model TEXT,\n run_id TEXT,\n cost_usd REAL,\n provenance TEXT,\n control_run TEXT,\n carried_from INTEGER,\n attrs TEXT NOT NULL,\n PRIMARY KEY (snapshot_id, key)\n );\n";
178
184
  /** Every table keyed by `snapshot_id`. No DDL above declares a foreign key, so dropping a snapshot walks this list. */
179
- declare const SNAPSHOT_SCOPED_TABLES: readonly ["node", "edge", "metric", "id_alias", "file_fingerprint"];
185
+ declare const SNAPSHOT_SCOPED_TABLES: readonly ["node", "edge", "metric", "id_alias", "file_fingerprint", "finding", "verdict"];
180
186
  declare const MIGRATIONS: Migration[];
181
187
  /** The top of this package's schema. A code graph database is never shared, so this is the top of one. */
182
188
  declare const SCHEMA_VERSION: number;
@@ -1299,6 +1305,67 @@ interface ExternalDiagnostic {
1299
1305
  declare function toFindings(result: CheckResult): Finding[];
1300
1306
  declare function externalToFinding(input: ExternalDiagnostic): Finding;
1301
1307
 
1308
+ type VerdictLabel = "confirmed" | "justified" | "unclear";
1309
+ interface VerdictCitation {
1310
+ path: string;
1311
+ lineStart: number;
1312
+ lineEnd: number;
1313
+ quote: string;
1314
+ }
1315
+ /** A finding as stored against a snapshot, addressed by its `findingKey`. */
1316
+ interface StoredFinding {
1317
+ key: string;
1318
+ finding: Finding;
1319
+ /** Hash of the excerpt a reader would be shown for this finding; carry-forward compares it. */
1320
+ excerptHash?: string;
1321
+ }
1322
+ /** A model's judgement of one stored finding, after its citations were verified. */
1323
+ interface StoredVerdict {
1324
+ key: string;
1325
+ verdict: VerdictLabel;
1326
+ rationale: string;
1327
+ citations: VerdictCitation[];
1328
+ /** Hash of the excerpt the verdict judged. */
1329
+ excerptHash: string;
1330
+ model?: string;
1331
+ costUsd?: number;
1332
+ runId?: string;
1333
+ provenance?: string;
1334
+ controlRun?: "ok" | "provisional";
1335
+ dropped?: string[];
1336
+ /** The snapshot this verdict was carried from, when carry-forward copied it. */
1337
+ carriedFrom?: number;
1338
+ }
1339
+ interface FindingKeyInput {
1340
+ finding: Finding;
1341
+ /** The innermost symbol node id containing the finding; the finding's path when absent. */
1342
+ anchor?: string;
1343
+ /** The source text of the flagged lines. */
1344
+ flaggedText: string;
1345
+ excerptHash?: string;
1346
+ }
1347
+ /** Whitespace-insensitive, so reindenting or reflowing the flagged lines keeps the key. */
1348
+ declare function normalizeFlaggedText(text: string): string;
1349
+ declare function hashText(text: string): string;
1350
+ /**
1351
+ * A finding's identity across snapshots: no line number, so an inserted line above it
1352
+ * keeps the key, and a collision ordinal separates same-rule findings on the same text.
1353
+ */
1354
+ declare function findingKey(input: FindingKeyInput, ordinal?: number): string;
1355
+ /** Key a whole audit's findings, numbering collisions in source order so the result ignores input order. */
1356
+ declare function keyFindings(inputs: readonly FindingKeyInput[]): StoredFinding[];
1357
+ /** Insert or replace findings by key within one snapshot. */
1358
+ declare function saveFindings(store: CodeGraphStore, snapshotId: number, findings: readonly StoredFinding[]): void;
1359
+ declare function listFindings(store: CodeGraphStore, snapshotId: number): StoredFinding[];
1360
+ /** Insert or replace verdicts by key within one snapshot. */
1361
+ declare function saveVerdicts(store: CodeGraphStore, snapshotId: number, verdicts: readonly StoredVerdict[]): void;
1362
+ declare function listVerdicts(store: CodeGraphStore, snapshotId: number): StoredVerdict[];
1363
+ /**
1364
+ * Copy each verdict of `from` onto the finding in `to` with the same key whose excerpt hash
1365
+ * equals the one the verdict judged. A verdict `to` already holds is kept. Returns the count copied.
1366
+ */
1367
+ declare function carryForwardVerdicts(store: CodeGraphStore, from: number, to: number): number;
1368
+
1302
1369
  /** Glob when the pattern has a `*` (`**` crosses directories, `*` stays in a segment); otherwise a case-sensitive substring. */
1303
1370
  declare function patternToRegex(pattern: string): RegExp;
1304
1371
  declare function compilePatterns(patterns: readonly string[] | undefined): RegExp[];
@@ -1619,4 +1686,4 @@ declare function getConventionMap(store: CodeGraphStore, snapshotId: number, mod
1619
1686
  */
1620
1687
  declare function findConventions(store: CodeGraphStore, snapshotId: number, query: string, embedder: Embedder, model: string, opts?: FindConventionsOptions): Promise<ConventionQueryResult>;
1621
1688
 
1622
- 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, type FileFingerprint, type FindConventionsOptions, type FindSimilarOptions, type Finding, 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 SummarizeConventionsResult, type Summarizer, type SymbolConsumers, type SymbolCouplingOptions, type SymbolCouplingPair, type SymbolSpan, type TestCoverageOwnershipOptions, type TestSourceLink, type TopMetricRow, TsMorphGraphExtractor, type TsMorphGraphExtractorOptions, type UnchangedViolation, type ValidateRulesOptions, aggregateMetrics, aliasChain, annotateRoles, attributeCoverage, buildAliases, buildConventionAreas, buildEmbedText, buildFileModuleNodes, buildIndexerMetrics, buildLineage, canonicalEdgeKind, canonicalMetricName, canonicalRole, 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, getConventionMap, getEdgeWeight, groupTestsBySource, hashContent, hashEmbedText, indexPaths, invertBuckets, isGeneratedByHeuristic, isGeneratedFile, isInsideGitRepo, lineagePath, linkTestsToSources, listEdges, listEdgesTouching, listEmbeddableSymbols, listMetrics, listMetricsForNode, listNodes, loadCheckRules, loadGeneratedPatterns, loadHistoryMetrics, loadLineage, matchesAny, moduleId, openCodeGraph, packageId, parentModuleId, parseSymbolId, patternToRegex, percentileOf, planPrune, priorSnapshotForRef, pruneDanglingReferences, readSourceFiles, rebasedViolationKey, resolveAlias, resolveBarrelEdges, resolveChurnWindows, resolveGitRef, resolveSnapshot, runChecks, runPrune, snapshotPageRank, snapshotReferenceEdges, snapshotRelevance, snapshotSymbolConsumers, snapshotSymbolCoupling, snapshotViolations, structuralSignature, summarizeConventions, symbolId, testCoverageCountMetrics, toFindings, tryEmbedSnapshot, validateRules, violationKey, walkSourceFiles, windowSuffix };
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 };
package/dist/index.js CHANGED
@@ -62,11 +62,53 @@ var DOMAIN_DDL = `
62
62
  PRIMARY KEY (snapshot_id, file_id)
63
63
  );
64
64
  `;
65
- var SNAPSHOT_SCOPED_TABLES = ["node", "edge", "metric", "id_alias", "file_fingerprint"];
65
+ var FINDING_DDL = `
66
+ CREATE TABLE IF NOT EXISTS finding (
67
+ snapshot_id INTEGER NOT NULL,
68
+ key TEXT NOT NULL,
69
+ finding_id TEXT NOT NULL,
70
+ tool TEXT NOT NULL,
71
+ signal TEXT NOT NULL,
72
+ path TEXT NOT NULL,
73
+ line_start INTEGER,
74
+ line_end INTEGER,
75
+ severity TEXT NOT NULL,
76
+ excerpt_hash TEXT,
77
+ attrs TEXT NOT NULL,
78
+ PRIMARY KEY (snapshot_id, key)
79
+ );
80
+ CREATE INDEX IF NOT EXISTS idx_finding_path ON finding(snapshot_id, path);
81
+
82
+ CREATE TABLE IF NOT EXISTS verdict (
83
+ snapshot_id INTEGER NOT NULL,
84
+ key TEXT NOT NULL,
85
+ verdict TEXT NOT NULL CHECK (verdict IN ('confirmed', 'justified', 'unclear')),
86
+ rationale TEXT NOT NULL,
87
+ excerpt_hash TEXT NOT NULL,
88
+ model TEXT,
89
+ run_id TEXT,
90
+ cost_usd REAL,
91
+ provenance TEXT,
92
+ control_run TEXT,
93
+ carried_from INTEGER,
94
+ attrs TEXT NOT NULL,
95
+ PRIMARY KEY (snapshot_id, key)
96
+ );
97
+ `;
98
+ var SNAPSHOT_SCOPED_TABLES = [
99
+ "node",
100
+ "edge",
101
+ "metric",
102
+ "id_alias",
103
+ "file_fingerprint",
104
+ "finding",
105
+ "verdict"
106
+ ];
66
107
  var MIGRATIONS = [
67
108
  kitMigration(1, { snapshot: KIT.snapshot, entitySnap: KIT.entitySnap, cacheBlob: KIT.cacheBlob }),
68
109
  { version: 2, name: "code graph columns", up: (db) => db.exec(KIT_EXTENSIONS) },
69
- { version: 3, name: "code graph tables", up: (db) => db.exec(DOMAIN_DDL) }
110
+ { version: 3, name: "code graph tables", up: (db) => db.exec(DOMAIN_DDL) },
111
+ { version: 4, name: "findings and verdicts", up: (db) => db.exec(FINDING_DDL) }
70
112
  ];
71
113
  var SCHEMA_VERSION = Math.max(...MIGRATIONS.map((m) => m.version));
72
114
 
@@ -1592,6 +1634,9 @@ function isInsideGitRepo(repoRoot) {
1592
1634
  function resolveGitRef(repoRoot, ref) {
1593
1635
  return runGit(repoRoot, ["rev-parse", ref]);
1594
1636
  }
1637
+ function isGitAncestor(repoRoot, ancestor, descendant) {
1638
+ return runGit(repoRoot, ["merge-base", "--is-ancestor", ancestor, descendant]) !== null;
1639
+ }
1595
1640
  function detectRenames(options) {
1596
1641
  const threshold = options.similarityThreshold ?? 80;
1597
1642
  const args = [
@@ -1763,9 +1808,15 @@ function priorSnapshotForRef(store, ref, options = {}) {
1763
1808
  const byCommit = commit ? candidates2.find((s) => s.commitHash === commit) : void 0;
1764
1809
  return byCommit ?? candidates2.find((s) => s.ref === ref) ?? null;
1765
1810
  }
1766
- function aliasBaseFor(store, ref) {
1811
+ function aliasTargetCommit(target) {
1812
+ return target.commit ?? detectGitHead(target.repoRoot);
1813
+ }
1814
+ function aliasBaseFor(store, ref, target) {
1767
1815
  const committed = allSnapshots(store).filter((s) => s.commitHash).reverse();
1768
- return committed.find((s) => s.ref === ref) ?? committed[0] ?? null;
1816
+ const candidate = committed.find((s) => s.ref === ref) ?? committed[0];
1817
+ if (!candidate?.commitHash) return null;
1818
+ const commit = aliasTargetCommit(target);
1819
+ return commit && isGitAncestor(target.repoRoot, candidate.commitHash, commit) ? candidate : null;
1769
1820
  }
1770
1821
 
1771
1822
  // src/identity/index-aliases.ts
@@ -1786,17 +1837,17 @@ function movedSymbolAliases(store, baseSnapshotId, fileAliases, nodes) {
1786
1837
  }
1787
1838
  return out;
1788
1839
  }
1789
- function renameAliases(input, baseCommit) {
1790
- if (input.detectRenames === false || !isInsideGitRepo(input.rootDir)) return [];
1791
- const target = input.commitHash ?? detectGitHead(input.rootDir) ?? void 0;
1792
- if (target === baseCommit) return [];
1840
+ function renameAliases(input, baseCommit, target) {
1841
+ if (input.detectRenames === false || target === baseCommit) return [];
1793
1842
  const pairs = detectRenames({ repoRoot: input.rootDir, fromCommit: baseCommit, toCommit: target });
1794
1843
  return buildAliases(input.idRoot, pairs);
1795
1844
  }
1796
1845
  function computeAliasBridge(store, input) {
1797
- const base = aliasBaseFor(store, input.ref);
1798
- if (!base?.commitHash) return { baseSnapshotId: null, aliases: [] };
1799
- const fileAliases = renameAliases(input, base.commitHash);
1846
+ const target = { repoRoot: input.rootDir, commit: input.commitHash };
1847
+ const base = aliasBaseFor(store, input.ref, target);
1848
+ const commit = base ? aliasTargetCommit(target) : null;
1849
+ if (!base?.commitHash || !commit) return { baseSnapshotId: null, aliases: [] };
1850
+ const fileAliases = renameAliases(input, base.commitHash, commit);
1800
1851
  const symbols = movedSymbolAliases(store, base.id, fileAliases, input.nodes);
1801
1852
  return { baseSnapshotId: base.id, aliases: [...fileAliases, ...symbols] };
1802
1853
  }
@@ -5605,6 +5656,127 @@ function withoutUndefined(finding) {
5605
5656
  return Object.fromEntries(Object.entries(finding).filter(([, v]) => v !== void 0));
5606
5657
  }
5607
5658
 
5659
+ // src/check/finding-store.ts
5660
+ import { createHash as createHash2 } from "crypto";
5661
+ function normalizeFlaggedText(text) {
5662
+ return text.split(/\r?\n/).map((line) => line.trim().replace(/\s+/g, " ")).filter((line) => line.length > 0).join("\n");
5663
+ }
5664
+ function hashText(text) {
5665
+ return createHash2("sha256").update(text, "utf8").digest("hex").slice(0, 16);
5666
+ }
5667
+ function findingKey(input, ordinal = 0) {
5668
+ const { finding } = input;
5669
+ const anchor = input.anchor ?? finding.path;
5670
+ const textHash = hashText(normalizeFlaggedText(input.flaggedText));
5671
+ return `${finding.tool}:${finding.signal}:${anchor}:${textHash}#${ordinal}`;
5672
+ }
5673
+ function keyFindings(inputs) {
5674
+ const ordered = [...inputs].sort(compareBySource);
5675
+ const seen = /* @__PURE__ */ new Map();
5676
+ return ordered.map((input) => {
5677
+ const base = findingKey(input);
5678
+ const ordinal = seen.get(base) ?? 0;
5679
+ seen.set(base, ordinal + 1);
5680
+ const stored = { key: findingKey(input, ordinal), finding: input.finding };
5681
+ if (input.excerptHash !== void 0) stored.excerptHash = input.excerptHash;
5682
+ return stored;
5683
+ });
5684
+ }
5685
+ function compareBySource(a, b) {
5686
+ const fa = a.finding;
5687
+ const fb = b.finding;
5688
+ return fa.path.localeCompare(fb.path) || (fa.lineStart ?? 0) - (fb.lineStart ?? 0) || (fa.lineEnd ?? 0) - (fb.lineEnd ?? 0) || (fa.evidence ?? "").localeCompare(fb.evidence ?? "") || fa.id.localeCompare(fb.id);
5689
+ }
5690
+ function saveFindings(store, snapshotId, findings) {
5691
+ const insert = store.db.prepare(
5692
+ `INSERT OR REPLACE INTO finding
5693
+ (snapshot_id, key, finding_id, tool, signal, path, line_start, line_end, severity, excerpt_hash, attrs)
5694
+ VALUES (@snapshotId, @key, @findingId, @tool, @signal, @path, @lineStart, @lineEnd, @severity, @excerptHash, @attrs)`
5695
+ );
5696
+ store.db.transaction(() => {
5697
+ for (const { key, finding, excerptHash } of findings) {
5698
+ insert.run({
5699
+ snapshotId,
5700
+ key,
5701
+ findingId: finding.id,
5702
+ tool: finding.tool,
5703
+ signal: finding.signal,
5704
+ path: finding.path,
5705
+ lineStart: finding.lineStart ?? null,
5706
+ lineEnd: finding.lineEnd ?? null,
5707
+ severity: finding.severity,
5708
+ excerptHash: excerptHash ?? null,
5709
+ attrs: JSON.stringify(finding)
5710
+ });
5711
+ }
5712
+ })();
5713
+ }
5714
+ function listFindings(store, snapshotId) {
5715
+ const rows = store.db.prepare("SELECT key, excerpt_hash, attrs FROM finding WHERE snapshot_id = ? ORDER BY key").all(snapshotId);
5716
+ return rows.map((row) => {
5717
+ const stored = { key: row.key, finding: JSON.parse(row.attrs) };
5718
+ if (row.excerpt_hash !== null) stored.excerptHash = row.excerpt_hash;
5719
+ return stored;
5720
+ });
5721
+ }
5722
+ function saveVerdicts(store, snapshotId, verdicts) {
5723
+ const insert = store.db.prepare(
5724
+ `INSERT OR REPLACE INTO verdict
5725
+ (snapshot_id, key, verdict, rationale, excerpt_hash, model, run_id, cost_usd, provenance, control_run,
5726
+ carried_from, attrs)
5727
+ VALUES (@snapshotId, @key, @verdict, @rationale, @excerptHash, @model, @runId, @costUsd, @provenance,
5728
+ @controlRun, @carriedFrom, @attrs)`
5729
+ );
5730
+ store.db.transaction(() => {
5731
+ for (const v of verdicts) insert.run(verdictParams(snapshotId, v));
5732
+ })();
5733
+ }
5734
+ function verdictParams(snapshotId, v) {
5735
+ return {
5736
+ snapshotId,
5737
+ key: v.key,
5738
+ verdict: v.verdict,
5739
+ rationale: v.rationale,
5740
+ excerptHash: v.excerptHash,
5741
+ model: v.model ?? null,
5742
+ runId: v.runId ?? null,
5743
+ costUsd: v.costUsd ?? null,
5744
+ provenance: v.provenance ?? null,
5745
+ controlRun: v.controlRun ?? null,
5746
+ carriedFrom: v.carriedFrom ?? null,
5747
+ attrs: JSON.stringify({ citations: v.citations, dropped: v.dropped ?? [] })
5748
+ };
5749
+ }
5750
+ function listVerdicts(store, snapshotId) {
5751
+ const rows = store.db.prepare("SELECT * FROM verdict WHERE snapshot_id = ? ORDER BY key").all(snapshotId);
5752
+ return rows.map(rowToVerdict);
5753
+ }
5754
+ function rowToVerdict(row) {
5755
+ const attrs = JSON.parse(row.attrs);
5756
+ const verdict = {
5757
+ key: row.key,
5758
+ verdict: row.verdict,
5759
+ rationale: row.rationale,
5760
+ citations: attrs.citations,
5761
+ excerptHash: row.excerpt_hash
5762
+ };
5763
+ if (row.model !== null) verdict.model = row.model;
5764
+ if (row.cost_usd !== null) verdict.costUsd = row.cost_usd;
5765
+ if (row.run_id !== null) verdict.runId = row.run_id;
5766
+ if (row.provenance !== null) verdict.provenance = row.provenance;
5767
+ if (row.control_run !== null) verdict.controlRun = row.control_run;
5768
+ if (attrs.dropped.length > 0) verdict.dropped = attrs.dropped;
5769
+ if (row.carried_from !== null) verdict.carriedFrom = row.carried_from;
5770
+ return verdict;
5771
+ }
5772
+ function carryForwardVerdicts(store, from, to) {
5773
+ const targets = new Map(listFindings(store, to).map((f) => [f.key, f.excerptHash]));
5774
+ const existing = new Set(listVerdicts(store, to).map((v) => v.key));
5775
+ const carried = listVerdicts(store, from).filter((v) => !existing.has(v.key) && targets.get(v.key) === v.excerptHash).map((v) => ({ ...v, carriedFrom: v.carriedFrom ?? from }));
5776
+ saveVerdicts(store, to, carried);
5777
+ return carried.length;
5778
+ }
5779
+
5608
5780
  // src/check/validate.ts
5609
5781
  function validateRules(input, options = {}) {
5610
5782
  if (!input || typeof input !== "object") {
@@ -6489,6 +6661,7 @@ export {
6489
6661
  DEFAULT_OUTLIER_MIN_SAMPLE,
6490
6662
  DOMAIN_DDL,
6491
6663
  EXCEPTION_METRIC_NAMES,
6664
+ FINDING_DDL,
6492
6665
  GROWTH_RISK_METRIC_NAMES,
6493
6666
  INDEX_VERSION,
6494
6667
  KIT,
@@ -6516,6 +6689,7 @@ export {
6516
6689
  canonicalEdgeKind,
6517
6690
  canonicalMetricName,
6518
6691
  canonicalRole,
6692
+ carryForwardVerdicts,
6519
6693
  checkSnapshot,
6520
6694
  classifyRole,
6521
6695
  collectDeclaredNames,
@@ -6552,6 +6726,7 @@ export {
6552
6726
  fileId,
6553
6727
  findConventions,
6554
6728
  findSimilarCapability,
6729
+ findingKey,
6555
6730
  getConventionMap,
6556
6731
  getEdgeWeight,
6557
6732
  getLanguageFromPath3 as getLanguageFromPath,
@@ -6559,25 +6734,30 @@ export {
6559
6734
  groupTestsBySource,
6560
6735
  hashContent,
6561
6736
  hashEmbedText,
6737
+ hashText,
6562
6738
  indexPaths,
6563
6739
  invertBuckets,
6564
6740
  isGeneratedByHeuristic,
6565
6741
  isGeneratedFile,
6566
6742
  isInsideGitRepo,
6743
+ keyFindings,
6567
6744
  lineagePath,
6568
6745
  linkTestsToSources,
6569
6746
  listEdges,
6570
6747
  listEdgesTouching,
6571
6748
  listEmbeddableSymbols,
6749
+ listFindings,
6572
6750
  listMetrics,
6573
6751
  listMetricsForNode,
6574
6752
  listNodes,
6753
+ listVerdicts,
6575
6754
  loadCheckRules,
6576
6755
  loadGeneratedPatterns,
6577
6756
  loadHistoryMetrics,
6578
6757
  loadLineage,
6579
6758
  matchesAny,
6580
6759
  moduleId,
6760
+ normalizeFlaggedText,
6581
6761
  openCodeGraph,
6582
6762
  packageId,
6583
6763
  parentModuleId,
@@ -6597,6 +6777,8 @@ export {
6597
6777
  resolveSnapshot,
6598
6778
  runChecks,
6599
6779
  runPrune,
6780
+ saveFindings,
6781
+ saveVerdicts,
6600
6782
  shouldIncludeFile2 as shouldIncludeFile,
6601
6783
  snapshotPageRank,
6602
6784
  snapshotReferenceEdges,