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