@titan-design/code-graph 0.3.0 → 0.5.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/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) {
@@ -4177,11 +4177,13 @@ function describeMetrics(names) {
4177
4177
 
4178
4178
  // src/check/context.ts
4179
4179
  function buildRuleContext(store, snapshotId) {
4180
- const nodes = store.listNodes(snapshotId);
4180
+ const all = store.listNodes(snapshotId, { includeSymbols: true });
4181
+ const nodes = all.filter((n) => n.kind !== "symbol");
4182
+ const symbolNodes2 = all.filter((n) => n.kind === "symbol");
4181
4183
  const edges = store.listEdges(snapshotId);
4182
4184
  const nodesById = /* @__PURE__ */ new Map();
4183
4185
  for (const n of nodes) nodesById.set(n.id, n);
4184
- return { nodes, nodesById, metricsByNode: indexMetrics(store.listMetrics(snapshotId)), edges };
4186
+ return { nodes, symbolNodes: symbolNodes2, nodesById, metricsByNode: indexMetrics(store.listMetrics(snapshotId)), edges };
4185
4187
  }
4186
4188
  function indexMetrics(metrics) {
4187
4189
  const metricsByNode = /* @__PURE__ */ new Map();
@@ -4339,6 +4341,18 @@ function barrelMessage(barrelPkg, importerCount) {
4339
4341
  return importerCount === 0 ? `barrel in package "${barrelPkg}" has no importers` : `barrel in package "${barrelPkg}" has ${importerCount} internal importer(s) but zero external`;
4340
4342
  }
4341
4343
 
4344
+ // src/check/violation-location.ts
4345
+ var CODE_GRAPH_TOOL = "code-graph";
4346
+ function locateNode(node) {
4347
+ if (node.kind !== "symbol") return { path: node.id };
4348
+ const location = { path: node.parentId ?? node.id, symbol: node.name };
4349
+ const startLine = node.attrs?.startLine;
4350
+ const endLine = node.attrs?.endLine;
4351
+ if (typeof startLine === "number") location.lineStart = startLine;
4352
+ if (typeof endLine === "number") location.lineEnd = endLine;
4353
+ return location;
4354
+ }
4355
+
4342
4356
  // src/check/metric-rules.ts
4343
4357
  function nodeFilter(rule) {
4344
4358
  return {
@@ -4353,71 +4367,76 @@ function passesFilter(node, filter) {
4353
4367
  if (node.role && filter.excludedRoles.has(node.role)) return false;
4354
4368
  return true;
4355
4369
  }
4356
- function runMetricMaxRule(rule, ctx) {
4370
+ function candidateNodes(rule, ctx) {
4357
4371
  const filter = nodeFilter(rule);
4372
+ const pool = rule.kind === "symbol" ? ctx.symbolNodes : ctx.nodes;
4373
+ return pool.filter((node) => passesFilter(node, filter));
4374
+ }
4375
+ function toViolation(rule, breach) {
4376
+ return {
4377
+ ruleId: rule.id,
4378
+ severity: severityOf(rule),
4379
+ nodeId: breach.node.id,
4380
+ metric: breach.metric,
4381
+ value: breach.value,
4382
+ threshold: breach.threshold,
4383
+ message: breach.message,
4384
+ ...locateNode(breach.node),
4385
+ evidence: breach.evidence,
4386
+ tool: CODE_GRAPH_TOOL
4387
+ };
4388
+ }
4389
+ function thresholdBreach(node, metric, value, bound, threshold) {
4390
+ const op = bound === "max" ? ">" : "<";
4391
+ return {
4392
+ node,
4393
+ metric,
4394
+ value,
4395
+ threshold,
4396
+ message: `${metric}=${formatNumber(value)} ${op} ${formatNumber(threshold)}`,
4397
+ evidence: `${metric}=${formatNumber(value)} (${bound} ${formatNumber(threshold)})`
4398
+ };
4399
+ }
4400
+ function runMetricMaxRule(rule, ctx) {
4358
4401
  const out = [];
4359
- for (const node of ctx.nodes) {
4360
- if (!passesFilter(node, filter)) continue;
4402
+ for (const node of candidateNodes(rule, ctx)) {
4361
4403
  const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
4362
- if (value === void 0) continue;
4363
- if (value <= rule.max) continue;
4364
- out.push({
4365
- ruleId: rule.id,
4366
- severity: severityOf(rule),
4367
- nodeId: node.id,
4368
- metric: rule.metric,
4369
- value,
4370
- threshold: rule.max,
4371
- message: `${rule.metric}=${formatNumber(value)} > ${formatNumber(rule.max)}`
4372
- });
4404
+ if (value === void 0 || value <= rule.max) continue;
4405
+ out.push(toViolation(rule, thresholdBreach(node, rule.metric, value, "max", rule.max)));
4373
4406
  }
4374
4407
  return out;
4375
4408
  }
4376
4409
  function runMetricMinRule(rule, ctx) {
4377
- const filter = nodeFilter(rule);
4378
4410
  const out = [];
4379
- for (const node of ctx.nodes) {
4380
- if (!passesFilter(node, filter)) continue;
4411
+ for (const node of candidateNodes(rule, ctx)) {
4381
4412
  const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
4382
- if (value === void 0) continue;
4383
- if (value >= rule.min) continue;
4384
- out.push({
4385
- ruleId: rule.id,
4386
- severity: severityOf(rule),
4387
- nodeId: node.id,
4388
- metric: rule.metric,
4389
- value,
4390
- threshold: rule.min,
4391
- message: `${rule.metric}=${formatNumber(value)} < ${formatNumber(rule.min)}`
4392
- });
4413
+ if (value === void 0 || value >= rule.min) continue;
4414
+ out.push(toViolation(rule, thresholdBreach(node, rule.metric, value, "min", rule.min)));
4393
4415
  }
4394
4416
  return out;
4395
4417
  }
4396
4418
  function runMetricProductMaxRule(rule, ctx) {
4397
- const filter = nodeFilter(rule);
4398
4419
  const out = [];
4399
- for (const node of ctx.nodes) {
4400
- if (!passesFilter(node, filter)) continue;
4420
+ for (const node of candidateNodes(rule, ctx)) {
4401
4421
  const inner = ctx.metricsByNode.get(node.id);
4402
4422
  if (!inner) continue;
4403
4423
  const components = collectComponents(rule.metrics, inner);
4404
4424
  if (!components) continue;
4405
4425
  const product = components.reduce((a, b) => a * b, 1);
4406
4426
  if (product <= rule.max) continue;
4407
- out.push(productViolation(rule, node.id, components, product));
4427
+ out.push(toViolation(rule, productBreach(rule, node, components, product)));
4408
4428
  }
4409
4429
  return out;
4410
4430
  }
4411
- function productViolation(rule, nodeId, components, product) {
4431
+ function productBreach(rule, node, components, product) {
4412
4432
  const detail = rule.metrics.map((m, i) => `${m}=${formatNumber(components[i])}`).join(" * ");
4413
4433
  return {
4414
- ruleId: rule.id,
4415
- severity: severityOf(rule),
4416
- nodeId,
4434
+ node,
4417
4435
  metric: rule.metrics.join(" * "),
4418
4436
  value: product,
4419
4437
  threshold: rule.max,
4420
- message: `${detail} = ${formatNumber(product)} > ${formatNumber(rule.max)}`
4438
+ message: `${detail} = ${formatNumber(product)} > ${formatNumber(rule.max)}`,
4439
+ evidence: `${detail} = ${formatNumber(product)} (max ${formatNumber(rule.max)})`
4421
4440
  };
4422
4441
  }
4423
4442
  function collectComponents(metrics, values) {
@@ -4507,6 +4526,43 @@ function countByOriginAndSeverity(violations) {
4507
4526
  return counts;
4508
4527
  }
4509
4528
 
4529
+ // src/check/findings.ts
4530
+ function toFindings(result) {
4531
+ return result.violations.map(violationToFinding);
4532
+ }
4533
+ function violationToFinding(v) {
4534
+ const tool = v.tool ?? CODE_GRAPH_TOOL;
4535
+ const target = v.destinationId ? `${v.nodeId}->${v.destinationId}` : v.nodeId;
4536
+ return withoutUndefined({
4537
+ id: `${tool}:${v.ruleId}:${target}`,
4538
+ path: v.path ?? v.nodeId,
4539
+ lineStart: v.lineStart,
4540
+ lineEnd: v.lineEnd,
4541
+ symbol: v.symbol,
4542
+ signal: v.ruleId,
4543
+ value: v.value,
4544
+ threshold: v.threshold,
4545
+ severity: v.severity,
4546
+ evidence: v.evidence ?? v.message,
4547
+ tool
4548
+ });
4549
+ }
4550
+ function externalToFinding(input) {
4551
+ return withoutUndefined({
4552
+ id: `${input.tool}:${input.rule}:${input.file}:${input.line}`,
4553
+ path: input.file,
4554
+ lineStart: input.line,
4555
+ lineEnd: input.endLine,
4556
+ signal: input.rule,
4557
+ severity: input.severity,
4558
+ evidence: input.message,
4559
+ tool: input.tool
4560
+ });
4561
+ }
4562
+ function withoutUndefined(finding) {
4563
+ return Object.fromEntries(Object.entries(finding).filter(([, v]) => v !== void 0));
4564
+ }
4565
+
4510
4566
  // src/check/validate.ts
4511
4567
  function validateRules(input, options = {}) {
4512
4568
  if (!input || typeof input !== "object") {
@@ -4723,18 +4779,18 @@ function checkSnapshot(store, options) {
4723
4779
  });
4724
4780
  return { snapshot, baselineSnapshot, result };
4725
4781
  }
4726
- async function loadCheckRules(path11, options = {}) {
4782
+ async function loadCheckRules(path12, options = {}) {
4727
4783
  let raw;
4728
4784
  try {
4729
- raw = await readFile2(path11, "utf8");
4785
+ raw = await readFile2(path12, "utf8");
4730
4786
  } catch (err) {
4731
- throw new Error(`Cannot read rules file at ${path11}: ${errorMessage(err)}`);
4787
+ throw new Error(`Cannot read rules file at ${path12}: ${errorMessage(err)}`);
4732
4788
  }
4733
4789
  let parsed;
4734
4790
  try {
4735
4791
  parsed = JSON.parse(raw);
4736
4792
  } catch (err) {
4737
- throw new Error(`Invalid JSON in ${path11}: ${errorMessage(err)}`);
4793
+ throw new Error(`Invalid JSON in ${path12}: ${errorMessage(err)}`);
4738
4794
  }
4739
4795
  return validateRules(parsed, options);
4740
4796
  }
@@ -5037,9 +5093,320 @@ function buildSymbolIndex(store, model, symbols) {
5037
5093
  function toCandidate(s, score) {
5038
5094
  return { id: s.id, name: s.name, file: s.file, signature: s.signature, purpose: s.purpose, score };
5039
5095
  }
5096
+
5097
+ // src/conventions/communities.ts
5098
+ function detectCommunities(fileIds, edges, opts) {
5099
+ const nodeSet = new Set(fileIds);
5100
+ const adj = buildUndirectedAdjacency(edges, nodeSet);
5101
+ const communityOf = greedyModularity(fileIds, adj, opts);
5102
+ const out = /* @__PURE__ */ new Map();
5103
+ for (const f of fileIds) pushMulti(out, communityOf.get(f) ?? f, f);
5104
+ return out;
5105
+ }
5106
+ function buildUndirectedAdjacency(edges, nodeSet) {
5107
+ const adj = /* @__PURE__ */ new Map();
5108
+ const link = (a, b) => {
5109
+ let row = adj.get(a);
5110
+ if (!row) {
5111
+ row = /* @__PURE__ */ new Map();
5112
+ adj.set(a, row);
5113
+ }
5114
+ row.set(b, (row.get(b) ?? 0) + 1);
5115
+ };
5116
+ for (const e of edges) {
5117
+ if (e.srcId === e.dstId) continue;
5118
+ if (!nodeSet.has(e.srcId) || !nodeSet.has(e.dstId)) continue;
5119
+ link(e.srcId, e.dstId);
5120
+ link(e.dstId, e.srcId);
5121
+ }
5122
+ return adj;
5123
+ }
5124
+ function greedyModularity(nodes, adj, opts) {
5125
+ const state = initCommunities(nodes, adj);
5126
+ if (state.twoM === 0) return new Map(nodes.map((n) => [n, n]));
5127
+ for (; ; ) {
5128
+ const coarsen = opts?.targetCount !== void 0 && state.members.size > opts.targetCount;
5129
+ const best = bestMerge(state, coarsen, opts?.maxSize);
5130
+ if (!best) break;
5131
+ applyMerge(state, best.i, best.j);
5132
+ }
5133
+ return communityLabels(state);
5134
+ }
5135
+ function initCommunities(nodes, adj) {
5136
+ const members = /* @__PURE__ */ new Map();
5137
+ const deg = /* @__PURE__ */ new Map();
5138
+ const between = /* @__PURE__ */ new Map();
5139
+ let twoM = 0;
5140
+ for (const n of nodes) {
5141
+ const row = /* @__PURE__ */ new Map();
5142
+ let d = 0;
5143
+ for (const [k, w] of adj.get(n) ?? []) {
5144
+ row.set(k, w);
5145
+ d += w;
5146
+ }
5147
+ members.set(n, [n]);
5148
+ deg.set(n, d);
5149
+ between.set(n, row);
5150
+ twoM += d;
5151
+ }
5152
+ return { members, deg, between, twoM };
5153
+ }
5154
+ function bestMerge(state, allowNegative, maxSize) {
5155
+ const { members, between, deg, twoM } = state;
5156
+ let best = null;
5157
+ for (const [i, row] of between) {
5158
+ for (const [j, eij] of row) {
5159
+ if (i >= j) continue;
5160
+ if (maxSize !== void 0 && members.get(i).length + members.get(j).length > maxSize) {
5161
+ continue;
5162
+ }
5163
+ const dQ = 2 * eij / twoM - 2 * deg.get(i) * deg.get(j) / (twoM * twoM);
5164
+ if (!allowNegative && dQ <= 1e-12) continue;
5165
+ if (best === null || dQ > best.dQ) best = { i, j, dQ };
5166
+ }
5167
+ }
5168
+ return best;
5169
+ }
5170
+ function applyMerge(state, i, j) {
5171
+ const { members, deg, between } = state;
5172
+ members.get(i).push(...members.get(j));
5173
+ members.delete(j);
5174
+ deg.set(i, deg.get(i) + deg.get(j));
5175
+ deg.delete(j);
5176
+ const rowI = between.get(i);
5177
+ for (const [k, w] of between.get(j)) {
5178
+ if (k === i) continue;
5179
+ rowI.set(k, (rowI.get(k) ?? 0) + w);
5180
+ const rowK = between.get(k);
5181
+ rowK.set(i, (rowK.get(i) ?? 0) + w);
5182
+ rowK.delete(j);
5183
+ }
5184
+ rowI.delete(j);
5185
+ between.delete(j);
5186
+ }
5187
+ function communityLabels(state) {
5188
+ const out = /* @__PURE__ */ new Map();
5189
+ const reps = [...state.members.keys()].sort();
5190
+ reps.forEach((rep, idx) => {
5191
+ for (const n of state.members.get(rep)) out.set(n, `c${idx}`);
5192
+ });
5193
+ return out;
5194
+ }
5195
+ function pushMulti(map, key, value) {
5196
+ let list = map.get(key);
5197
+ if (!list) {
5198
+ list = [];
5199
+ map.set(key, list);
5200
+ }
5201
+ list.push(value);
5202
+ }
5203
+
5204
+ // src/conventions/areas.ts
5205
+ import * as path11 from "path";
5206
+ import { contentHashOf as contentHashOf2 } from "@titan-design/store-sqlite";
5207
+ var DEFAULT_MIN_SIZE = 3;
5208
+ var PROMPT_FILE_CAP = 30;
5209
+ var PROMPT_SYMBOL_CAP = 15;
5210
+ function defaultTargetCount(fileCount) {
5211
+ return Math.min(40, Math.max(6, Math.ceil(fileCount / 25)));
5212
+ }
5213
+ function buildConventionAreas(store, snapshotId, opts) {
5214
+ const nodes = store.listNodes(snapshotId, { includeSymbols: true });
5215
+ const { files, edges } = resolvedFileGraph(store, snapshotId, nodes);
5216
+ const symbolsByFile = collectExportedSymbols(nodes);
5217
+ const areas = [];
5218
+ const prompts = /* @__PURE__ */ new Map();
5219
+ for (const members of partitionFiles(files, edges, opts)) {
5220
+ const { area, prompt } = buildArea(members, edges, symbolsByFile);
5221
+ areas.push(area);
5222
+ prompts.set(area.contentHash, prompt);
5223
+ }
5224
+ const grouped = areas.reduce((s, a) => s + a.size, 0);
5225
+ return { areas, prompts, coverage: { files: files.length, grouped, areas: areas.length, summarized: 0 } };
5226
+ }
5227
+ function resolvedFileGraph(store, snapshotId, nodes) {
5228
+ const files = nodes.filter((n) => n.kind === "file" && n.role !== "generated").map((n) => n.id).sort();
5229
+ const fileSet = new Set(files);
5230
+ const edges = resolveBarrelEdges(nodes, store.listEdges(snapshotId)).filter(
5231
+ (e) => e.srcId !== e.dstId && fileSet.has(e.srcId) && fileSet.has(e.dstId)
5232
+ );
5233
+ return { files, edges };
5234
+ }
5235
+ function partitionFiles(files, edges, opts) {
5236
+ const target = opts?.targetCount ?? defaultTargetCount(files.length);
5237
+ const minSize = opts?.minSize ?? DEFAULT_MIN_SIZE;
5238
+ const maxSize = Math.max(minSize * 2, Math.ceil(files.length / target * 2));
5239
+ return [...detectCommunities(files, edges, { targetCount: target, maxSize }).values()].filter((members) => members.length >= minSize).sort(bySizeThenFirstFile);
5240
+ }
5241
+ function bySizeThenFirstFile(a, b) {
5242
+ if (a.length !== b.length) return b.length - a.length;
5243
+ return (a[0] ?? "") < (b[0] ?? "") ? -1 : 1;
5244
+ }
5245
+ function collectExportedSymbols(nodes) {
5246
+ const byFile = /* @__PURE__ */ new Map();
5247
+ for (const n of nodes) {
5248
+ if (n.kind !== "symbol") continue;
5249
+ const attrs = n.attrs ?? {};
5250
+ if (attrs.exported !== true || !attrs.signature) continue;
5251
+ const file = n.parentId ?? n.id.split("#")[0] ?? n.id;
5252
+ const list = byFile.get(file) ?? [];
5253
+ list.push({ name: n.name, signature: attrs.signature, purpose: attrs.purpose });
5254
+ byFile.set(file, list);
5255
+ }
5256
+ return byFile;
5257
+ }
5258
+ function buildArea(members, edges, symbolsByFile) {
5259
+ const files = rankByDegree(members, edges);
5260
+ const label = dominantDirectory(files);
5261
+ const topSymbols = pickTopSymbols(files, symbolsByFile);
5262
+ const prompt = buildPrompt(label, files, topSymbols);
5263
+ const area = {
5264
+ id: `area-${contentHashOf2(files.join("\n")).slice(0, 8)}`,
5265
+ label,
5266
+ files,
5267
+ size: files.length,
5268
+ topSymbols,
5269
+ contentHash: contentHashOf2(prompt)
5270
+ };
5271
+ return { area, prompt };
5272
+ }
5273
+ function rankByDegree(members, edges) {
5274
+ const memberSet = new Set(members);
5275
+ const degree = /* @__PURE__ */ new Map();
5276
+ for (const e of edges) {
5277
+ if (!memberSet.has(e.srcId) || !memberSet.has(e.dstId)) continue;
5278
+ degree.set(e.srcId, (degree.get(e.srcId) ?? 0) + 1);
5279
+ degree.set(e.dstId, (degree.get(e.dstId) ?? 0) + 1);
5280
+ }
5281
+ return [...members].sort((a, b) => {
5282
+ const d = (degree.get(b) ?? 0) - (degree.get(a) ?? 0);
5283
+ return d !== 0 ? d : a < b ? -1 : 1;
5284
+ });
5285
+ }
5286
+ function dominantDirectory(files) {
5287
+ const counts = /* @__PURE__ */ new Map();
5288
+ for (const f of files) {
5289
+ const dir = path11.posix.dirname(f);
5290
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
5291
+ }
5292
+ const dirs = [...counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
5293
+ const topDir = dirs[0]?.[0] ?? ".";
5294
+ return dirs.length > 1 ? `${topDir} (+${dirs.length - 1} dirs)` : topDir;
5295
+ }
5296
+ function pickTopSymbols(rankedFiles, symbolsByFile) {
5297
+ const out = [];
5298
+ for (const file of rankedFiles) {
5299
+ for (const sym of symbolsByFile.get(file) ?? []) {
5300
+ out.push(sym);
5301
+ if (out.length >= PROMPT_SYMBOL_CAP) return out;
5302
+ }
5303
+ }
5304
+ return out;
5305
+ }
5306
+ function buildPrompt(label, files, symbols) {
5307
+ const shown = files.slice(0, PROMPT_FILE_CAP);
5308
+ const lines = [
5309
+ 'You are writing one entry of a repository "convention map" \u2014 a',
5310
+ "capability-altitude summary coding agents read at PLAN time to learn how",
5311
+ "this repo does things and where new code belongs.",
5312
+ "",
5313
+ `Capability area: ${label}`,
5314
+ `Files (${files.length}):`,
5315
+ ...shown.map((f) => `- ${f}`)
5316
+ ];
5317
+ if (files.length > shown.length) lines.push(`- \u2026and ${files.length - shown.length} more`);
5318
+ if (symbols.length > 0) {
5319
+ lines.push("Key exported symbols:");
5320
+ for (const s of symbols) lines.push(`- ${s.signature}${s.purpose ? ` \u2014 ${s.purpose}` : ""}`);
5321
+ }
5322
+ lines.push(
5323
+ "",
5324
+ "Write 2-3 sentences: (1) what this area does, and (2) HOW \u2014 the key entry",
5325
+ "points, patterns and conventions to follow when adding related code.",
5326
+ "Plain text only, no preamble or headings."
5327
+ );
5328
+ return lines.join("\n");
5329
+ }
5330
+
5331
+ // src/conventions/summaries.ts
5332
+ import { CacheBlobTable as CacheBlobTable2 } from "@titan-design/store-sqlite";
5333
+ var COMMUNITY_SUMMARY_NAMESPACE = "code-graph/community-summary";
5334
+ function summaryCache(store) {
5335
+ return new CacheBlobTable2(store.db, { name: KIT.cacheBlob });
5336
+ }
5337
+ function loadSummaries(store, model, hashes) {
5338
+ const cache = summaryCache(store);
5339
+ const found = /* @__PURE__ */ new Map();
5340
+ for (const contentHash of new Set(hashes)) {
5341
+ const hit = cache.get({ namespace: COMMUNITY_SUMMARY_NAMESPACE, model, contentHash });
5342
+ if (hit) found.set(contentHash, hit.value.toString("utf8"));
5343
+ }
5344
+ return found;
5345
+ }
5346
+ function storeSummary(store, model, contentHash, summary) {
5347
+ summaryCache(store).put({ namespace: COMMUNITY_SUMMARY_NAMESPACE, model, contentHash }, summary);
5348
+ }
5349
+ function attachSummaries(areas, summaries) {
5350
+ for (const area of areas) area.summary = summaries.get(area.contentHash);
5351
+ }
5352
+ async function summarizeConventions(store, snapshotId, summarizer, opts) {
5353
+ const { areas, prompts, coverage } = buildConventionAreas(store, snapshotId, opts);
5354
+ attachSummaries(areas, loadSummaries(store, summarizer.model, areas.map((a) => a.contentHash)));
5355
+ let newlySummarized = 0;
5356
+ for (const area of areas) {
5357
+ if (area.summary !== void 0) continue;
5358
+ const summary = (await summarizer.summarize(prompts.get(area.contentHash))).trim();
5359
+ storeSummary(store, summarizer.model, area.contentHash, summary);
5360
+ area.summary = summary;
5361
+ newlySummarized++;
5362
+ }
5363
+ coverage.summarized = areas.filter((a) => a.summary).length;
5364
+ return { model: summarizer.model, coverage, areas, newlySummarized, reused: areas.length - newlySummarized };
5365
+ }
5366
+ function getConventionMap(store, snapshotId, model, opts) {
5367
+ const { areas, coverage } = buildConventionAreas(store, snapshotId, opts);
5368
+ attachSummaries(areas, loadSummaries(store, model, areas.map((a) => a.contentHash)));
5369
+ coverage.summarized = areas.filter((a) => a.summary).length;
5370
+ return { model, coverage, areas };
5371
+ }
5372
+
5373
+ // src/conventions/query.ts
5374
+ import { BruteForceVectorIndex as BruteForceVectorIndex2, vectorRetriever as vectorRetriever2 } from "@titan-design/retrieval";
5375
+ var MATCH_FILE_CAP = 5;
5376
+ var DEFAULT_LIMIT = 3;
5377
+ async function findConventions(store, snapshotId, query, embedder, model, opts = {}) {
5378
+ const map = getConventionMap(store, snapshotId, model, opts);
5379
+ const summarized = map.areas.filter((a) => Boolean(a.summary));
5380
+ if (summarized.length === 0) {
5381
+ throw new Error(`No convention summaries stored for model ${model}; run summarizeConventions first.`);
5382
+ }
5383
+ const { byHash } = await embedTextsCached(store, embedder, summarized.map((a) => a.summary));
5384
+ const index = new BruteForceVectorIndex2();
5385
+ for (const area of summarized) index.add(area.id, byHash.get(hashEmbedText(area.summary)));
5386
+ const hits = await vectorRetriever2(embedder, index).retrieve(query, { limit: opts.limit ?? DEFAULT_LIMIT });
5387
+ const byId = new Map(summarized.map((a) => [a.id, a]));
5388
+ return {
5389
+ query,
5390
+ model,
5391
+ embeddingModel: embedder.model,
5392
+ coverage: map.coverage,
5393
+ matches: hits.map((h) => toMatch(byId.get(h.id), h.score ?? 0))
5394
+ };
5395
+ }
5396
+ function toMatch(area, score) {
5397
+ return {
5398
+ id: area.id,
5399
+ label: area.label,
5400
+ summary: area.summary,
5401
+ files: area.files.slice(0, MATCH_FILE_CAP),
5402
+ size: area.size,
5403
+ score
5404
+ };
5405
+ }
5040
5406
  export {
5041
5407
  ALIAS_BASE_ATTR,
5042
5408
  ALL_ROLES,
5409
+ COMMUNITY_SUMMARY_NAMESPACE,
5043
5410
  COVERAGE_METRIC_NAME,
5044
5411
  CodeGraphStore,
5045
5412
  DEAD_CODE_METRIC_NAMES,
@@ -5063,6 +5430,7 @@ export {
5063
5430
  annotateRoles,
5064
5431
  attributeCoverage,
5065
5432
  buildAliases,
5433
+ buildConventionAreas,
5066
5434
  buildEmbedText,
5067
5435
  buildFileModuleNodes,
5068
5436
  buildIndexerMetrics,
@@ -5089,8 +5457,10 @@ export {
5089
5457
  computeSymbolCoupling,
5090
5458
  computeTestCoverageOwnership,
5091
5459
  createAliasChain,
5460
+ defaultTargetCount,
5092
5461
  describeMetric,
5093
5462
  describeMetrics,
5463
+ detectCommunities,
5094
5464
  detectGitHead,
5095
5465
  detectGitToplevel,
5096
5466
  detectRenames,
@@ -5100,8 +5470,11 @@ export {
5100
5470
  embedSnapshot,
5101
5471
  embedTextsCached,
5102
5472
  externalId,
5473
+ externalToFinding,
5103
5474
  fileId,
5475
+ findConventions,
5104
5476
  findSimilarCapability,
5477
+ getConventionMap,
5105
5478
  getEdgeWeight,
5106
5479
  getLanguageFromPath3 as getLanguageFromPath,
5107
5480
  getSupportedLanguages,
@@ -5153,8 +5526,10 @@ export {
5153
5526
  snapshotSymbolCoupling,
5154
5527
  snapshotViolations,
5155
5528
  structuralSignature,
5529
+ summarizeConventions,
5156
5530
  symbolId,
5157
5531
  testCoverageCountMetrics,
5532
+ toFindings,
5158
5533
  tryEmbedSnapshot,
5159
5534
  validateRules,
5160
5535
  violationKey,