@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/README.md +194 -13
- package/dist/index.d.ts +483 -5
- package/dist/index.js +1429 -49
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
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)
|
|
653
|
+
if (child) step2(child, inner);
|
|
499
654
|
}
|
|
500
655
|
};
|
|
501
|
-
|
|
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
|
|
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)
|
|
2254
|
+
if (child) walk2(child);
|
|
1943
2255
|
}
|
|
1944
2256
|
};
|
|
1945
|
-
|
|
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
|
|
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)
|
|
2270
|
+
if (child) walk2(child);
|
|
1959
2271
|
}
|
|
1960
2272
|
};
|
|
1961
|
-
|
|
2273
|
+
walk2(fn);
|
|
1962
2274
|
}
|
|
1963
2275
|
function countReferences(fn, declIds, out) {
|
|
1964
|
-
const
|
|
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)
|
|
2281
|
+
if (child) walk2(child);
|
|
1970
2282
|
}
|
|
1971
2283
|
};
|
|
1972
|
-
|
|
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.
|
|
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,
|
|
2672
|
-
const
|
|
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
|
-
...
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
4726
|
+
async function loadCheckRules(path12, options = {}) {
|
|
3692
4727
|
let raw;
|
|
3693
4728
|
try {
|
|
3694
|
-
raw = await readFile2(
|
|
4729
|
+
raw = await readFile2(path12, "utf8");
|
|
3695
4730
|
} catch (err) {
|
|
3696
|
-
throw new Error(`Cannot read rules file at ${
|
|
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 ${
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
5477
|
+
violationKey,
|
|
5478
|
+
walkSourceFiles,
|
|
5479
|
+
windowSuffix
|
|
4100
5480
|
};
|
|
4101
5481
|
//# sourceMappingURL=index.js.map
|