@titan-design/code-graph 0.2.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/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  } from "./chunk-PFI5XMUG.js";
14
14
 
15
15
  // src/store.ts
16
- import { openDatabase, runMigrations } from "@titan-design/store-sqlite";
16
+ import { openDatabase, quoteIdent, runMigrations } from "@titan-design/store-sqlite";
17
17
 
18
18
  // src/schema.ts
19
19
  import { kitMigration } from "@titan-design/store-sqlite";
@@ -62,6 +62,7 @@ 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
66
  var MIGRATIONS = [
66
67
  kitMigration(1, { snapshot: KIT.snapshot, entitySnap: KIT.entitySnap, cacheBlob: KIT.cacheBlob }),
67
68
  { version: 2, name: "code graph columns", up: (db) => db.exec(KIT_EXTENSIONS) },
@@ -122,15 +123,81 @@ function rowToFingerprint(row) {
122
123
  };
123
124
  }
124
125
 
126
+ // src/store-reads.ts
127
+ var EDGE_COLS = "src_id, dst_id, kind, attrs";
128
+ var AGGREGATE_SELECT = `SELECT m.name AS name, n.kind AS node_kind, COUNT(m.value) AS count,
129
+ SUM(m.value) AS sum, MIN(m.value) AS min, MAX(m.value) AS max
130
+ FROM metric m CROSS JOIN node n ON n.snapshot_id = m.snapshot_id AND n.id = m.node_id`;
131
+ var TOP_SELECT = `SELECT m.node_id, m.value, m.unit, n.kind, n.name, n.role
132
+ FROM metric m JOIN node n ON n.snapshot_id = m.snapshot_id AND n.id = m.node_id
133
+ WHERE m.snapshot_id = ? AND m.name = ?`;
134
+ var TOP_ORDER = "ORDER BY m.value DESC LIMIT ?";
135
+ function prepareTargetedStatements(db) {
136
+ return {
137
+ listMetricsForNode: db.prepare(
138
+ "SELECT node_id, name, value, unit FROM metric WHERE snapshot_id = ? AND node_id = ?"
139
+ ),
140
+ // The second arm skips self-loops so an edge from a node to itself comes back once.
141
+ listEdgesTouching: db.prepare(
142
+ `SELECT ${EDGE_COLS} FROM edge WHERE snapshot_id = @snapshotId AND src_id = @nodeId
143
+ UNION ALL
144
+ SELECT ${EDGE_COLS} FROM edge WHERE snapshot_id = @snapshotId AND dst_id = @nodeId AND src_id <> @nodeId`
145
+ ),
146
+ aggregateMetric: db.prepare(
147
+ `${AGGREGATE_SELECT} WHERE m.snapshot_id = ? AND m.name = ? GROUP BY n.kind ORDER BY n.kind`
148
+ ),
149
+ aggregateMetrics: db.prepare(
150
+ `${AGGREGATE_SELECT} WHERE m.snapshot_id = ? GROUP BY m.name, n.kind ORDER BY m.name, n.kind`
151
+ ),
152
+ listMetricNames: db.prepare("SELECT DISTINCT name FROM metric WHERE snapshot_id = ? ORDER BY name"),
153
+ topByMetric: db.prepare(`${TOP_SELECT} ${TOP_ORDER}`),
154
+ topByMetricOfKind: db.prepare(`${TOP_SELECT} AND n.kind = ? ${TOP_ORDER}`)
155
+ };
156
+ }
157
+ function readMetricsForNode(stmts, snapshotId, nodeId) {
158
+ return stmts.listMetricsForNode.all(snapshotId, nodeId).map(rowToMetric);
159
+ }
160
+ function readEdgesTouching(stmts, snapshotId, nodeId) {
161
+ return stmts.listEdgesTouching.all({ snapshotId, nodeId }).map(rowToEdge);
162
+ }
163
+ function readMetricNames(stmts, snapshotId) {
164
+ return stmts.listMetricNames.all(snapshotId).map((r) => r.name);
165
+ }
166
+ function readTopByMetric(stmts, opts) {
167
+ const limit = opts.limit ?? 20;
168
+ const rows = opts.kind === void 0 ? stmts.topByMetric.all(opts.snapshotId, opts.metric, limit) : stmts.topByMetricOfKind.all(opts.snapshotId, opts.metric, opts.kind, limit);
169
+ return rows.map((r) => ({
170
+ nodeId: r.node_id,
171
+ name: r.name,
172
+ kind: r.kind,
173
+ role: r.role,
174
+ value: r.value,
175
+ unit: r.unit
176
+ }));
177
+ }
178
+ function readMetricAggregates(stmts, snapshotId, name) {
179
+ const rows = name === void 0 ? stmts.aggregateMetrics.all(snapshotId) : stmts.aggregateMetric.all(snapshotId, name);
180
+ return rows.map((r) => ({
181
+ name: r.name,
182
+ nodeKind: r.node_kind,
183
+ count: r.count,
184
+ sum: r.sum,
185
+ min: r.min,
186
+ max: r.max
187
+ }));
188
+ }
189
+
125
190
  // src/store.ts
126
191
  var NODE_COLS = "id, kind, name, parent_id, language, role, attrs";
127
192
  var CodeGraphStore = class {
128
193
  constructor(db) {
129
194
  this.db = db;
130
195
  this.statements = prepareStatements(db);
196
+ this.targeted = prepareTargetedStatements(db);
131
197
  }
132
198
  db;
133
199
  statements;
200
+ targeted;
134
201
  createSnapshot(input) {
135
202
  const result = this.statements.insertSnapshot.run({
136
203
  ref: input.ref,
@@ -179,6 +246,21 @@ var CodeGraphStore = class {
179
246
  }
180
247
  })();
181
248
  }
249
+ /**
250
+ * Replace every metric of one `name` for a snapshot in a single transaction —
251
+ * for an overlay such as coverage, which is re-ingested wholesale and must not
252
+ * accumulate stale rows across runs.
253
+ */
254
+ replaceMetricsByName(snapshotId, name, metrics) {
255
+ const del = this.statements.deleteMetricsByName;
256
+ const insert = this.statements.insertMetric;
257
+ this.db.transaction(() => {
258
+ del.run(snapshotId, name);
259
+ for (const m of metrics) {
260
+ insert.run({ snapshotId, nodeId: m.nodeId, name: m.name, value: m.value, unit: m.unit ?? null });
261
+ }
262
+ })();
263
+ }
182
264
  insertAliases(snapshotId, aliases) {
183
265
  const stmt = this.statements.insertAlias;
184
266
  this.db.transaction(() => {
@@ -233,12 +315,53 @@ var CodeGraphStore = class {
233
315
  listMetrics(snapshotId) {
234
316
  return this.statements.listMetrics.all(snapshotId).map(rowToMetric);
235
317
  }
318
+ /** Every metric on one node, from the primary key instead of a whole-snapshot read. */
319
+ listMetricsForNode(snapshotId, nodeId) {
320
+ return readMetricsForNode(this.targeted, snapshotId, nodeId);
321
+ }
322
+ /** Distinct metric names stored for a snapshot, ascending. */
323
+ listMetricNames(snapshotId) {
324
+ return readMetricNames(this.targeted, snapshotId);
325
+ }
326
+ /** The highest-valued nodes for one metric, descending; `limit` defaults to 20. */
327
+ topByMetric(opts) {
328
+ return readTopByMetric(this.targeted, opts);
329
+ }
330
+ /** Edges into or out of one node; `references` edges are excluded unless asked, as in {@link listEdges}. */
331
+ listEdgesTouching(snapshotId, nodeId, opts) {
332
+ const edges = readEdgesTouching(this.targeted, snapshotId, nodeId);
333
+ return opts?.includeReferences ? edges : edges.filter((e) => e.kind !== "references");
334
+ }
335
+ /** Count, sum, min, and max per metric name and node kind; pass `name` for one metric. */
336
+ aggregateMetrics(snapshotId, opts) {
337
+ return readMetricAggregates(this.targeted, snapshotId, opts?.name);
338
+ }
236
339
  listAliases(snapshotId) {
237
340
  return this.statements.listAliases.all(snapshotId).map(rowToAlias);
238
341
  }
239
342
  listFingerprints(snapshotId) {
240
343
  return this.statements.listFingerprints.all(snapshotId).map(rowToFingerprint);
241
344
  }
345
+ /** Drop whole snapshots. The domain tables carry no foreign key, so each is cleared by hand. */
346
+ deleteSnapshots(ids) {
347
+ if (ids.length === 0) return;
348
+ this.db.transaction(() => {
349
+ for (const id of ids) {
350
+ for (const stmt of this.statements.deleteBySnapshot) stmt.run(id);
351
+ }
352
+ })();
353
+ }
354
+ vacuum() {
355
+ this.db.exec("VACUUM");
356
+ }
357
+ countRowsByTable(tables) {
358
+ const out = {};
359
+ for (const table of tables) {
360
+ const row = this.db.prepare(`SELECT COUNT(*) AS n FROM ${quoteIdent(table)}`).get();
361
+ out[table] = row.n;
362
+ }
363
+ return out;
364
+ }
242
365
  close() {
243
366
  this.db.close();
244
367
  }
@@ -281,6 +404,10 @@ function prepareStatements(db) {
281
404
  listAliases: db.prepare("SELECT old_id, new_id, reason FROM id_alias WHERE snapshot_id = ?"),
282
405
  listFingerprints: db.prepare(
283
406
  "SELECT file_id, content_hash, structural_hash FROM file_fingerprint WHERE snapshot_id = ?"
407
+ ),
408
+ deleteMetricsByName: db.prepare("DELETE FROM metric WHERE snapshot_id = ? AND name = ?"),
409
+ deleteBySnapshot: [...SNAPSHOT_SCOPED_TABLES, "snapshot"].map(
410
+ (table) => db.prepare(`DELETE FROM ${quoteIdent(table)} WHERE ${table === "snapshot" ? "id" : "snapshot_id"} = ?`)
284
411
  )
285
412
  };
286
413
  }
@@ -290,6 +417,34 @@ function openCodeGraph(dbPath) {
290
417
  return new CodeGraphStore(db);
291
418
  }
292
419
 
420
+ // src/prune.ts
421
+ var CASCADE_TABLES = SNAPSHOT_SCOPED_TABLES;
422
+ function planPrune(store, options = {}) {
423
+ const keepCount = options.keep ?? 10;
424
+ const keepRefs = new Set(options.keepRefs ?? []);
425
+ const all = store.listSnapshots({ limit: 1e6 });
426
+ const keep = [];
427
+ const remove = [];
428
+ for (let i = 0; i < all.length; i++) {
429
+ const snap = all[i];
430
+ if (i < keepCount || keepRefs.has(snap.ref)) {
431
+ keep.push(snap);
432
+ } else {
433
+ remove.push(snap);
434
+ }
435
+ }
436
+ return { keep, remove };
437
+ }
438
+ function runPrune(store, options = {}) {
439
+ const plan = planPrune(store, options);
440
+ const tables = ["snapshot", ...CASCADE_TABLES];
441
+ const rowsBefore = store.countRowsByTable(tables);
442
+ store.deleteSnapshots(plan.remove.map((s) => s.id));
443
+ if (options.vacuum) store.vacuum();
444
+ const rowsAfter = store.countRowsByTable(tables);
445
+ return { plan, rowsBefore, rowsAfter, vacuumed: !!options.vacuum };
446
+ }
447
+
293
448
  // src/indexer.ts
294
449
  import { realpathSync } from "fs";
295
450
  import * as path9 from "path";
@@ -491,14 +646,14 @@ function enter(scope, segment) {
491
646
  return qualify(scope, segment);
492
647
  }
493
648
  function walkScopes(root, python, visit) {
494
- const step = (node, scope) => {
649
+ const step2 = (node, scope) => {
495
650
  visit(node, scope);
496
651
  const inner = enter(scope, scopeSegment(node, python));
497
652
  for (const child of node.children) {
498
- if (child) step(child, inner);
653
+ if (child) step2(child, inner);
499
654
  }
500
655
  };
501
- step(root, "");
656
+ step2(root, "");
502
657
  }
503
658
 
504
659
  // src/declared-names.ts
@@ -1109,6 +1264,9 @@ function detectGitHead(repoRoot) {
1109
1264
  function isInsideGitRepo(repoRoot) {
1110
1265
  return runGit(repoRoot, ["rev-parse", "--is-inside-work-tree"]) === "true";
1111
1266
  }
1267
+ function resolveGitRef(repoRoot, ref) {
1268
+ return runGit(repoRoot, ["rev-parse", ref]);
1269
+ }
1112
1270
  function detectRenames(options) {
1113
1271
  const threshold = options.similarityThreshold ?? 80;
1114
1272
  const args = [
@@ -1164,6 +1322,160 @@ function buildAliases(repoRoot, pairs) {
1164
1322
  return out;
1165
1323
  }
1166
1324
 
1325
+ // src/identity/lineage.ts
1326
+ var ALIAS_BASE_ATTR = "aliasBase";
1327
+ var DEFAULT_MAX_HOPS = 1e4;
1328
+ function recordedBase(snap) {
1329
+ if (!(ALIAS_BASE_ATTR in snap.attrs)) return void 0;
1330
+ const base = snap.attrs[ALIAS_BASE_ATTR];
1331
+ return typeof base === "number" && base < snap.id ? base : null;
1332
+ }
1333
+ function buildLineage(snapshots) {
1334
+ const out = /* @__PURE__ */ new Map();
1335
+ let lastWithCommit = null;
1336
+ for (const snap of [...snapshots].sort((a, b) => a.id - b.id)) {
1337
+ const recorded = recordedBase(snap);
1338
+ out.set(snap.id, recorded === void 0 ? lastWithCommit : recorded);
1339
+ if (snap.commitHash) lastWithCommit = snap.id;
1340
+ }
1341
+ return out;
1342
+ }
1343
+ function ancestry(lineage, snapshotId, maxHops = DEFAULT_MAX_HOPS) {
1344
+ const out = [];
1345
+ let current = snapshotId;
1346
+ while (current !== null && current !== void 0 && out.length <= maxHops) {
1347
+ out.push(current);
1348
+ current = lineage.get(current);
1349
+ }
1350
+ return out;
1351
+ }
1352
+ function lineagePath(lineage, fromId, toId, maxHops = DEFAULT_MAX_HOPS) {
1353
+ const up = ancestry(lineage, fromId, maxHops);
1354
+ const down = ancestry(lineage, toId, maxHops);
1355
+ const upIndex = new Map(up.map((id, i) => [id, i]));
1356
+ const meet = down.findIndex((id) => upIndex.has(id));
1357
+ if (meet < 0) return null;
1358
+ const backward = up.slice(0, upIndex.get(down[meet])).map((id) => step(id, "backward"));
1359
+ const forward = down.slice(0, meet).reverse().map((id) => step(id, "forward"));
1360
+ return [...backward, ...forward];
1361
+ }
1362
+ function rootPath(lineage, toId, maxHops = DEFAULT_MAX_HOPS) {
1363
+ return ancestry(lineage, toId, maxHops).reverse().map((id) => step(id, "forward"));
1364
+ }
1365
+ function step(snapshotId, direction) {
1366
+ return { snapshotId, direction };
1367
+ }
1368
+
1369
+ // src/identity/alias-chain.ts
1370
+ function stepMap(aliases, direction) {
1371
+ const byId = /* @__PURE__ */ new Map();
1372
+ const sorted = [...aliases].sort((a, b) => compare(a.oldId, b.oldId) || compare(a.newId, b.newId));
1373
+ for (const alias of sorted) {
1374
+ const key = direction === "forward" ? alias.oldId : alias.newId;
1375
+ if (!byId.has(key)) byId.set(key, alias);
1376
+ }
1377
+ return { direction, byId };
1378
+ }
1379
+ function compare(a, b) {
1380
+ return a < b ? -1 : a > b ? 1 : 0;
1381
+ }
1382
+ function overallReason(hops) {
1383
+ if (hops.length === 0) return void 0;
1384
+ return hops.some((h) => h.reason === "move") ? "move" : hops[hops.length - 1].reason;
1385
+ }
1386
+ function walk(requestedId, maps) {
1387
+ let id = requestedId;
1388
+ const hops = [];
1389
+ for (const { direction, byId } of maps) {
1390
+ const alias = byId.get(id);
1391
+ if (!alias) continue;
1392
+ hops.push(alias);
1393
+ id = direction === "forward" ? alias.newId : alias.oldId;
1394
+ }
1395
+ return { requestedId, id, hops, reason: overallReason(hops) };
1396
+ }
1397
+ function chainSteps(input) {
1398
+ if (input.from === null) return { steps: rootPath(input.lineage, input.to, input.maxHops), connected: true };
1399
+ const path12 = lineagePath(input.lineage, input.from, input.to, input.maxHops);
1400
+ if (path12) return { steps: path12, connected: true };
1401
+ return { steps: [{ snapshotId: input.to, direction: "forward" }], connected: false };
1402
+ }
1403
+ function createAliasChain(input) {
1404
+ const { steps, connected } = chainSteps(input);
1405
+ const maps = steps.map((s) => stepMap(input.loadAliases(s.snapshotId), s.direction));
1406
+ const cache = /* @__PURE__ */ new Map();
1407
+ const trace = (id) => {
1408
+ let hit = cache.get(id);
1409
+ if (!hit) cache.set(id, hit = walk(id, maps));
1410
+ return hit;
1411
+ };
1412
+ return { fromSnapshotId: input.from, toSnapshotId: input.to, connected, steps, resolve: (id) => trace(id).id, trace };
1413
+ }
1414
+
1415
+ // src/identity/store-identity.ts
1416
+ function allSnapshots(store) {
1417
+ return store.db.prepare("SELECT * FROM snapshot ORDER BY id").all().map(rowToSnapshot);
1418
+ }
1419
+ function loadLineage(store) {
1420
+ return buildLineage(allSnapshots(store));
1421
+ }
1422
+ function aliasChain(store, fromSnapshotId, toSnapshotId, options = {}) {
1423
+ return createAliasChain({
1424
+ lineage: loadLineage(store),
1425
+ loadAliases: (id) => store.listAliases(id),
1426
+ from: fromSnapshotId,
1427
+ to: toSnapshotId,
1428
+ maxHops: options.maxHops
1429
+ });
1430
+ }
1431
+ function resolveAlias(store, id, toSnapshotId, options = {}) {
1432
+ return aliasChain(store, options.fromSnapshotId ?? null, toSnapshotId, options).trace(id);
1433
+ }
1434
+ function priorSnapshotForRef(store, ref, options = {}) {
1435
+ const before = options.before ?? Number.POSITIVE_INFINITY;
1436
+ const candidates2 = allSnapshots(store).filter((s) => s.id < before).reverse();
1437
+ const commit = options.repoRoot ? resolveGitRef(options.repoRoot, `${ref}^{commit}`) : null;
1438
+ const byCommit = commit ? candidates2.find((s) => s.commitHash === commit) : void 0;
1439
+ return byCommit ?? candidates2.find((s) => s.ref === ref) ?? null;
1440
+ }
1441
+ function aliasBaseFor(store, ref) {
1442
+ const committed = allSnapshots(store).filter((s) => s.commitHash).reverse();
1443
+ return committed.find((s) => s.ref === ref) ?? committed[0] ?? null;
1444
+ }
1445
+
1446
+ // src/identity/index-aliases.ts
1447
+ var SYMBOLS_OF_FILE = `SELECT id FROM node
1448
+ WHERE snapshot_id = ? AND id >= ? AND id < ? AND kind = 'symbol'`;
1449
+ function symbolIdsOf(store, snapshotId, fileId2) {
1450
+ const rows = store.db.prepare(SYMBOLS_OF_FILE).all(snapshotId, `${fileId2}#`, `${fileId2}$`);
1451
+ return rows.map((r) => r.id);
1452
+ }
1453
+ function movedSymbolAliases(store, baseSnapshotId, fileAliases, nodes) {
1454
+ const out = [];
1455
+ for (const alias of fileAliases) {
1456
+ if (nodes.get(alias.newId)?.kind !== "file") continue;
1457
+ for (const oldId of symbolIdsOf(store, baseSnapshotId, alias.oldId)) {
1458
+ const newId = alias.newId + SYMBOL_ID_SEP + oldId.slice(alias.oldId.length + 1);
1459
+ if (nodes.get(newId)?.kind === "symbol") out.push({ oldId, newId, reason: alias.reason });
1460
+ }
1461
+ }
1462
+ return out;
1463
+ }
1464
+ function renameAliases(input, baseCommit) {
1465
+ if (input.detectRenames === false || !isInsideGitRepo(input.rootDir)) return [];
1466
+ const target = input.commitHash ?? detectGitHead(input.rootDir) ?? void 0;
1467
+ if (target === baseCommit) return [];
1468
+ const pairs = detectRenames({ repoRoot: input.rootDir, fromCommit: baseCommit, toCommit: target });
1469
+ return buildAliases(input.idRoot, pairs);
1470
+ }
1471
+ function computeAliasBridge(store, input) {
1472
+ const base = aliasBaseFor(store, input.ref);
1473
+ if (!base?.commitHash) return { baseSnapshotId: null, aliases: [] };
1474
+ const fileAliases = renameAliases(input, base.commitHash);
1475
+ const symbols = movedSymbolAliases(store, base.id, fileAliases, input.nodes);
1476
+ return { baseSnapshotId: base.id, aliases: [...fileAliases, ...symbols] };
1477
+ }
1478
+
1167
1479
  // src/generated.ts
1168
1480
  import { readFileSync } from "fs";
1169
1481
  import * as path7 from "path";
@@ -1932,17 +2244,17 @@ function ownLocalNames(fn) {
1932
2244
  const body = fn.childForFieldName("body");
1933
2245
  if (!body) return [];
1934
2246
  const out = [];
1935
- const walk = (node) => {
2247
+ const walk2 = (node) => {
1936
2248
  if (FUNCTION_TYPES.has(node.type)) return;
1937
2249
  if (node.type === "variable_declarator") {
1938
2250
  const name = node.childForFieldName("name");
1939
2251
  if (name?.type === "identifier") out.push(name.text);
1940
2252
  }
1941
2253
  for (const child of node.namedChildren) {
1942
- if (child) walk(child);
2254
+ if (child) walk2(child);
1943
2255
  }
1944
2256
  };
1945
- walk(body);
2257
+ walk2(body);
1946
2258
  return out;
1947
2259
  }
1948
2260
  function collectBindingDecls(fn, ids, nameCount) {
@@ -1951,25 +2263,25 @@ function collectBindingDecls(fn, ids, nameCount) {
1951
2263
  ids.add(n.id);
1952
2264
  nameCount.set(n.text, (nameCount.get(n.text) ?? 0) + 1);
1953
2265
  };
1954
- const walk = (node) => {
2266
+ const walk2 = (node) => {
1955
2267
  if (PARAM_TYPES.has(node.type)) mark(node.namedChild(0));
1956
2268
  else if (node.type === "variable_declarator") mark(node.childForFieldName("name"));
1957
2269
  for (const child of node.namedChildren) {
1958
- if (child) walk(child);
2270
+ if (child) walk2(child);
1959
2271
  }
1960
2272
  };
1961
- walk(fn);
2273
+ walk2(fn);
1962
2274
  }
1963
2275
  function countReferences(fn, declIds, out) {
1964
- const walk = (node) => {
2276
+ const walk2 = (node) => {
1965
2277
  if ((node.type === "identifier" || node.type === "shorthand_property_identifier") && !declIds.has(node.id)) {
1966
2278
  out.set(node.text, (out.get(node.text) ?? 0) + 1);
1967
2279
  }
1968
2280
  for (const child of node.namedChildren) {
1969
- if (child) walk(child);
2281
+ if (child) walk2(child);
1970
2282
  }
1971
2283
  };
1972
- walk(fn);
2284
+ walk2(fn);
1973
2285
  }
1974
2286
 
1975
2287
  // src/analysis/growth-risk.ts
@@ -2650,7 +2962,7 @@ function qualifiedByName(file) {
2650
2962
  }
2651
2963
 
2652
2964
  // src/indexer.ts
2653
- var INDEX_VERSION = "0.14.0";
2965
+ var INDEX_VERSION = "0.15.0";
2654
2966
  var LANGUAGES = ["typescript", "python"];
2655
2967
  function canonicalizePath(p) {
2656
2968
  try {
@@ -2668,26 +2980,16 @@ function countByKind(items) {
2668
2980
  for (const item of items) out[item.kind] = (out[item.kind] ?? 0) + 1;
2669
2981
  return out;
2670
2982
  }
2671
- function persist(store, ref, commitHash, accumulator, aliases, metrics) {
2672
- const snapshotId = store.createSnapshot({ ref, commitHash, indexVersion: INDEX_VERSION });
2983
+ function persist(store, snapshot, accumulator, aliases, metrics) {
2984
+ const { ref, commitHash, aliasBase } = snapshot;
2985
+ const attrs = { [ALIAS_BASE_ATTR]: aliasBase };
2986
+ const snapshotId = store.createSnapshot({ ref, commitHash, indexVersion: INDEX_VERSION, attrs });
2673
2987
  store.insertNodes(snapshotId, [...accumulator.nodes.values()]);
2674
2988
  store.insertEdges(snapshotId, [...accumulator.edges.values()]);
2675
2989
  if (aliases.length > 0) store.insertAliases(snapshotId, aliases);
2676
2990
  if (metrics.length > 0) store.insertMetrics(snapshotId, metrics);
2677
2991
  return snapshotId;
2678
2992
  }
2679
- function findPriorCommit(store) {
2680
- return store.listSnapshots({ limit: 50 }).find((s) => s.commitHash)?.commitHash ?? null;
2681
- }
2682
- function resolveAliases(rootDir, idRoot, options, store) {
2683
- if (options.detectRenames === false) return [];
2684
- if (!isInsideGitRepo(rootDir)) return [];
2685
- const priorCommit = findPriorCommit(store);
2686
- if (!priorCommit) return [];
2687
- const target = options.commitHash ?? detectGitHead(rootDir) ?? void 0;
2688
- if (target === priorCommit) return [];
2689
- return buildAliases(idRoot, detectRenames({ repoRoot: rootDir, fromCommit: priorCommit, toCommit: target }));
2690
- }
2691
2993
  function resolveSymbolAliases(store, idRoot, parsedByPath, nodeIds) {
2692
2994
  const prior = store.listSnapshots({ limit: 1 })[0];
2693
2995
  if (!prior || !predatesQualifiedSymbols(prior.indexVersion)) return [];
@@ -2745,12 +3047,15 @@ async function indexPaths(store, options) {
2745
3047
  history: options.computeChurn === false ? void 0 : historyOptions(options)
2746
3048
  });
2747
3049
  const rootDir = rootDirs[0];
3050
+ const ref = options.ref ?? "wd";
3051
+ const bridge = computeAliasBridge(store, { ...options, rootDir, idRoot, ref, nodes: accumulator.nodes });
2748
3052
  const aliases = [
2749
- ...resolveAliases(rootDir, idRoot, options, store),
3053
+ ...bridge.aliases,
2750
3054
  ...resolveSymbolAliases(store, idRoot, parsedByPath, new Set(accumulator.nodes.keys()))
2751
3055
  ];
2752
3056
  const commitHash = options.commitHash ?? detectGitHead(rootDir) ?? void 0;
2753
- const snapshotId = persist(store, options.ref ?? "wd", commitHash, accumulator, aliases, metrics);
3057
+ const snapshot = { ref, commitHash, aliasBase: bridge.baseSnapshotId };
3058
+ const snapshotId = persist(store, snapshot, accumulator, aliases, metrics);
2754
3059
  store.insertFingerprints(snapshotId, buildFingerprints(readFiles, idRoot, classified.structuralByFileId));
2755
3060
  return {
2756
3061
  snapshotId,
@@ -2770,6 +3075,143 @@ async function indexPaths(store, options) {
2770
3075
  // src/index.ts
2771
3076
  import { getLanguageFromPath as getLanguageFromPath3, getSupportedLanguages, parseFile as parseFile2, shouldIncludeFile as shouldIncludeFile2 } from "@titan-design/code-parser";
2772
3077
 
3078
+ // src/extractors/deep-ast.ts
3079
+ import { readFileSync as readFileSync2 } from "fs";
3080
+ import * as path10 from "path";
3081
+ import {
3082
+ Node as Node4,
3083
+ Project as Project2,
3084
+ ScriptTarget as ScriptTarget2
3085
+ } from "ts-morph";
3086
+ function computeDeepAst(input) {
3087
+ const sf = loadSourceFile(input.absPath);
3088
+ if (!sf) return null;
3089
+ const target = input.symbolName ? `${input.filePath}#${input.symbolName}` : input.filePath;
3090
+ return input.symbolName ? symbolDeepAst(sf, input.symbolName, target) : fileDeepAst(sf, target);
3091
+ }
3092
+ function loadSourceFile(absPath) {
3093
+ try {
3094
+ const text = readFileSync2(absPath, "utf8");
3095
+ const project = new Project2({
3096
+ useInMemoryFileSystem: true,
3097
+ compilerOptions: { target: ScriptTarget2.Latest, allowJs: true }
3098
+ });
3099
+ return project.createSourceFile(`f${path10.extname(absPath) || ".ts"}`, text);
3100
+ } catch {
3101
+ return null;
3102
+ }
3103
+ }
3104
+ function symbolDeepAst(sf, name, target) {
3105
+ const decl = lookupDeclaration(sf, name);
3106
+ if (!decl) {
3107
+ return { ...empty(target, "symbol"), note: "declaration not found in source" };
3108
+ }
3109
+ const text = declarationText(decl);
3110
+ return {
3111
+ target,
3112
+ kind: "symbol",
3113
+ declarationKind: decl.getKindName(),
3114
+ signature: text.signature ?? null,
3115
+ purpose: text.purpose ?? null,
3116
+ params: paramsOf(decl),
3117
+ returnType: returnTypeOf(decl),
3118
+ members: membersOf(decl)
3119
+ };
3120
+ }
3121
+ function fileDeepAst(sf, target) {
3122
+ return {
3123
+ ...empty(target, "file"),
3124
+ declarationKind: "SourceFile",
3125
+ members: exportedDeclarations(sf)
3126
+ };
3127
+ }
3128
+ function empty(target, kind) {
3129
+ return {
3130
+ target,
3131
+ kind,
3132
+ declarationKind: null,
3133
+ signature: null,
3134
+ purpose: null,
3135
+ params: [],
3136
+ returnType: null,
3137
+ members: []
3138
+ };
3139
+ }
3140
+ function callableOf(decl) {
3141
+ if (Node4.isFunctionDeclaration(decl) || Node4.isMethodDeclaration(decl)) return decl;
3142
+ if (Node4.isVariableDeclaration(decl)) {
3143
+ const init = decl.getInitializer();
3144
+ if (init && (Node4.isArrowFunction(init) || Node4.isFunctionExpression(init))) return init;
3145
+ }
3146
+ return null;
3147
+ }
3148
+ function paramsOf(decl) {
3149
+ const callable = callableOf(decl);
3150
+ if (!callable) return [];
3151
+ return callable.getParameters().map((p) => ({
3152
+ name: p.getName(),
3153
+ type: p.getTypeNode()?.getText() ?? null
3154
+ }));
3155
+ }
3156
+ function returnTypeOf(decl) {
3157
+ return callableOf(decl)?.getReturnTypeNode()?.getText() ?? null;
3158
+ }
3159
+ function membersOf(decl) {
3160
+ if (Node4.isClassDeclaration(decl)) return classMembers(decl);
3161
+ if (Node4.isInterfaceDeclaration(decl)) return interfaceMembers(decl);
3162
+ return [];
3163
+ }
3164
+ function classMembers(cls) {
3165
+ return cls.getMembers().map((m) => ({
3166
+ name: memberName(m),
3167
+ memberKind: m.getKindName(),
3168
+ signature: memberSignature(m),
3169
+ isStatic: Node4.isStaticable(m) ? m.isStatic() : false
3170
+ }));
3171
+ }
3172
+ function interfaceMembers(iface) {
3173
+ return iface.getMembers().map((m) => ({
3174
+ name: memberName(m),
3175
+ memberKind: m.getKindName(),
3176
+ signature: memberSignature(m),
3177
+ isStatic: false
3178
+ }));
3179
+ }
3180
+ function exportedDeclarations(sf) {
3181
+ const out = [];
3182
+ for (const [name, decls] of sf.getExportedDeclarations()) {
3183
+ const decl = decls[0];
3184
+ if (!decl) continue;
3185
+ out.push({
3186
+ name,
3187
+ memberKind: decl.getKindName(),
3188
+ signature: declarationText(decl).signature ?? memberSignature(decl),
3189
+ isStatic: false
3190
+ });
3191
+ }
3192
+ return out;
3193
+ }
3194
+ function memberName(m) {
3195
+ if (Node4.isConstructorDeclaration(m)) return "constructor";
3196
+ return Node4.hasName(m) ? m.getName() : "";
3197
+ }
3198
+ function memberSignature(m) {
3199
+ if (Node4.isMethodDeclaration(m) || Node4.isMethodSignature(m) || Node4.isConstructorDeclaration(m)) {
3200
+ const params = m.getParameters().map((p) => oneLine2(p.getText())).join(", ");
3201
+ const ret = m.getReturnTypeNode()?.getText();
3202
+ const name = Node4.isConstructorDeclaration(m) ? "constructor" : memberName(m);
3203
+ return oneLine2(`${name}(${params})${ret ? `: ${ret}` : ""}`);
3204
+ }
3205
+ if (Node4.isPropertyDeclaration(m) || Node4.isPropertySignature(m)) {
3206
+ const type = m.getTypeNode()?.getText();
3207
+ return oneLine2(`${memberName(m)}${type ? `: ${type}` : ""}`);
3208
+ }
3209
+ return null;
3210
+ }
3211
+ function oneLine2(s) {
3212
+ return s.replace(/\s+/g, " ").trim();
3213
+ }
3214
+
2773
3215
  // src/aliases.ts
2774
3216
  var METRIC_ALIASES = /* @__PURE__ */ new Map([
2775
3217
  ["lines", "loc"],
@@ -3121,6 +3563,199 @@ function comparePairs(a, b) {
3121
3563
  return a.bId < b.bId ? -1 : a.bId > b.bId ? 1 : 0;
3122
3564
  }
3123
3565
 
3566
+ // src/analysis/partition-quality.ts
3567
+ var FOUNDATION_INSTABILITY_MAX = 0.3;
3568
+ var TOP_INSTABILITY_MIN = 0.9;
3569
+ var WEAK_COHESION_MAX = 0.5;
3570
+ var TIGHT_INTENSITY_MIN = 0.6;
3571
+ var MODERATE_INTENSITY_MIN = 0.3;
3572
+ function computePartitionQuality(input) {
3573
+ const resolveBarrels = input.resolveBarrels ?? false;
3574
+ const pkgByFile = invertBuckets(input.fileByPackage);
3575
+ const barrelTargets = resolveBarrels ? buildBarrelTargetMap(input.nodes, input.edges) : /* @__PURE__ */ new Map();
3576
+ const resolvedPairs = buildResolvedPairs(
3577
+ input.edges,
3578
+ pkgByFile,
3579
+ barrelTargets,
3580
+ resolveBarrels
3581
+ );
3582
+ const counts = countByPackage(resolvedPairs);
3583
+ const totalEdges = resolvedPairs.length;
3584
+ const filesByPkg = filesPerPackage(input.fileByPackage);
3585
+ const abstractByPkg = countAbstractFiles(input.nodes, pkgByFile);
3586
+ const perPackage = input.packages.map(
3587
+ (p) => buildPackageStats(p.id, counts, filesByPkg, abstractByPkg)
3588
+ );
3589
+ const pairCoupling = buildPairCoupling(resolvedPairs, filesByPkg);
3590
+ const modularityQ = computeModularity(perPackage, totalEdges);
3591
+ const flagsCount = perPackage.reduce((acc, p) => acc + p.flags.length, 0) + pairCoupling.filter((c) => c.flag === "tight").length;
3592
+ return { modularityQ, totalEdges, perPackage, pairCoupling, flagsCount };
3593
+ }
3594
+ function invertBuckets(fileByPackage) {
3595
+ const out = /* @__PURE__ */ new Map();
3596
+ for (const [pkgId, files] of fileByPackage) {
3597
+ if (pkgId === "") continue;
3598
+ for (const f of files) out.set(f, pkgId);
3599
+ }
3600
+ return out;
3601
+ }
3602
+ function filesPerPackage(fileByPackage) {
3603
+ const out = /* @__PURE__ */ new Map();
3604
+ for (const [pkgId, files] of fileByPackage) {
3605
+ out.set(pkgId, files.length);
3606
+ }
3607
+ return out;
3608
+ }
3609
+ function buildBarrelTargetMap(nodes, edges) {
3610
+ const barrelIds = new Set(
3611
+ nodes.filter((n) => n.role === "barrel").map((n) => n.id)
3612
+ );
3613
+ if (barrelIds.size === 0) return /* @__PURE__ */ new Map();
3614
+ const directReExports = /* @__PURE__ */ new Map();
3615
+ for (const e of edges) {
3616
+ if (!barrelIds.has(e.srcId)) continue;
3617
+ if (e.kind !== "re-exports") continue;
3618
+ let list = directReExports.get(e.srcId);
3619
+ if (!list) {
3620
+ list = [];
3621
+ directReExports.set(e.srcId, list);
3622
+ }
3623
+ list.push(e.dstId);
3624
+ }
3625
+ const resolved = /* @__PURE__ */ new Map();
3626
+ const resolve4 = (barrel, visiting) => {
3627
+ if (resolved.has(barrel)) return resolved.get(barrel);
3628
+ if (visiting.has(barrel)) return [];
3629
+ visiting.add(barrel);
3630
+ const out = [];
3631
+ for (const target of directReExports.get(barrel) ?? []) {
3632
+ if (barrelIds.has(target)) {
3633
+ for (const inner of resolve4(target, visiting)) out.push(inner);
3634
+ } else {
3635
+ out.push(target);
3636
+ }
3637
+ }
3638
+ visiting.delete(barrel);
3639
+ resolved.set(barrel, out);
3640
+ return out;
3641
+ };
3642
+ for (const id of barrelIds) resolve4(id, /* @__PURE__ */ new Set());
3643
+ return resolved;
3644
+ }
3645
+ function buildResolvedPairs(edges, pkgByFile, barrelTargets, resolveBarrels) {
3646
+ const out = [];
3647
+ for (const e of edges) {
3648
+ if (resolveBarrels && e.kind === "re-exports") continue;
3649
+ const fromPkg = pkgByFile.get(e.srcId);
3650
+ if (!fromPkg) continue;
3651
+ const targets = barrelTargets.get(e.dstId) ?? [e.dstId];
3652
+ for (const dst of targets) {
3653
+ const toPkg = pkgByFile.get(dst);
3654
+ if (toPkg) out.push({ from: fromPkg, to: toPkg });
3655
+ }
3656
+ }
3657
+ return out;
3658
+ }
3659
+ function countByPackage(pairs) {
3660
+ const out = /* @__PURE__ */ new Map();
3661
+ const get = (id) => {
3662
+ let c = out.get(id);
3663
+ if (!c) {
3664
+ c = { internal: 0, outgoing: 0, incoming: 0 };
3665
+ out.set(id, c);
3666
+ }
3667
+ return c;
3668
+ };
3669
+ for (const p of pairs) {
3670
+ if (p.from === p.to) {
3671
+ get(p.from).internal += 1;
3672
+ } else {
3673
+ get(p.from).outgoing += 1;
3674
+ get(p.to).incoming += 1;
3675
+ }
3676
+ }
3677
+ return out;
3678
+ }
3679
+ function buildPackageStats(pkgId, counts, filesByPkg, abstractByPkg) {
3680
+ const c = counts.get(pkgId) ?? { internal: 0, outgoing: 0, incoming: 0 };
3681
+ const cohesion = ratioOrZero(c.internal, c.internal + c.outgoing);
3682
+ const instability = ratioOrZero(c.outgoing, c.outgoing + c.incoming);
3683
+ const fileCount = filesByPkg.get(pkgId) ?? 0;
3684
+ const abstractness = ratioOrZero(abstractByPkg.get(pkgId) ?? 0, fileCount);
3685
+ const layer = classifyLayer(instability);
3686
+ const flags = [];
3687
+ if (layer !== "top" && cohesion < WEAK_COHESION_MAX && c.internal + c.outgoing > 0) {
3688
+ flags.push("weak-boundary");
3689
+ }
3690
+ return {
3691
+ pkgId,
3692
+ fileCount,
3693
+ internalEdges: c.internal,
3694
+ outgoingEdges: c.outgoing,
3695
+ incomingEdges: c.incoming,
3696
+ cohesion,
3697
+ instability,
3698
+ abstractness,
3699
+ layer,
3700
+ flags
3701
+ };
3702
+ }
3703
+ function countAbstractFiles(nodes, pkgByFile) {
3704
+ const out = /* @__PURE__ */ new Map();
3705
+ for (const n of nodes) {
3706
+ if (n.kind !== "file" || n.role !== "types") continue;
3707
+ const pkg = pkgByFile.get(n.id);
3708
+ if (pkg) out.set(pkg, (out.get(pkg) ?? 0) + 1);
3709
+ }
3710
+ return out;
3711
+ }
3712
+ function ratioOrZero(num, denom) {
3713
+ return denom === 0 ? 0 : num / denom;
3714
+ }
3715
+ function classifyLayer(instability) {
3716
+ if (instability <= FOUNDATION_INSTABILITY_MAX) return "foundation";
3717
+ if (instability >= TOP_INSTABILITY_MIN) return "top";
3718
+ return "middle";
3719
+ }
3720
+ function buildPairCoupling(pairs, filesByPkg) {
3721
+ const counts = /* @__PURE__ */ new Map();
3722
+ for (const p of pairs) {
3723
+ if (p.from === p.to) continue;
3724
+ const key = JSON.stringify([p.from, p.to]);
3725
+ const entry = counts.get(key);
3726
+ if (entry) entry.edges += 1;
3727
+ else counts.set(key, { from: p.from, to: p.to, edges: 1 });
3728
+ }
3729
+ const out = [];
3730
+ for (const { from, to, edges } of counts.values()) {
3731
+ const filesFrom = filesByPkg.get(from) ?? 0;
3732
+ const intensity = filesFrom === 0 ? 0 : edges / filesFrom;
3733
+ const flag = classifyPairIntensity(intensity);
3734
+ out.push({ from, to, edges, intensity, flag });
3735
+ }
3736
+ return out.sort(comparePairs2);
3737
+ }
3738
+ function classifyPairIntensity(intensity) {
3739
+ if (intensity >= TIGHT_INTENSITY_MIN) return "tight";
3740
+ if (intensity >= MODERATE_INTENSITY_MIN) return "moderate";
3741
+ return "none";
3742
+ }
3743
+ function comparePairs2(a, b) {
3744
+ if (a.from !== b.from) return a.from < b.from ? -1 : 1;
3745
+ return a.to < b.to ? -1 : 1;
3746
+ }
3747
+ function computeModularity(perPackage, totalEdges) {
3748
+ if (totalEdges === 0) return 0;
3749
+ let Q = 0;
3750
+ for (const p of perPackage) {
3751
+ const e_cc = p.internalEdges / totalEdges;
3752
+ const touching = p.internalEdges * 2 + p.outgoingEdges + p.incomingEdges;
3753
+ const a_c = touching / (2 * totalEdges);
3754
+ Q += e_cc - a_c * a_c;
3755
+ }
3756
+ return Q;
3757
+ }
3758
+
3124
3759
  // src/analysis/snapshot.ts
3125
3760
  function snapshotPageRank(store, snapshotId, options = {}) {
3126
3761
  return computePageRank(store.listNodes(snapshotId), store.listEdges(snapshotId), options);
@@ -3148,6 +3783,397 @@ function listEdges(store, snapshotId, opts) {
3148
3783
  function listMetrics(store, snapshotId) {
3149
3784
  return store.listMetrics(snapshotId);
3150
3785
  }
3786
+ function listMetricsForNode(store, snapshotId, nodeId) {
3787
+ return store.listMetricsForNode(snapshotId, nodeId);
3788
+ }
3789
+ function listEdgesTouching(store, snapshotId, nodeId, opts) {
3790
+ return store.listEdgesTouching(snapshotId, nodeId, opts);
3791
+ }
3792
+ function aggregateMetrics(store, snapshotId, opts) {
3793
+ return store.aggregateMetrics(snapshotId, opts);
3794
+ }
3795
+
3796
+ // src/catalogue/entries.ts
3797
+ var FILE = ["file"];
3798
+ var SYMBOL = ["symbol"];
3799
+ var STRUCTURAL = ["file", "module", "external"];
3800
+ var DEGREE = [
3801
+ {
3802
+ name: "fan_in",
3803
+ unit: "count",
3804
+ appliesTo: STRUCTURAL,
3805
+ rollup: "none",
3806
+ direction: "neutral",
3807
+ absent: "zero",
3808
+ source: "degree",
3809
+ description: "Import and re-export edges into the node. Summing over a group would count its internal edges."
3810
+ },
3811
+ {
3812
+ name: "fan_out",
3813
+ unit: "count",
3814
+ appliesTo: STRUCTURAL,
3815
+ rollup: "none",
3816
+ direction: "higher-worse",
3817
+ absent: "zero",
3818
+ source: "degree",
3819
+ description: "Import and re-export edges out of the node. Summing over a group would count its internal edges."
3820
+ },
3821
+ {
3822
+ name: "instability",
3823
+ unit: "ratio",
3824
+ appliesTo: STRUCTURAL,
3825
+ rollup: "none",
3826
+ direction: "neutral",
3827
+ absent: "exclude",
3828
+ source: "degree",
3829
+ description: "fan_out / (fan_in + fan_out); written only when the node has an import edge."
3830
+ },
3831
+ {
3832
+ name: "utilization",
3833
+ unit: "count",
3834
+ appliesTo: [...STRUCTURAL, "symbol"],
3835
+ rollup: "none",
3836
+ direction: "neutral",
3837
+ absent: "zero",
3838
+ source: "degree",
3839
+ description: "Inbound edges weighted by reference count over the barrel-resolved graph: how heavily a node is used."
3840
+ }
3841
+ ];
3842
+ var SOURCE = [
3843
+ {
3844
+ name: "loc",
3845
+ unit: "lines",
3846
+ appliesTo: FILE,
3847
+ rollup: "sum",
3848
+ direction: "higher-worse",
3849
+ absent: "zero",
3850
+ source: "source-metrics",
3851
+ description: "Non-blank lines in the file."
3852
+ },
3853
+ {
3854
+ name: "function_count",
3855
+ unit: "count",
3856
+ appliesTo: FILE,
3857
+ rollup: "sum",
3858
+ direction: "neutral",
3859
+ absent: "zero",
3860
+ source: "source-metrics",
3861
+ description: "Functions, methods, and arrow functions declared in the file."
3862
+ },
3863
+ {
3864
+ name: "cyclomatic_max",
3865
+ unit: "count",
3866
+ appliesTo: FILE,
3867
+ rollup: "max",
3868
+ direction: "higher-worse",
3869
+ absent: "exclude",
3870
+ source: "source-metrics",
3871
+ description: "Highest cyclomatic complexity of any function in the file."
3872
+ },
3873
+ {
3874
+ name: "cyclomatic_sum",
3875
+ unit: "count",
3876
+ appliesTo: FILE,
3877
+ rollup: "sum",
3878
+ direction: "higher-worse",
3879
+ absent: "zero",
3880
+ source: "source-metrics",
3881
+ description: "Cyclomatic complexity summed over the file's functions."
3882
+ },
3883
+ {
3884
+ name: "cognitive_max",
3885
+ unit: "count",
3886
+ appliesTo: FILE,
3887
+ rollup: "max",
3888
+ direction: "higher-worse",
3889
+ absent: "exclude",
3890
+ source: "source-metrics",
3891
+ description: "Highest cognitive complexity of any function in the file."
3892
+ },
3893
+ {
3894
+ name: "cognitive_sum",
3895
+ unit: "count",
3896
+ appliesTo: FILE,
3897
+ rollup: "sum",
3898
+ direction: "higher-worse",
3899
+ absent: "zero",
3900
+ source: "source-metrics",
3901
+ description: "Cognitive complexity summed over the file's functions."
3902
+ },
3903
+ {
3904
+ name: "max_nesting_depth",
3905
+ unit: "count",
3906
+ appliesTo: FILE,
3907
+ rollup: "max",
3908
+ direction: "higher-worse",
3909
+ absent: "exclude",
3910
+ source: "source-metrics",
3911
+ description: "Deepest block nesting inside any function in the file."
3912
+ },
3913
+ {
3914
+ name: "symbol_cognitive",
3915
+ unit: "count",
3916
+ appliesTo: SYMBOL,
3917
+ rollup: "max",
3918
+ direction: "higher-worse",
3919
+ absent: "exclude",
3920
+ source: "source-metrics",
3921
+ description: "Cognitive complexity of the function a symbol names; the max when several functions share the name."
3922
+ },
3923
+ {
3924
+ name: "symbol_cyclomatic",
3925
+ unit: "count",
3926
+ appliesTo: SYMBOL,
3927
+ rollup: "max",
3928
+ direction: "higher-worse",
3929
+ absent: "exclude",
3930
+ source: "source-metrics",
3931
+ description: "Cyclomatic complexity of the function a symbol names; the max when several functions share the name."
3932
+ },
3933
+ {
3934
+ name: "class_count",
3935
+ unit: "count",
3936
+ appliesTo: FILE,
3937
+ rollup: "sum",
3938
+ direction: "neutral",
3939
+ absent: "zero",
3940
+ source: "lcom",
3941
+ description: "Classes declared in the file; written only when there is one."
3942
+ },
3943
+ {
3944
+ name: "lcom4_max",
3945
+ unit: "count",
3946
+ appliesTo: FILE,
3947
+ rollup: "max",
3948
+ direction: "higher-worse",
3949
+ absent: "exclude",
3950
+ source: "lcom",
3951
+ description: "Highest LCOM4 of any class in the file; above 1 means the class could split."
3952
+ }
3953
+ ];
3954
+ var SMELLS = [
3955
+ {
3956
+ name: "unreachable_statements",
3957
+ unit: "count",
3958
+ appliesTo: FILE,
3959
+ rollup: "sum",
3960
+ direction: "higher-worse",
3961
+ absent: "zero",
3962
+ source: "dead-code",
3963
+ description: "Statements after an unconditional return, throw, break, or continue. TypeScript only."
3964
+ },
3965
+ {
3966
+ name: "unused_locals",
3967
+ unit: "count",
3968
+ appliesTo: FILE,
3969
+ rollup: "sum",
3970
+ direction: "higher-worse",
3971
+ absent: "zero",
3972
+ source: "dead-code",
3973
+ description: "Local bindings never read. TypeScript only."
3974
+ },
3975
+ {
3976
+ name: "unused_params",
3977
+ unit: "count",
3978
+ appliesTo: FILE,
3979
+ rollup: "sum",
3980
+ direction: "higher-worse",
3981
+ absent: "zero",
3982
+ source: "dead-code",
3983
+ description: "Trailing parameters never read. TypeScript only."
3984
+ },
3985
+ {
3986
+ name: "loop_depth",
3987
+ unit: "count",
3988
+ appliesTo: FILE,
3989
+ rollup: "max",
3990
+ direction: "higher-worse",
3991
+ absent: "zero",
3992
+ source: "growth-risk",
3993
+ description: "Deepest lexical loop nesting; written at 2 or more."
3994
+ },
3995
+ {
3996
+ name: "recursive_functions",
3997
+ unit: "count",
3998
+ appliesTo: FILE,
3999
+ rollup: "sum",
4000
+ direction: "higher-worse",
4001
+ absent: "zero",
4002
+ source: "growth-risk",
4003
+ description: "Functions that call themselves by name. TypeScript only."
4004
+ },
4005
+ {
4006
+ name: "search_in_loop",
4007
+ unit: "count",
4008
+ appliesTo: FILE,
4009
+ rollup: "sum",
4010
+ direction: "higher-worse",
4011
+ absent: "zero",
4012
+ source: "growth-risk",
4013
+ description: "Linear searches such as .includes or .find inside a loop. TypeScript only."
4014
+ }
4015
+ ];
4016
+ var HISTORY = [
4017
+ {
4018
+ name: "churn_{w}",
4019
+ unit: "lines",
4020
+ appliesTo: FILE,
4021
+ rollup: "sum",
4022
+ direction: "neutral",
4023
+ absent: "zero",
4024
+ source: "history",
4025
+ windowed: true,
4026
+ description: "Lines added plus deleted in the {w} window."
4027
+ },
4028
+ {
4029
+ name: "churn_{w}_commits",
4030
+ unit: "count",
4031
+ appliesTo: FILE,
4032
+ rollup: "none",
4033
+ direction: "neutral",
4034
+ absent: "zero",
4035
+ source: "history",
4036
+ windowed: true,
4037
+ description: "Commits touching the file in the {w} window. A group's distinct commits are not the sum."
4038
+ },
4039
+ {
4040
+ name: "churn_{w}_authors",
4041
+ unit: "count",
4042
+ appliesTo: FILE,
4043
+ rollup: "none",
4044
+ direction: "neutral",
4045
+ absent: "zero",
4046
+ source: "history",
4047
+ windowed: true,
4048
+ description: "Distinct authors of the file's commits in the {w} window. Not additive."
4049
+ },
4050
+ {
4051
+ name: "recency_{w}",
4052
+ unit: "ratio",
4053
+ appliesTo: FILE,
4054
+ rollup: "none",
4055
+ direction: "neutral",
4056
+ absent: "exclude",
4057
+ source: "history",
4058
+ windowed: true,
4059
+ description: "min(1, file age / window) for a file that churned in the {w} window; 1 for lifetime. A hotspot discount."
4060
+ },
4061
+ {
4062
+ name: "file_age_days",
4063
+ unit: "days",
4064
+ appliesTo: FILE,
4065
+ rollup: "max",
4066
+ direction: "neutral",
4067
+ absent: "exclude",
4068
+ source: "history",
4069
+ description: "Days since the file first appeared in git history."
4070
+ },
4071
+ {
4072
+ name: "bus_factor_{w}",
4073
+ unit: "count",
4074
+ appliesTo: FILE,
4075
+ rollup: "none",
4076
+ direction: "lower-worse",
4077
+ absent: "exclude",
4078
+ source: "history",
4079
+ windowed: true,
4080
+ description: "Fewest authors covering half the file's churn in the {w} window."
4081
+ },
4082
+ {
4083
+ name: "top_author_share_{w}",
4084
+ unit: "ratio",
4085
+ appliesTo: FILE,
4086
+ rollup: "none",
4087
+ direction: "higher-worse",
4088
+ absent: "exclude",
4089
+ source: "history",
4090
+ windowed: true,
4091
+ description: "Share of the file's churn in the {w} window written by its top author."
4092
+ }
4093
+ ];
4094
+ var TESTS = [
4095
+ {
4096
+ name: "linked_test_count",
4097
+ unit: "count",
4098
+ appliesTo: FILE,
4099
+ rollup: "none",
4100
+ direction: "lower-worse",
4101
+ absent: "zero",
4102
+ source: "test-linker",
4103
+ description: "Test files linked to this source. One test can link to several sources, so it does not sum."
4104
+ },
4105
+ {
4106
+ name: "test_bus_factor_{w}",
4107
+ unit: "count",
4108
+ appliesTo: FILE,
4109
+ rollup: "none",
4110
+ direction: "lower-worse",
4111
+ absent: "exclude",
4112
+ source: "test-linker",
4113
+ windowed: true,
4114
+ description: "Bus factor of the churn on this source's linked tests in the {w} window."
4115
+ },
4116
+ {
4117
+ name: "test_top_author_share_{w}",
4118
+ unit: "ratio",
4119
+ appliesTo: FILE,
4120
+ rollup: "none",
4121
+ direction: "higher-worse",
4122
+ absent: "exclude",
4123
+ source: "test-linker",
4124
+ windowed: true,
4125
+ description: "Top author's share of the churn on this source's linked tests in the {w} window."
4126
+ },
4127
+ {
4128
+ name: "coverage_pct",
4129
+ unit: "percent",
4130
+ appliesTo: ["file", "symbol"],
4131
+ rollup: "none",
4132
+ direction: "lower-worse",
4133
+ absent: "exclude",
4134
+ source: "coverage",
4135
+ description: "Functions covered over functions total, from an ingested Istanbul report. Never written by the indexer."
4136
+ }
4137
+ ];
4138
+ var METRIC_CATALOGUE = [...DEGREE, ...SOURCE, ...SMELLS, ...HISTORY, ...TESTS];
4139
+
4140
+ // src/catalogue/describe.ts
4141
+ var WINDOW_TOKEN = "{w}";
4142
+ var WINDOW_PATTERN = "(\\d+d|lifetime)";
4143
+ var BY_NAME = new Map(METRIC_CATALOGUE.filter((d) => !d.windowed).map((d) => [d.name, d]));
4144
+ var WINDOWED = METRIC_CATALOGUE.filter((d) => d.windowed).map((template) => ({
4145
+ pattern: new RegExp(`^${template.name.replace(WINDOW_TOKEN, WINDOW_PATTERN)}$`),
4146
+ template
4147
+ }));
4148
+ function resolveWindow(template, window) {
4149
+ const resolved = {
4150
+ ...template,
4151
+ name: template.name.replace(WINDOW_TOKEN, window),
4152
+ description: template.description.replace(WINDOW_TOKEN, window),
4153
+ window
4154
+ };
4155
+ delete resolved.windowed;
4156
+ return resolved;
4157
+ }
4158
+ function describeMetric(name) {
4159
+ const exact = BY_NAME.get(name);
4160
+ if (exact) return exact;
4161
+ for (const { pattern, template } of WINDOWED) {
4162
+ const match = pattern.exec(name);
4163
+ if (match) return resolveWindow(template, match[1]);
4164
+ }
4165
+ return null;
4166
+ }
4167
+ function describeMetrics(names) {
4168
+ const described = [];
4169
+ const unknown = [];
4170
+ for (const name of new Set(names)) {
4171
+ const descriptor = describeMetric(name);
4172
+ if (descriptor) described.push(descriptor);
4173
+ else unknown.push(name);
4174
+ }
4175
+ return { described, unknown };
4176
+ }
3151
4177
 
3152
4178
  // src/check/context.ts
3153
4179
  function buildRuleContext(store, snapshotId) {
@@ -3430,7 +4456,7 @@ function runChecks(store, options) {
3430
4456
  violations.push(...runRule(rule, ctx));
3431
4457
  }
3432
4458
  if (options.baselineSnapshotId) {
3433
- const baselineKeys = collectBaselineKeys(store, options.baselineSnapshotId, options.rules);
4459
+ const baselineKeys = collectBaselineKeys(store, options.baselineSnapshotId, options.snapshotId, options.rules);
3434
4460
  for (const v of violations) {
3435
4461
  if (baselineKeys.has(violationKey(v))) v.isCarryover = true;
3436
4462
  }
@@ -3446,17 +4472,26 @@ function runChecks(store, options) {
3446
4472
  passed: counts.newErrors === 0
3447
4473
  };
3448
4474
  }
3449
- function collectBaselineKeys(store, baselineSnapshotId, rules) {
4475
+ function snapshotViolations(store, snapshotId, rules) {
4476
+ const ctx = buildRuleContext(store, snapshotId);
4477
+ return rules.flatMap((rule) => runRule(rule, ctx));
4478
+ }
4479
+ function collectBaselineKeys(store, baselineSnapshotId, snapshotId, rules) {
3450
4480
  const ctx = buildRuleContext(store, baselineSnapshotId);
4481
+ const chain = aliasChain(store, baselineSnapshotId, snapshotId);
3451
4482
  const keys = /* @__PURE__ */ new Set();
3452
4483
  for (const rule of rules) {
3453
- for (const v of runRule(rule, ctx)) keys.add(violationKey(v));
4484
+ for (const v of runRule(rule, ctx)) keys.add(rebasedViolationKey(v, chain.resolve));
3454
4485
  }
3455
4486
  return keys;
3456
4487
  }
3457
4488
  function violationKey(v) {
3458
4489
  return v.destinationId ? `${v.ruleId}|${v.nodeId}|${v.destinationId}` : `${v.ruleId}|${v.nodeId}`;
3459
4490
  }
4491
+ function rebasedViolationKey(v, resolve4) {
4492
+ const destinationId = v.destinationId ? resolve4(v.destinationId) : v.destinationId;
4493
+ return violationKey({ ...v, nodeId: resolve4(v.nodeId), destinationId });
4494
+ }
3460
4495
  function countByOriginAndSeverity(violations) {
3461
4496
  const counts = { newErrors: 0, newWarnings: 0, carryoverErrors: 0, carryoverWarnings: 0 };
3462
4497
  for (const v of violations) {
@@ -3688,18 +4723,18 @@ function checkSnapshot(store, options) {
3688
4723
  });
3689
4724
  return { snapshot, baselineSnapshot, result };
3690
4725
  }
3691
- async function loadCheckRules(path10, options = {}) {
4726
+ async function loadCheckRules(path12, options = {}) {
3692
4727
  let raw;
3693
4728
  try {
3694
- raw = await readFile2(path10, "utf8");
4729
+ raw = await readFile2(path12, "utf8");
3695
4730
  } catch (err) {
3696
- throw new Error(`Cannot read rules file at ${path10}: ${errorMessage(err)}`);
4731
+ throw new Error(`Cannot read rules file at ${path12}: ${errorMessage(err)}`);
3697
4732
  }
3698
4733
  let parsed;
3699
4734
  try {
3700
4735
  parsed = JSON.parse(raw);
3701
4736
  } catch (err) {
3702
- throw new Error(`Invalid JSON in ${path10}: ${errorMessage(err)}`);
4737
+ throw new Error(`Invalid JSON in ${path12}: ${errorMessage(err)}`);
3703
4738
  }
3704
4739
  return validateRules(parsed, options);
3705
4740
  }
@@ -3708,13 +4743,8 @@ function errorMessage(err) {
3708
4743
  }
3709
4744
 
3710
4745
  // src/diff/diff.ts
3711
- function aliasMap(aliases) {
3712
- const m = /* @__PURE__ */ new Map();
3713
- for (const a of aliases) m.set(a.oldId, a);
3714
- return m;
3715
- }
3716
4746
  function resolveId(id, aliases) {
3717
- return aliases.get(id)?.newId ?? id;
4747
+ return aliases.resolve(id);
3718
4748
  }
3719
4749
  function edgeKey2(srcId, dstId, kind) {
3720
4750
  return JSON.stringify([srcId, dstId, canonicalEdgeKind(kind)]);
@@ -3741,7 +4771,7 @@ function diffNodes(fromNodes, toNodes, aliases) {
3741
4771
  }
3742
4772
  matchedToIds.add(newId);
3743
4773
  if (newId !== fromNode.id) {
3744
- renamed.push({ oldId: fromNode.id, newId, reason: aliases.get(fromNode.id).reason, node: toNode });
4774
+ renamed.push({ oldId: fromNode.id, newId, reason: aliases.trace(fromNode.id).reason, node: toNode });
3745
4775
  }
3746
4776
  }
3747
4777
  const added = toNodes.filter((toNode) => !matchedToIds.has(toNode.id));
@@ -3792,7 +4822,7 @@ function diffMetrics(fromMetrics, toMetrics, aliases, commonNewIds) {
3792
4822
  }
3793
4823
  function diffSnapshots(store, options) {
3794
4824
  const { fromSnapshotId, toSnapshotId } = options;
3795
- const aliases = aliasMap(store.listAliases(toSnapshotId));
4825
+ const aliases = aliasChain(store, fromSnapshotId, toSnapshotId);
3796
4826
  const nodeDiff = diffNodes(store.listNodes(fromSnapshotId), store.listNodes(toSnapshotId), aliases);
3797
4827
  const edgeDiff = diffEdges(store.listEdges(fromSnapshotId), store.listEdges(toSnapshotId), aliases);
3798
4828
  const metricDeltas = diffMetrics(
@@ -3829,11 +4859,12 @@ function summarize({ fromSnapshotId, toSnapshotId }, nodeDiff, edgeDiff, metricD
3829
4859
  function diffCheckResults(store, options) {
3830
4860
  const from = runChecks(store, { snapshotId: options.fromSnapshotId, rules: options.rules });
3831
4861
  const to = runChecks(store, { snapshotId: options.toSnapshotId, rules: options.rules });
4862
+ const chain = aliasChain(store, options.fromSnapshotId, options.toSnapshotId);
3832
4863
  return {
3833
4864
  fromSnapshotId: options.fromSnapshotId,
3834
4865
  toSnapshotId: options.toSnapshotId,
3835
4866
  rulesEvaluated: options.rules.length,
3836
- ...bucketViolations(indexByKey(from.violations), indexByKey(to.violations))
4867
+ ...bucketViolations(indexByKey(from.violations, chain.resolve), indexByKey(to.violations))
3837
4868
  };
3838
4869
  }
3839
4870
  function bucketViolations(fromByKey, toByKey) {
@@ -3855,9 +4886,9 @@ function bucketViolations(fromByKey, toByKey) {
3855
4886
  }
3856
4887
  return buckets;
3857
4888
  }
3858
- function indexByKey(violations) {
4889
+ function indexByKey(violations, resolve4) {
3859
4890
  const out = /* @__PURE__ */ new Map();
3860
- for (const v of violations) out.set(violationKey(v), v);
4891
+ for (const v of violations) out.set(resolve4 ? rebasedViolationKey(v, resolve4) : violationKey(v), v);
3861
4892
  return out;
3862
4893
  }
3863
4894
  function computeDelta(from, to) {
@@ -4006,29 +5037,348 @@ function buildSymbolIndex(store, model, symbols) {
4006
5037
  function toCandidate(s, score) {
4007
5038
  return { id: s.id, name: s.name, file: s.file, signature: s.signature, purpose: s.purpose, score };
4008
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
+ }
4009
5350
  export {
5351
+ ALIAS_BASE_ATTR,
4010
5352
  ALL_ROLES,
5353
+ COMMUNITY_SUMMARY_NAMESPACE,
4011
5354
  COVERAGE_METRIC_NAME,
4012
5355
  CodeGraphStore,
4013
5356
  DEAD_CODE_METRIC_NAMES,
5357
+ DEFAULT_CHURN_WINDOWS,
4014
5358
  DOMAIN_DDL,
4015
5359
  GROWTH_RISK_METRIC_NAMES,
4016
5360
  INDEX_VERSION,
4017
5361
  KIT,
4018
5362
  LanguageExtractor,
5363
+ METRIC_CATALOGUE,
4019
5364
  MIGRATIONS,
4020
5365
  PythonGraphExtractor,
4021
5366
  SCHEMA_VERSION,
5367
+ SNAPSHOT_SCOPED_TABLES,
4022
5368
  SOURCE_METRIC_NAMES,
4023
5369
  SYMBOL_EMBEDDING_NAMESPACE,
4024
5370
  SYMBOL_ID_SEP,
4025
5371
  TsMorphGraphExtractor,
5372
+ aggregateMetrics,
5373
+ aliasChain,
4026
5374
  annotateRoles,
4027
5375
  attributeCoverage,
4028
5376
  buildAliases,
5377
+ buildConventionAreas,
4029
5378
  buildEmbedText,
4030
5379
  buildFileModuleNodes,
4031
5380
  buildIndexerMetrics,
5381
+ buildLineage,
4032
5382
  canonicalEdgeKind,
4033
5383
  canonicalMetricName,
4034
5384
  canonicalRole,
@@ -4036,16 +5386,25 @@ export {
4036
5386
  classifyRole,
4037
5387
  collectDeclaredNames,
4038
5388
  collectDeclaredSpans,
5389
+ compilePatterns,
4039
5390
  computeDeadCodeMetrics,
5391
+ computeDeepAst,
4040
5392
  computeGrowthRiskMetrics,
4041
5393
  computeMetrics,
4042
5394
  computePageRank,
5395
+ computePartitionQuality,
5396
+ computeRecencyWindows,
4043
5397
  computeRelevance,
4044
5398
  computeRoleHints,
4045
5399
  computeSourceMetrics,
4046
5400
  computeSymbolConsumers,
4047
5401
  computeSymbolCoupling,
4048
5402
  computeTestCoverageOwnership,
5403
+ createAliasChain,
5404
+ defaultTargetCount,
5405
+ describeMetric,
5406
+ describeMetrics,
5407
+ detectCommunities,
4049
5408
  detectGitHead,
4050
5409
  detectGitToplevel,
4051
5410
  detectRenames,
@@ -4056,7 +5415,9 @@ export {
4056
5415
  embedTextsCached,
4057
5416
  externalId,
4058
5417
  fileId,
5418
+ findConventions,
4059
5419
  findSimilarCapability,
5420
+ getConventionMap,
4060
5421
  getEdgeWeight,
4061
5422
  getLanguageFromPath3 as getLanguageFromPath,
4062
5423
  getSupportedLanguages,
@@ -4064,38 +5425,57 @@ export {
4064
5425
  hashContent,
4065
5426
  hashEmbedText,
4066
5427
  indexPaths,
5428
+ invertBuckets,
4067
5429
  isGeneratedByHeuristic,
4068
5430
  isGeneratedFile,
4069
5431
  isInsideGitRepo,
5432
+ lineagePath,
4070
5433
  linkTestsToSources,
4071
5434
  listEdges,
5435
+ listEdgesTouching,
4072
5436
  listEmbeddableSymbols,
4073
5437
  listMetrics,
5438
+ listMetricsForNode,
4074
5439
  listNodes,
4075
5440
  loadCheckRules,
4076
5441
  loadGeneratedPatterns,
5442
+ loadHistoryMetrics,
5443
+ loadLineage,
5444
+ matchesAny,
4077
5445
  moduleId,
4078
5446
  openCodeGraph,
4079
5447
  packageId,
4080
5448
  parentModuleId,
4081
5449
  parseFile2 as parseFile,
4082
5450
  parseSymbolId,
5451
+ patternToRegex,
5452
+ planPrune,
5453
+ priorSnapshotForRef,
4083
5454
  pruneDanglingReferences,
4084
5455
  readSourceFiles,
5456
+ rebasedViolationKey,
5457
+ resolveAlias,
4085
5458
  resolveBarrelEdges,
5459
+ resolveChurnWindows,
5460
+ resolveGitRef,
4086
5461
  resolveSnapshot,
4087
5462
  runChecks,
5463
+ runPrune,
4088
5464
  shouldIncludeFile2 as shouldIncludeFile,
4089
5465
  snapshotPageRank,
4090
5466
  snapshotReferenceEdges,
4091
5467
  snapshotRelevance,
4092
5468
  snapshotSymbolConsumers,
4093
5469
  snapshotSymbolCoupling,
5470
+ snapshotViolations,
4094
5471
  structuralSignature,
5472
+ summarizeConventions,
4095
5473
  symbolId,
4096
5474
  testCoverageCountMetrics,
4097
5475
  tryEmbedSnapshot,
4098
5476
  validateRules,
4099
- walkSourceFiles
5477
+ violationKey,
5478
+ walkSourceFiles,
5479
+ windowSuffix
4100
5480
  };
4101
5481
  //# sourceMappingURL=index.js.map