@titan-design/code-graph 0.3.0 → 0.4.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
@@ -44,10 +44,13 @@ Ported with TP-250: package partition quality (`src/analysis/partition-quality.t
44
44
  snapshot pruning (`src/prune.ts`). See [Partition quality](#partition-quality) and
45
45
  [Pruning snapshots](#pruning-snapshots).
46
46
 
47
- Deferred, all of it still in codewatch, all of it a follow-up on this package rather than a
48
- change to it:
47
+ Ported with TP-130: community detection and the convention layer (`src/conventions/`). See
48
+ [Conventions](#conventions). codewatch keeps the CLI command, the `claude -p` summarizer, and
49
+ the MCP and read-API wiring.
49
50
 
50
- - Graph analyses over a finished snapshot: communities, conventions, reuse-delta reporting.
51
+ Deferred, still in codewatch, a follow-up on this package rather than a change to it:
52
+
53
+ - Reuse-delta reporting over a finished snapshot.
51
54
 
52
55
  Python support is new here rather than ported. codewatch walked TypeScript only; the parser
53
56
  already had the grammar. The Python extractor is deliberately narrower than the ts-morph one:
@@ -364,6 +367,43 @@ The domain tables declare no foreign key, so `CodeGraphStore.deleteSnapshots` cl
364
367
  `SNAPSHOT_SCOPED_TABLES` itself rather than relying on a cascade. `blob_cache` is
365
368
  content-addressed and not snapshot-scoped, so a prune never drops a cached embedding.
366
369
 
370
+ ## Conventions
371
+
372
+ "How does this repo do X, and where does code like this belong?" Ported from codewatch's
373
+ unmerged C-88 branch (TP-130). The layer cuts the barrel-resolved file graph into a few coarse
374
+ areas, has an injected summarizer describe each one, and ranks areas against a question by
375
+ embedding similarity. Verified against this release on this repo's `packages/`, with a fake
376
+ summarizer:
377
+
378
+ ```ts
379
+ import { findConventions, getConventionMap, summarizeConventions } from "@titan-design/code-graph";
380
+
381
+ const summarizer = { model: "claude:sonnet", summarize: (prompt: string) => callYourLlm(prompt) };
382
+ await summarizeConventions(store, snapshotId, summarizer);
383
+ // { coverage: { files: 713, grouped: 513, areas: 24, summarized: 24 }, newlySummarized: 24, reused: 0, … }
384
+ // a second run: { newlySummarized: 0, reused: 24 }
385
+
386
+ getConventionMap(store, snapshotId, "claude:sonnet"); // the same areas, stored summaries only
387
+ await findConventions(store, snapshotId, "how are CLI commands registered?", embedder, "claude:sonnet");
388
+ // { matches: [ { label, summary, files: [ …up to 5 ], size, score }, … up to 3 ] }
389
+ ```
390
+
391
+ - **The cut.** `detectCommunities` is greedy modularity (Clauset-Newman-Moore), not Leiden. It
392
+ is deterministic without a seed: ties resolve by sorted id. `targetCount` keeps merging past
393
+ the natural modularity stop until that many communities remain, and a size cap of twice the
394
+ ideal share keeps a dense repo from collapsing into one area. The default target is one area
395
+ per 25 files, clamped to 6..40. Areas under `minSize` (default 3) files are left unsummarized.
396
+ Disconnected components never merge, so the component count is the floor.
397
+ - **Only the coarse level is summarized.** LLM cost is one call per area, and each summary is
398
+ stored in `blob_cache` under `code-graph/community-summary`, keyed by `summarizer.model` and a
399
+ hash of the prompt. The prompt carries the member files and their key exported signatures, so
400
+ an area whose membership and signatures did not change is a cache hit in any snapshot. On
401
+ this repo a finer cut (`targetCount: 60`) still reused 8 of its 35 areas.
402
+ - **The package ships no LLM client.** `Summarizer` is `{ model, summarize(prompt) }`; the
403
+ product supplies it. `getConventionMap` and `findConventions` never call it. `findConventions`
404
+ throws when no summary is stored for the model, and returns candidates with scores, not
405
+ verdicts. Summary vectors go through the same embedding cache as similar symbols.
406
+
367
407
  ## Git history
368
408
 
369
409
  Ported in TP-126, strictly as codewatch had it: churn over rolling windows, first-seen dates,
package/dist/index.d.ts CHANGED
@@ -1432,4 +1432,124 @@ declare function tryEmbedSnapshot(store: CodeGraphStore, snapshotId: number, emb
1432
1432
  */
1433
1433
  declare function findSimilarCapability(store: CodeGraphStore, snapshotId: number, query: string, embedder: Embedder, opts?: FindSimilarOptions): Promise<SimilarResult>;
1434
1434
 
1435
- export { ALIAS_BASE_ATTR, ALL_ROLES, type AliasChain, type AliasChainInput, type AliasChainOptions, type AliasLoader, type AliasResolution, COVERAGE_METRIC_NAME, type CachedEmbedResult, type CheckDiff, type CheckResult, type CheckRule, type CheckRulesFile, type CheckSnapshotOptions, type CheckSnapshotResult, type CheckViolation, CodeGraphStore, DEAD_CODE_METRIC_NAMES, DEFAULT_CHURN_WINDOWS, DOMAIN_DDL, type DeepAst, type DeepAstInput, type DiffCheckResultsOptions, type DiffSnapshotsOptions, type EdgeKind, type EmbedAttempt, type EmbedCoverage, type EmbedSnapshotResult, type EmbeddableSymbol, type FileFingerprint, type FindSimilarOptions, 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 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, type Severity, type SimilarCandidate, type SimilarResult, type SnapshotInsert, type SnapshotRow, type SnapshotSpec, type SourceLanguage, 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, 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, describeMetric, describeMetrics, detectGitHead, detectGitToplevel, detectRenames, diffCheckResults, diffSnapshots, edgeWeight, embedSnapshot, embedTextsCached, externalId, fileId, findSimilarCapability, 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, planPrune, priorSnapshotForRef, pruneDanglingReferences, readSourceFiles, rebasedViolationKey, resolveAlias, resolveBarrelEdges, resolveChurnWindows, resolveGitRef, resolveSnapshot, runChecks, runPrune, snapshotPageRank, snapshotReferenceEdges, snapshotRelevance, snapshotSymbolConsumers, snapshotSymbolCoupling, snapshotViolations, structuralSignature, symbolId, testCoverageCountMetrics, tryEmbedSnapshot, validateRules, violationKey, walkSourceFiles, windowSuffix };
1435
+ /** A text-summarization backend the product injects; `model` keys the stored summaries. */
1436
+ interface Summarizer {
1437
+ readonly model: string;
1438
+ summarize(prompt: string): Promise<string>;
1439
+ }
1440
+ interface ConventionSymbol {
1441
+ name: string;
1442
+ signature: string;
1443
+ purpose?: string;
1444
+ }
1445
+ interface ConventionArea {
1446
+ /** Stable over membership: `area-` plus a hash of the member file list. */
1447
+ id: string;
1448
+ /** Dominant directory of the members. */
1449
+ label: string;
1450
+ /** Member files, most-connected first. */
1451
+ files: string[];
1452
+ size: number;
1453
+ topSymbols: ConventionSymbol[];
1454
+ /** Hash of the summarizer prompt, which is the summary-cache key. */
1455
+ contentHash: string;
1456
+ summary?: string;
1457
+ }
1458
+ interface ConventionCoverage {
1459
+ /** Non-generated indexed files in the snapshot. */
1460
+ files: number;
1461
+ /** Files inside a kept area; the rest are too small or isolated to summarize. */
1462
+ grouped: number;
1463
+ areas: number;
1464
+ summarized: number;
1465
+ }
1466
+ interface ConventionMap {
1467
+ model: string;
1468
+ coverage: ConventionCoverage;
1469
+ areas: ConventionArea[];
1470
+ }
1471
+ interface SummarizeConventionsResult extends ConventionMap {
1472
+ /** Cache misses sent to the summarizer on this call. */
1473
+ newlySummarized: number;
1474
+ /** Areas whose summary was already stored. */
1475
+ reused: number;
1476
+ }
1477
+ interface ConventionMatch {
1478
+ id: string;
1479
+ label: string;
1480
+ summary: string;
1481
+ files: string[];
1482
+ size: number;
1483
+ /** Cosine similarity of the query to the area summary, in [-1, 1]. */
1484
+ score: number;
1485
+ }
1486
+ interface ConventionQueryResult {
1487
+ query: string;
1488
+ model: string;
1489
+ embeddingModel: string;
1490
+ coverage: ConventionCoverage;
1491
+ matches: ConventionMatch[];
1492
+ }
1493
+ /** The cut level: how coarse the partition is and which areas are kept. */
1494
+ interface ConventionOptions {
1495
+ /** Coarse community count to merge down to; default scales with repo size. */
1496
+ targetCount?: number;
1497
+ /** Areas smaller than this stay ungrouped (not summarized). Default 3. */
1498
+ minSize?: number;
1499
+ }
1500
+ interface FindConventionsOptions extends ConventionOptions {
1501
+ /** Matches returned. Default 3. */
1502
+ limit?: number;
1503
+ }
1504
+ interface ConventionCorpus {
1505
+ areas: ConventionArea[];
1506
+ /** Summarizer prompt per area, keyed by the area's `contentHash`. */
1507
+ prompts: Map<string, string>;
1508
+ coverage: ConventionCoverage;
1509
+ }
1510
+
1511
+ /**
1512
+ * Greedy-modularity (Clauset-Newman-Moore) community detection over the
1513
+ * undirected file dependency graph. Deterministic: ties and labels resolve
1514
+ * by sorted node id, so the same graph always yields the same partition.
1515
+ *
1516
+ * Two optional controls shape the C-88 capability-altitude cut, in opposite
1517
+ * directions: `maxSize` skips any merge that would grow a community past it
1518
+ * (greedy CNM otherwise snowballs a dense graph into one giant blob — the
1519
+ * cap shapes formation instead of trying to split after the fact), and
1520
+ * `targetCount` keeps merging past the natural modularity stop (taking the
1521
+ * least-bad allowed merge) while more communities than that remain, so a
1522
+ * sparse fragmented graph still coarsens. Disconnected components can never
1523
+ * merge, so the floor is the component count.
1524
+ */
1525
+ declare function detectCommunities(fileIds: readonly string[], edges: readonly GraphEdge[], opts?: {
1526
+ targetCount?: number;
1527
+ maxSize?: number;
1528
+ }): Map<string, string[]>;
1529
+
1530
+ /** Coarse capability-altitude cut: about one area per 25 files, clamped to 6..40. */
1531
+ declare function defaultTargetCount(fileCount: number): number;
1532
+ /**
1533
+ * The deterministic half of the convention layer: partition the barrel-resolved
1534
+ * file graph into coarse areas and build each area's summarizer prompt and cache key.
1535
+ */
1536
+ declare function buildConventionAreas(store: CodeGraphStore, snapshotId: number, opts?: ConventionOptions): ConventionCorpus;
1537
+
1538
+ /** The blob_cache namespace for area summaries; the model column holds `summarizer.model`. */
1539
+ declare const COMMUNITY_SUMMARY_NAMESPACE = "code-graph/community-summary";
1540
+ /**
1541
+ * Generate or reuse the summary of every area. Only cache misses reach the
1542
+ * summarizer, so cost scales with what structurally changed, not with repo size.
1543
+ */
1544
+ declare function summarizeConventions(store: CodeGraphStore, snapshotId: number, summarizer: Summarizer, opts?: ConventionOptions): Promise<SummarizeConventionsResult>;
1545
+ /** Read-only view: the partition plus whatever summaries are already stored. Never calls a summarizer. */
1546
+ declare function getConventionMap(store: CodeGraphStore, snapshotId: number, model: string, opts?: ConventionOptions): ConventionMap;
1547
+
1548
+ /**
1549
+ * "How does this repo do X?": rank summarized areas by similarity of their
1550
+ * summary to the question. Returns candidates, not verdicts. Summary vectors
1551
+ * share the content-addressed embedding cache with the symbol layer.
1552
+ */
1553
+ declare function findConventions(store: CodeGraphStore, snapshotId: number, query: string, embedder: Embedder, model: string, opts?: FindConventionsOptions): Promise<ConventionQueryResult>;
1554
+
1555
+ 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, DOMAIN_DDL, type DeepAst, type DeepAstInput, type DiffCheckResultsOptions, type DiffSnapshotsOptions, type EdgeKind, type EmbedAttempt, type EmbedCoverage, type EmbedSnapshotResult, type EmbeddableSymbol, type FileFingerprint, type FindConventionsOptions, type FindSimilarOptions, 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 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, 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, 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, planPrune, priorSnapshotForRef, pruneDanglingReferences, readSourceFiles, rebasedViolationKey, resolveAlias, resolveBarrelEdges, resolveChurnWindows, resolveGitRef, resolveSnapshot, runChecks, runPrune, snapshotPageRank, snapshotReferenceEdges, snapshotRelevance, snapshotSymbolConsumers, snapshotSymbolCoupling, snapshotViolations, structuralSignature, summarizeConventions, symbolId, testCoverageCountMetrics, tryEmbedSnapshot, validateRules, violationKey, walkSourceFiles, windowSuffix };
package/dist/index.js CHANGED
@@ -1396,8 +1396,8 @@ function walk(requestedId, maps) {
1396
1396
  }
1397
1397
  function chainSteps(input) {
1398
1398
  if (input.from === null) return { steps: rootPath(input.lineage, input.to, input.maxHops), connected: true };
1399
- const path11 = lineagePath(input.lineage, input.from, input.to, input.maxHops);
1400
- if (path11) return { steps: path11, connected: true };
1399
+ const path12 = lineagePath(input.lineage, input.from, input.to, input.maxHops);
1400
+ if (path12) return { steps: path12, connected: true };
1401
1401
  return { steps: [{ snapshotId: input.to, direction: "forward" }], connected: false };
1402
1402
  }
1403
1403
  function createAliasChain(input) {
@@ -4723,18 +4723,18 @@ function checkSnapshot(store, options) {
4723
4723
  });
4724
4724
  return { snapshot, baselineSnapshot, result };
4725
4725
  }
4726
- async function loadCheckRules(path11, options = {}) {
4726
+ async function loadCheckRules(path12, options = {}) {
4727
4727
  let raw;
4728
4728
  try {
4729
- raw = await readFile2(path11, "utf8");
4729
+ raw = await readFile2(path12, "utf8");
4730
4730
  } catch (err) {
4731
- throw new Error(`Cannot read rules file at ${path11}: ${errorMessage(err)}`);
4731
+ throw new Error(`Cannot read rules file at ${path12}: ${errorMessage(err)}`);
4732
4732
  }
4733
4733
  let parsed;
4734
4734
  try {
4735
4735
  parsed = JSON.parse(raw);
4736
4736
  } catch (err) {
4737
- throw new Error(`Invalid JSON in ${path11}: ${errorMessage(err)}`);
4737
+ throw new Error(`Invalid JSON in ${path12}: ${errorMessage(err)}`);
4738
4738
  }
4739
4739
  return validateRules(parsed, options);
4740
4740
  }
@@ -5037,9 +5037,320 @@ function buildSymbolIndex(store, model, symbols) {
5037
5037
  function toCandidate(s, score) {
5038
5038
  return { id: s.id, name: s.name, file: s.file, signature: s.signature, purpose: s.purpose, score };
5039
5039
  }
5040
+
5041
+ // src/conventions/communities.ts
5042
+ function detectCommunities(fileIds, edges, opts) {
5043
+ const nodeSet = new Set(fileIds);
5044
+ const adj = buildUndirectedAdjacency(edges, nodeSet);
5045
+ const communityOf = greedyModularity(fileIds, adj, opts);
5046
+ const out = /* @__PURE__ */ new Map();
5047
+ for (const f of fileIds) pushMulti(out, communityOf.get(f) ?? f, f);
5048
+ return out;
5049
+ }
5050
+ function buildUndirectedAdjacency(edges, nodeSet) {
5051
+ const adj = /* @__PURE__ */ new Map();
5052
+ const link = (a, b) => {
5053
+ let row = adj.get(a);
5054
+ if (!row) {
5055
+ row = /* @__PURE__ */ new Map();
5056
+ adj.set(a, row);
5057
+ }
5058
+ row.set(b, (row.get(b) ?? 0) + 1);
5059
+ };
5060
+ for (const e of edges) {
5061
+ if (e.srcId === e.dstId) continue;
5062
+ if (!nodeSet.has(e.srcId) || !nodeSet.has(e.dstId)) continue;
5063
+ link(e.srcId, e.dstId);
5064
+ link(e.dstId, e.srcId);
5065
+ }
5066
+ return adj;
5067
+ }
5068
+ function greedyModularity(nodes, adj, opts) {
5069
+ const state = initCommunities(nodes, adj);
5070
+ if (state.twoM === 0) return new Map(nodes.map((n) => [n, n]));
5071
+ for (; ; ) {
5072
+ const coarsen = opts?.targetCount !== void 0 && state.members.size > opts.targetCount;
5073
+ const best = bestMerge(state, coarsen, opts?.maxSize);
5074
+ if (!best) break;
5075
+ applyMerge(state, best.i, best.j);
5076
+ }
5077
+ return communityLabels(state);
5078
+ }
5079
+ function initCommunities(nodes, adj) {
5080
+ const members = /* @__PURE__ */ new Map();
5081
+ const deg = /* @__PURE__ */ new Map();
5082
+ const between = /* @__PURE__ */ new Map();
5083
+ let twoM = 0;
5084
+ for (const n of nodes) {
5085
+ const row = /* @__PURE__ */ new Map();
5086
+ let d = 0;
5087
+ for (const [k, w] of adj.get(n) ?? []) {
5088
+ row.set(k, w);
5089
+ d += w;
5090
+ }
5091
+ members.set(n, [n]);
5092
+ deg.set(n, d);
5093
+ between.set(n, row);
5094
+ twoM += d;
5095
+ }
5096
+ return { members, deg, between, twoM };
5097
+ }
5098
+ function bestMerge(state, allowNegative, maxSize) {
5099
+ const { members, between, deg, twoM } = state;
5100
+ let best = null;
5101
+ for (const [i, row] of between) {
5102
+ for (const [j, eij] of row) {
5103
+ if (i >= j) continue;
5104
+ if (maxSize !== void 0 && members.get(i).length + members.get(j).length > maxSize) {
5105
+ continue;
5106
+ }
5107
+ const dQ = 2 * eij / twoM - 2 * deg.get(i) * deg.get(j) / (twoM * twoM);
5108
+ if (!allowNegative && dQ <= 1e-12) continue;
5109
+ if (best === null || dQ > best.dQ) best = { i, j, dQ };
5110
+ }
5111
+ }
5112
+ return best;
5113
+ }
5114
+ function applyMerge(state, i, j) {
5115
+ const { members, deg, between } = state;
5116
+ members.get(i).push(...members.get(j));
5117
+ members.delete(j);
5118
+ deg.set(i, deg.get(i) + deg.get(j));
5119
+ deg.delete(j);
5120
+ const rowI = between.get(i);
5121
+ for (const [k, w] of between.get(j)) {
5122
+ if (k === i) continue;
5123
+ rowI.set(k, (rowI.get(k) ?? 0) + w);
5124
+ const rowK = between.get(k);
5125
+ rowK.set(i, (rowK.get(i) ?? 0) + w);
5126
+ rowK.delete(j);
5127
+ }
5128
+ rowI.delete(j);
5129
+ between.delete(j);
5130
+ }
5131
+ function communityLabels(state) {
5132
+ const out = /* @__PURE__ */ new Map();
5133
+ const reps = [...state.members.keys()].sort();
5134
+ reps.forEach((rep, idx) => {
5135
+ for (const n of state.members.get(rep)) out.set(n, `c${idx}`);
5136
+ });
5137
+ return out;
5138
+ }
5139
+ function pushMulti(map, key, value) {
5140
+ let list = map.get(key);
5141
+ if (!list) {
5142
+ list = [];
5143
+ map.set(key, list);
5144
+ }
5145
+ list.push(value);
5146
+ }
5147
+
5148
+ // src/conventions/areas.ts
5149
+ import * as path11 from "path";
5150
+ import { contentHashOf as contentHashOf2 } from "@titan-design/store-sqlite";
5151
+ var DEFAULT_MIN_SIZE = 3;
5152
+ var PROMPT_FILE_CAP = 30;
5153
+ var PROMPT_SYMBOL_CAP = 15;
5154
+ function defaultTargetCount(fileCount) {
5155
+ return Math.min(40, Math.max(6, Math.ceil(fileCount / 25)));
5156
+ }
5157
+ function buildConventionAreas(store, snapshotId, opts) {
5158
+ const nodes = store.listNodes(snapshotId, { includeSymbols: true });
5159
+ const { files, edges } = resolvedFileGraph(store, snapshotId, nodes);
5160
+ const symbolsByFile = collectExportedSymbols(nodes);
5161
+ const areas = [];
5162
+ const prompts = /* @__PURE__ */ new Map();
5163
+ for (const members of partitionFiles(files, edges, opts)) {
5164
+ const { area, prompt } = buildArea(members, edges, symbolsByFile);
5165
+ areas.push(area);
5166
+ prompts.set(area.contentHash, prompt);
5167
+ }
5168
+ const grouped = areas.reduce((s, a) => s + a.size, 0);
5169
+ return { areas, prompts, coverage: { files: files.length, grouped, areas: areas.length, summarized: 0 } };
5170
+ }
5171
+ function resolvedFileGraph(store, snapshotId, nodes) {
5172
+ const files = nodes.filter((n) => n.kind === "file" && n.role !== "generated").map((n) => n.id).sort();
5173
+ const fileSet = new Set(files);
5174
+ const edges = resolveBarrelEdges(nodes, store.listEdges(snapshotId)).filter(
5175
+ (e) => e.srcId !== e.dstId && fileSet.has(e.srcId) && fileSet.has(e.dstId)
5176
+ );
5177
+ return { files, edges };
5178
+ }
5179
+ function partitionFiles(files, edges, opts) {
5180
+ const target = opts?.targetCount ?? defaultTargetCount(files.length);
5181
+ const minSize = opts?.minSize ?? DEFAULT_MIN_SIZE;
5182
+ const maxSize = Math.max(minSize * 2, Math.ceil(files.length / target * 2));
5183
+ return [...detectCommunities(files, edges, { targetCount: target, maxSize }).values()].filter((members) => members.length >= minSize).sort(bySizeThenFirstFile);
5184
+ }
5185
+ function bySizeThenFirstFile(a, b) {
5186
+ if (a.length !== b.length) return b.length - a.length;
5187
+ return (a[0] ?? "") < (b[0] ?? "") ? -1 : 1;
5188
+ }
5189
+ function collectExportedSymbols(nodes) {
5190
+ const byFile = /* @__PURE__ */ new Map();
5191
+ for (const n of nodes) {
5192
+ if (n.kind !== "symbol") continue;
5193
+ const attrs = n.attrs ?? {};
5194
+ if (attrs.exported !== true || !attrs.signature) continue;
5195
+ const file = n.parentId ?? n.id.split("#")[0] ?? n.id;
5196
+ const list = byFile.get(file) ?? [];
5197
+ list.push({ name: n.name, signature: attrs.signature, purpose: attrs.purpose });
5198
+ byFile.set(file, list);
5199
+ }
5200
+ return byFile;
5201
+ }
5202
+ function buildArea(members, edges, symbolsByFile) {
5203
+ const files = rankByDegree(members, edges);
5204
+ const label = dominantDirectory(files);
5205
+ const topSymbols = pickTopSymbols(files, symbolsByFile);
5206
+ const prompt = buildPrompt(label, files, topSymbols);
5207
+ const area = {
5208
+ id: `area-${contentHashOf2(files.join("\n")).slice(0, 8)}`,
5209
+ label,
5210
+ files,
5211
+ size: files.length,
5212
+ topSymbols,
5213
+ contentHash: contentHashOf2(prompt)
5214
+ };
5215
+ return { area, prompt };
5216
+ }
5217
+ function rankByDegree(members, edges) {
5218
+ const memberSet = new Set(members);
5219
+ const degree = /* @__PURE__ */ new Map();
5220
+ for (const e of edges) {
5221
+ if (!memberSet.has(e.srcId) || !memberSet.has(e.dstId)) continue;
5222
+ degree.set(e.srcId, (degree.get(e.srcId) ?? 0) + 1);
5223
+ degree.set(e.dstId, (degree.get(e.dstId) ?? 0) + 1);
5224
+ }
5225
+ return [...members].sort((a, b) => {
5226
+ const d = (degree.get(b) ?? 0) - (degree.get(a) ?? 0);
5227
+ return d !== 0 ? d : a < b ? -1 : 1;
5228
+ });
5229
+ }
5230
+ function dominantDirectory(files) {
5231
+ const counts = /* @__PURE__ */ new Map();
5232
+ for (const f of files) {
5233
+ const dir = path11.posix.dirname(f);
5234
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
5235
+ }
5236
+ const dirs = [...counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
5237
+ const topDir = dirs[0]?.[0] ?? ".";
5238
+ return dirs.length > 1 ? `${topDir} (+${dirs.length - 1} dirs)` : topDir;
5239
+ }
5240
+ function pickTopSymbols(rankedFiles, symbolsByFile) {
5241
+ const out = [];
5242
+ for (const file of rankedFiles) {
5243
+ for (const sym of symbolsByFile.get(file) ?? []) {
5244
+ out.push(sym);
5245
+ if (out.length >= PROMPT_SYMBOL_CAP) return out;
5246
+ }
5247
+ }
5248
+ return out;
5249
+ }
5250
+ function buildPrompt(label, files, symbols) {
5251
+ const shown = files.slice(0, PROMPT_FILE_CAP);
5252
+ const lines = [
5253
+ 'You are writing one entry of a repository "convention map" \u2014 a',
5254
+ "capability-altitude summary coding agents read at PLAN time to learn how",
5255
+ "this repo does things and where new code belongs.",
5256
+ "",
5257
+ `Capability area: ${label}`,
5258
+ `Files (${files.length}):`,
5259
+ ...shown.map((f) => `- ${f}`)
5260
+ ];
5261
+ if (files.length > shown.length) lines.push(`- \u2026and ${files.length - shown.length} more`);
5262
+ if (symbols.length > 0) {
5263
+ lines.push("Key exported symbols:");
5264
+ for (const s of symbols) lines.push(`- ${s.signature}${s.purpose ? ` \u2014 ${s.purpose}` : ""}`);
5265
+ }
5266
+ lines.push(
5267
+ "",
5268
+ "Write 2-3 sentences: (1) what this area does, and (2) HOW \u2014 the key entry",
5269
+ "points, patterns and conventions to follow when adding related code.",
5270
+ "Plain text only, no preamble or headings."
5271
+ );
5272
+ return lines.join("\n");
5273
+ }
5274
+
5275
+ // src/conventions/summaries.ts
5276
+ import { CacheBlobTable as CacheBlobTable2 } from "@titan-design/store-sqlite";
5277
+ var COMMUNITY_SUMMARY_NAMESPACE = "code-graph/community-summary";
5278
+ function summaryCache(store) {
5279
+ return new CacheBlobTable2(store.db, { name: KIT.cacheBlob });
5280
+ }
5281
+ function loadSummaries(store, model, hashes) {
5282
+ const cache = summaryCache(store);
5283
+ const found = /* @__PURE__ */ new Map();
5284
+ for (const contentHash of new Set(hashes)) {
5285
+ const hit = cache.get({ namespace: COMMUNITY_SUMMARY_NAMESPACE, model, contentHash });
5286
+ if (hit) found.set(contentHash, hit.value.toString("utf8"));
5287
+ }
5288
+ return found;
5289
+ }
5290
+ function storeSummary(store, model, contentHash, summary) {
5291
+ summaryCache(store).put({ namespace: COMMUNITY_SUMMARY_NAMESPACE, model, contentHash }, summary);
5292
+ }
5293
+ function attachSummaries(areas, summaries) {
5294
+ for (const area of areas) area.summary = summaries.get(area.contentHash);
5295
+ }
5296
+ async function summarizeConventions(store, snapshotId, summarizer, opts) {
5297
+ const { areas, prompts, coverage } = buildConventionAreas(store, snapshotId, opts);
5298
+ attachSummaries(areas, loadSummaries(store, summarizer.model, areas.map((a) => a.contentHash)));
5299
+ let newlySummarized = 0;
5300
+ for (const area of areas) {
5301
+ if (area.summary !== void 0) continue;
5302
+ const summary = (await summarizer.summarize(prompts.get(area.contentHash))).trim();
5303
+ storeSummary(store, summarizer.model, area.contentHash, summary);
5304
+ area.summary = summary;
5305
+ newlySummarized++;
5306
+ }
5307
+ coverage.summarized = areas.filter((a) => a.summary).length;
5308
+ return { model: summarizer.model, coverage, areas, newlySummarized, reused: areas.length - newlySummarized };
5309
+ }
5310
+ function getConventionMap(store, snapshotId, model, opts) {
5311
+ const { areas, coverage } = buildConventionAreas(store, snapshotId, opts);
5312
+ attachSummaries(areas, loadSummaries(store, model, areas.map((a) => a.contentHash)));
5313
+ coverage.summarized = areas.filter((a) => a.summary).length;
5314
+ return { model, coverage, areas };
5315
+ }
5316
+
5317
+ // src/conventions/query.ts
5318
+ import { BruteForceVectorIndex as BruteForceVectorIndex2, vectorRetriever as vectorRetriever2 } from "@titan-design/retrieval";
5319
+ var MATCH_FILE_CAP = 5;
5320
+ var DEFAULT_LIMIT = 3;
5321
+ async function findConventions(store, snapshotId, query, embedder, model, opts = {}) {
5322
+ const map = getConventionMap(store, snapshotId, model, opts);
5323
+ const summarized = map.areas.filter((a) => Boolean(a.summary));
5324
+ if (summarized.length === 0) {
5325
+ throw new Error(`No convention summaries stored for model ${model}; run summarizeConventions first.`);
5326
+ }
5327
+ const { byHash } = await embedTextsCached(store, embedder, summarized.map((a) => a.summary));
5328
+ const index = new BruteForceVectorIndex2();
5329
+ for (const area of summarized) index.add(area.id, byHash.get(hashEmbedText(area.summary)));
5330
+ const hits = await vectorRetriever2(embedder, index).retrieve(query, { limit: opts.limit ?? DEFAULT_LIMIT });
5331
+ const byId = new Map(summarized.map((a) => [a.id, a]));
5332
+ return {
5333
+ query,
5334
+ model,
5335
+ embeddingModel: embedder.model,
5336
+ coverage: map.coverage,
5337
+ matches: hits.map((h) => toMatch(byId.get(h.id), h.score ?? 0))
5338
+ };
5339
+ }
5340
+ function toMatch(area, score) {
5341
+ return {
5342
+ id: area.id,
5343
+ label: area.label,
5344
+ summary: area.summary,
5345
+ files: area.files.slice(0, MATCH_FILE_CAP),
5346
+ size: area.size,
5347
+ score
5348
+ };
5349
+ }
5040
5350
  export {
5041
5351
  ALIAS_BASE_ATTR,
5042
5352
  ALL_ROLES,
5353
+ COMMUNITY_SUMMARY_NAMESPACE,
5043
5354
  COVERAGE_METRIC_NAME,
5044
5355
  CodeGraphStore,
5045
5356
  DEAD_CODE_METRIC_NAMES,
@@ -5063,6 +5374,7 @@ export {
5063
5374
  annotateRoles,
5064
5375
  attributeCoverage,
5065
5376
  buildAliases,
5377
+ buildConventionAreas,
5066
5378
  buildEmbedText,
5067
5379
  buildFileModuleNodes,
5068
5380
  buildIndexerMetrics,
@@ -5089,8 +5401,10 @@ export {
5089
5401
  computeSymbolCoupling,
5090
5402
  computeTestCoverageOwnership,
5091
5403
  createAliasChain,
5404
+ defaultTargetCount,
5092
5405
  describeMetric,
5093
5406
  describeMetrics,
5407
+ detectCommunities,
5094
5408
  detectGitHead,
5095
5409
  detectGitToplevel,
5096
5410
  detectRenames,
@@ -5101,7 +5415,9 @@ export {
5101
5415
  embedTextsCached,
5102
5416
  externalId,
5103
5417
  fileId,
5418
+ findConventions,
5104
5419
  findSimilarCapability,
5420
+ getConventionMap,
5105
5421
  getEdgeWeight,
5106
5422
  getLanguageFromPath3 as getLanguageFromPath,
5107
5423
  getSupportedLanguages,
@@ -5153,6 +5469,7 @@ export {
5153
5469
  snapshotSymbolCoupling,
5154
5470
  snapshotViolations,
5155
5471
  structuralSignature,
5472
+ summarizeConventions,
5156
5473
  symbolId,
5157
5474
  testCoverageCountMetrics,
5158
5475
  tryEmbedSnapshot,