@ainyc/canonry 5.1.4 → 5.2.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.
Files changed (37) hide show
  1. package/assets/assets/{AuditHistoryPanel-Dc_pymFg.js → AuditHistoryPanel-D0H5TtxC.js} +1 -1
  2. package/assets/assets/{BacklinksPage-BkSo48YX.js → BacklinksPage-DRihwG5O.js} +1 -1
  3. package/assets/assets/{HistoryPage-BbswsdU7.js → HistoryPage-BQ2nEXly.js} +1 -1
  4. package/assets/assets/{MeasurementPropertyPage-BbgtalkM.js → MeasurementPropertyPage-Bp6EURzJ.js} +1 -1
  5. package/assets/assets/ProjectPage-Cd03Phwx.js +8 -0
  6. package/assets/assets/{RunRow-2cNW8FHz.js → RunRow-Bo_Adiy0.js} +1 -1
  7. package/assets/assets/{RunsPage-Cz3lJ6VJ.js → RunsPage-CDkbzjuo.js} +1 -1
  8. package/assets/assets/{SettingsPage-BQwB0r9k.js → SettingsPage-CF4i_EzR.js} +1 -1
  9. package/assets/assets/SiteHealthSection-BrzBV7dt.js +4 -0
  10. package/assets/assets/{TrafficPage-DTic3zhO.js → TrafficPage-BHSz1N4P.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-BKfabnCw.js → TrafficSourceDetailPage-DzHLwhpy.js} +1 -1
  12. package/assets/assets/{extract-error-message-BzB8j3fC.js → extract-error-message-WHM24Sai.js} +1 -1
  13. package/assets/assets/{index-DQGEzxrN.js → index-5W4ZZnwg.js} +22 -22
  14. package/assets/assets/index-DCLmKL1s.css +1 -0
  15. package/assets/assets/{react-sigma_core.esm.min-rYv0aKKx.js → react-sigma_core.esm.min-BB2Lcq4W.js} +1 -1
  16. package/assets/assets/{v2-overview-adapter-CWWPfsm-.js → v2-overview-adapter-ebrNjcUG.js} +1 -1
  17. package/assets/assets/vendor-lucide-D_WBzb76.js +1 -0
  18. package/assets/index.html +3 -3
  19. package/dist/{chunk-MMSPU72Z.js → chunk-2U5IW3G5.js} +4 -0
  20. package/dist/chunk-5VONCLVQ.js +652 -0
  21. package/dist/chunk-AOJU5LKX.js +8 -0
  22. package/dist/{chunk-KJM4DRZN.js → chunk-C7PITYL3.js} +5 -3
  23. package/dist/{chunk-3KXI4C3S.js → chunk-CW2KPN3L.js} +310 -941
  24. package/dist/{chunk-AEE5DGTE.js → chunk-IOP36L3C.js} +4 -7
  25. package/dist/chunk-RB2F4D3Z.js +67 -0
  26. package/dist/{chunk-EBUX3C5H.js → chunk-YQNKULAD.js} +30 -10
  27. package/dist/cli.js +100 -5
  28. package/dist/demo-server-BIPT53EY.js +2125 -0
  29. package/dist/index.js +6 -4
  30. package/dist/intelligence-service-SJO5E7WN.js +9 -0
  31. package/dist/mcp.js +4 -3
  32. package/package.json +13 -13
  33. package/assets/assets/ProjectPage-CAcRQp-T.js +0 -8
  34. package/assets/assets/SiteHealthSection-C0aJ_UhN.js +0 -4
  35. package/assets/assets/index-CjEprBri.css +0 -1
  36. package/assets/assets/vendor-lucide-CW2qe0uH.js +0 -1
  37. package/dist/intelligence-service-25NQCETC.js +0 -7
@@ -0,0 +1,652 @@
1
+ import {
2
+ siteCrawlEdges,
3
+ siteCrawlGraphEdges,
4
+ siteCrawlGraphLayouts,
5
+ siteCrawlGraphNodes,
6
+ siteCrawlPages,
7
+ siteCrawlSnapshots
8
+ } from "./chunk-YQNKULAD.js";
9
+ import {
10
+ SITE_CRAWL_GRAPH_MAX_EDGES,
11
+ SITE_CRAWL_GRAPH_MAX_NODES,
12
+ TEMPLATE_LINK_RATIO_THRESHOLD
13
+ } from "./chunk-2U5IW3G5.js";
14
+
15
+ // src/site-crawl-graph-layout.ts
16
+ import crypto from "crypto";
17
+ import { createRequire } from "module";
18
+ import { Worker } from "worker_threads";
19
+ import { and, asc, count, desc, eq, isNull, ne, or, sql } from "drizzle-orm";
20
+ import { alias } from "drizzle-orm/sqlite-core";
21
+ var SITE_CRAWL_GRAPH_LAYOUT_TIMEOUT_MS = 15e3;
22
+ var SITE_CRAWL_GRAPH_SEED_SCALE = 100;
23
+ var graphSourcePage = alias(siteCrawlPages, "layout_graph_source_page");
24
+ var graphTargetPage = alias(siteCrawlPages, "layout_graph_target_page");
25
+ var FORCE_ATLAS_SETTINGS = Object.freeze({
26
+ adjustSizes: false,
27
+ barnesHutOptimize: true,
28
+ barnesHutTheta: 0.5,
29
+ edgeWeightInfluence: 0.5,
30
+ gravity: 0.2,
31
+ linLogMode: true,
32
+ outboundAttractionDistribution: true,
33
+ scalingRatio: 2,
34
+ slowDown: 10,
35
+ strongGravityMode: false
36
+ });
37
+ var RENDER_MAX_NODE_SIZE = 14;
38
+ var RENDER_HALF_EXTENT_PX = 420;
39
+ var NODE_SIZE_EXTENT_FRACTION = RENDER_MAX_NODE_SIZE / RENDER_HALF_EXTENT_PX;
40
+ function siteCrawlGraphNodeSize(extent, nodeCount) {
41
+ if (!Number.isFinite(extent) || extent <= 0 || nodeCount <= 0) return 0;
42
+ const byExtent = extent * NODE_SIZE_EXTENT_FRACTION;
43
+ const byArea = Math.sqrt(NOVERLAP_SETTINGS.maxPackingFraction * (2 * extent) ** 2 / nodeCount) / 2;
44
+ return Math.min(byExtent, byArea);
45
+ }
46
+ function siteCrawlGraphExtent(positions) {
47
+ if (positions.length === 0) return 0;
48
+ const centerX = positions.reduce((sum, p) => sum + p.x, 0) / positions.length;
49
+ const centerY = positions.reduce((sum, p) => sum + p.y, 0) / positions.length;
50
+ let extent = 0;
51
+ for (const position of positions) {
52
+ extent = Math.max(extent, Math.abs(position.x - centerX), Math.abs(position.y - centerY));
53
+ }
54
+ return extent;
55
+ }
56
+ var NOVERLAP_SETTINGS = Object.freeze({
57
+ /** Half a node of clear space between two touching nodes. */
58
+ marginFactor: 0.5,
59
+ expansion: 1.1,
60
+ ratio: 1,
61
+ speed: 3,
62
+ /**
63
+ * Node discs may claim at most this share of the graph's bounding box. At
64
+ * the 20k cap the extent-derived size asks for spacing the graph physically
65
+ * does not have, and noverlap either runs for minutes or overflows its
66
+ * collision set. This bound keeps the pass solvable at every graph size,
67
+ * and it only binds on graphs far too dense to read node-by-node anyway.
68
+ */
69
+ maxPackingFraction: 0.15,
70
+ /** Cells hold a handful of nodes each, which is what keeps the pass linear. */
71
+ gridSizeMin: 10,
72
+ gridSizeMax: 200,
73
+ iterationLadder: [
74
+ { maxNodes: 1e3, iterations: 120 },
75
+ { maxNodes: 5e3, iterations: 60 }
76
+ ],
77
+ iterationsFloor: 20
78
+ });
79
+ function noverlapGridSize(nodeCount) {
80
+ return Math.max(
81
+ NOVERLAP_SETTINGS.gridSizeMin,
82
+ Math.min(NOVERLAP_SETTINGS.gridSizeMax, Math.ceil(Math.sqrt(Math.max(1, nodeCount))))
83
+ );
84
+ }
85
+ function noverlapMaxIterations(nodeCount) {
86
+ for (const step of NOVERLAP_SETTINGS.iterationLadder) {
87
+ if (nodeCount <= step.maxNodes) return step.iterations;
88
+ }
89
+ return NOVERLAP_SETTINGS.iterationsFloor;
90
+ }
91
+ var COMPONENT_PACKING_SPACING_FACTOR = 3;
92
+ function siteCrawlGraphLayoutSettingsFingerprint(settings) {
93
+ return crypto.createHash("sha256").update(JSON.stringify(settings)).digest("hex").slice(0, 8);
94
+ }
95
+ var SITE_CRAWL_GRAPH_LAYOUT_ALGORITHM = "site-health-fa2-v5";
96
+ var SITE_CRAWL_GRAPH_LAYOUT_VERSION = `${SITE_CRAWL_GRAPH_LAYOUT_ALGORITHM}-${siteCrawlGraphLayoutSettingsFingerprint({
97
+ forceAtlas: FORCE_ATLAS_SETTINGS,
98
+ noverlap: NOVERLAP_SETTINGS,
99
+ nodeSizeExtentFraction: NODE_SIZE_EXTENT_FRACTION,
100
+ componentPackingSpacingFactor: COMPONENT_PACKING_SPACING_FACTOR,
101
+ templateLinkRatioThreshold: TEMPLATE_LINK_RATIO_THRESHOLD
102
+ })}`;
103
+ var WORKER_SOURCE = String.raw`
104
+ const {parentPort, workerData} = require('node:worker_threads');
105
+ try {
106
+ const graphologyModule = require(workerData.graphologyPath);
107
+ const forceAtlasModule = require(workerData.forceAtlasPath);
108
+ const noverlapModule = require(workerData.noverlapPath);
109
+ const MultiDirectedGraph = graphologyModule.MultiDirectedGraph;
110
+ const forceAtlas2 = forceAtlasModule.default || forceAtlasModule;
111
+ const noverlap = noverlapModule.default || noverlapModule;
112
+ const graph = new MultiDirectedGraph({allowSelfLoops: true});
113
+ for (const node of workerData.input.nodes) {
114
+ graph.addNode(node.nodeKey, {x: node.x, y: node.y});
115
+ }
116
+ for (const edge of workerData.input.edges) {
117
+ graph.addDirectedEdgeWithKey(edge.edgeKey, edge.sourceNodeKey, edge.targetNodeKey, {
118
+ weight: 1 + Math.log2(Math.max(1, edge.occurrences)),
119
+ });
120
+ }
121
+ if (graph.order > 1 && graph.size > 0) {
122
+ forceAtlas2.assign(graph, {
123
+ iterations: workerData.input.iterations,
124
+ settings: workerData.settings,
125
+ });
126
+ }
127
+ let nodeSize = 0;
128
+ // Anti-overlap runs on the spatialization ForceAtlas2 just produced. Node
129
+ // size is uniform and derived from the graph's own extent, so the pass is
130
+ // scale invariant and stays deterministic for a fixed input.
131
+ if (graph.order > 1) {
132
+ let sumX = 0;
133
+ let sumY = 0;
134
+ graph.forEachNode(function (nodeKey, attributes) {
135
+ sumX += attributes.x;
136
+ sumY += attributes.y;
137
+ });
138
+ const centerX = sumX / graph.order;
139
+ const centerY = sumY / graph.order;
140
+ let extent = 0;
141
+ graph.forEachNode(function (nodeKey, attributes) {
142
+ extent = Math.max(extent, Math.abs(attributes.x - centerX), Math.abs(attributes.y - centerY));
143
+ });
144
+ if (Number.isFinite(extent) && extent > 0) {
145
+ // Whichever bound is tighter: one render-sized node, or the largest disc
146
+ // that still leaves the graph mostly empty space at this node count.
147
+ // Mirrors siteCrawlGraphNodeSize; a test asserts the two agree.
148
+ const byExtent = extent * workerData.nodeSizeExtentFraction;
149
+ const byArea = Math.sqrt(
150
+ (workerData.noverlap.maxPackingFraction * Math.pow(2 * extent, 2)) / graph.order,
151
+ ) / 2;
152
+ nodeSize = Math.min(byExtent, byArea);
153
+ graph.forEachNode(function (nodeKey) {
154
+ graph.setNodeAttribute(nodeKey, 'size', nodeSize);
155
+ });
156
+ noverlap.assign(graph, {
157
+ maxIterations: workerData.noverlap.maxIterations,
158
+ settings: {
159
+ gridSize: workerData.noverlap.gridSize,
160
+ margin: nodeSize * workerData.noverlap.marginFactor,
161
+ expansion: workerData.noverlap.expansion,
162
+ ratio: workerData.noverlap.ratio,
163
+ speed: workerData.noverlap.speed,
164
+ },
165
+ });
166
+ }
167
+ }
168
+ const positions = [];
169
+ graph.forEachNode((nodeKey, attributes) => {
170
+ positions.push({nodeKey, x: attributes.x, y: attributes.y});
171
+ });
172
+ parentPort.postMessage({ok: true, positions, nodeSize});
173
+ } catch (error) {
174
+ parentPort.postMessage({
175
+ ok: false,
176
+ error: error && typeof error.message === 'string' ? error.message : String(error),
177
+ });
178
+ }
179
+ `;
180
+ var GraphLayoutTimeoutError = class extends Error {
181
+ name = "GraphLayoutTimeoutError";
182
+ };
183
+ function hashUnit(value) {
184
+ let hash = 2166136261;
185
+ for (let i = 0; i < value.length; i++) {
186
+ hash ^= value.charCodeAt(i);
187
+ hash = Math.imul(hash, 16777619);
188
+ }
189
+ return (hash >>> 0) / 4294967296;
190
+ }
191
+ function pathSection(path) {
192
+ return path.split("/").filter(Boolean)[0] ?? "__root__";
193
+ }
194
+ function seedSiteCrawlGraphNodes(nodes, rootNodeKey, priorPositions) {
195
+ return [...nodes].sort((a, b) => a.sampleRank - b.sampleRank || a.nodeKey.localeCompare(b.nodeKey)).map((node) => {
196
+ const prior = priorPositions?.get(node.nodeKey);
197
+ if (prior && Number.isFinite(prior.x) && Number.isFinite(prior.y)) {
198
+ return {
199
+ ...node,
200
+ x: prior.x * SITE_CRAWL_GRAPH_SEED_SCALE,
201
+ y: prior.y * SITE_CRAWL_GRAPH_SEED_SCALE
202
+ };
203
+ }
204
+ if (node.nodeKey === rootNodeKey) return { ...node, x: 0, y: 0 };
205
+ const section = pathSection(node.path);
206
+ const sectionAngle = hashUnit(`section:${section}`) * Math.PI * 2;
207
+ const sectionRadius = section === "__root__" ? 24 : 100;
208
+ const clusterX = Math.cos(sectionAngle) * sectionRadius;
209
+ const clusterY = Math.sin(sectionAngle) * sectionRadius;
210
+ const localAngle = hashUnit(`node-angle:${node.nodeKey}`) * Math.PI * 2;
211
+ const depth = Math.max(0, Math.min(12, node.depth ?? 4));
212
+ const localRadius = 4 + depth * 2.5 + Math.sqrt(hashUnit(`node-radius:${node.nodeKey}`)) * 14;
213
+ return {
214
+ ...node,
215
+ x: clusterX + Math.cos(localAngle) * localRadius,
216
+ y: clusterY + Math.sin(localAngle) * localRadius
217
+ };
218
+ });
219
+ }
220
+ function adaptiveForceAtlasIterations(nodeCount, edgeCount) {
221
+ if (nodeCount <= 100 && edgeCount <= 500) return 100;
222
+ if (nodeCount <= 500 && edgeCount <= 2500) return 70;
223
+ if (nodeCount <= 2e3 && edgeCount <= 1e4) return 40;
224
+ if (nodeCount <= 5e3 && edgeCount <= 2e4) return 24;
225
+ if (nodeCount <= 1e4 && edgeCount <= 35e3) return 12;
226
+ return 6;
227
+ }
228
+ async function computeForceAtlasPositions(input, options = {}) {
229
+ if (input.nodes.length <= 1 || input.edges.length === 0) {
230
+ const positions = input.nodes.map(({ nodeKey, x, y }) => ({ nodeKey, x, y }));
231
+ return { positions, nodeSize: siteCrawlGraphNodeSize(siteCrawlGraphExtent(positions), positions.length) };
232
+ }
233
+ const requireFromPackage = createRequire(import.meta.url);
234
+ const worker = new Worker(WORKER_SOURCE, {
235
+ eval: true,
236
+ workerData: {
237
+ graphologyPath: requireFromPackage.resolve("graphology"),
238
+ forceAtlasPath: requireFromPackage.resolve("graphology-layout-forceatlas2"),
239
+ noverlapPath: requireFromPackage.resolve("graphology-layout-noverlap"),
240
+ input,
241
+ settings: FORCE_ATLAS_SETTINGS,
242
+ noverlap: {
243
+ ...NOVERLAP_SETTINGS,
244
+ gridSize: noverlapGridSize(input.nodes.length),
245
+ maxIterations: noverlapMaxIterations(input.nodes.length)
246
+ },
247
+ nodeSizeExtentFraction: NODE_SIZE_EXTENT_FRACTION
248
+ }
249
+ });
250
+ const timeoutMs = options.timeoutMs ?? SITE_CRAWL_GRAPH_LAYOUT_TIMEOUT_MS;
251
+ return await new Promise((resolve, reject) => {
252
+ let settled = false;
253
+ const finish = (result) => {
254
+ if (settled) return;
255
+ settled = true;
256
+ clearTimeout(timer);
257
+ options.signal?.removeEventListener("abort", onAbort);
258
+ void worker.terminate();
259
+ if (result.error) reject(result.error instanceof Error ? result.error : new Error("Graph layout failed with a non-Error reason", { cause: result.error }));
260
+ else resolve({ positions: result.positions ?? [], nodeSize: result.nodeSize ?? 0 });
261
+ };
262
+ const onAbort = () => finish({ error: options.signal?.reason ?? new DOMException("Layout aborted", "AbortError") });
263
+ const timer = setTimeout(() => finish({ error: new GraphLayoutTimeoutError(`Graph layout exceeded ${timeoutMs}ms`) }), timeoutMs);
264
+ options.signal?.addEventListener("abort", onAbort, { once: true });
265
+ if (options.signal?.aborted) return onAbort();
266
+ worker.once("message", (message) => {
267
+ if (!message.ok) finish({ error: new Error(message.error ?? "Graph layout worker failed") });
268
+ else finish({ positions: message.positions, nodeSize: message.nodeSize });
269
+ });
270
+ worker.once("error", (error) => finish({ error }));
271
+ worker.once("exit", (code) => {
272
+ if (code !== 0 && !settled) finish({ error: new Error(`Graph layout worker exited with code ${code}`) });
273
+ });
274
+ });
275
+ }
276
+ function siteCrawlGraphComponents(nodeKeys, edges) {
277
+ const parent = new Map(nodeKeys.map((key) => [key, key]));
278
+ const find = (key) => {
279
+ let root = key;
280
+ while (parent.get(root) !== root) root = parent.get(root);
281
+ let cursor = key;
282
+ while (parent.get(cursor) !== root) {
283
+ const next = parent.get(cursor);
284
+ parent.set(cursor, root);
285
+ cursor = next;
286
+ }
287
+ return root;
288
+ };
289
+ for (const edge of edges) {
290
+ if (!parent.has(edge.sourceNodeKey) || !parent.has(edge.targetNodeKey)) continue;
291
+ const sourceRoot = find(edge.sourceNodeKey);
292
+ const targetRoot = find(edge.targetNodeKey);
293
+ if (sourceRoot === targetRoot) continue;
294
+ if (sourceRoot < targetRoot) parent.set(targetRoot, sourceRoot);
295
+ else parent.set(sourceRoot, targetRoot);
296
+ }
297
+ const members = /* @__PURE__ */ new Map();
298
+ for (const key of [...nodeKeys].sort()) {
299
+ const root = find(key);
300
+ const group = members.get(root);
301
+ if (group) group.push(key);
302
+ else members.set(root, [key]);
303
+ }
304
+ return [...members.values()].sort((left, right) => right.length - left.length || (left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0));
305
+ }
306
+ function describeComponent(nodeKeys, byKey) {
307
+ let sumX = 0;
308
+ let sumY = 0;
309
+ for (const nodeKey of nodeKeys) {
310
+ const position = byKey.get(nodeKey);
311
+ sumX += position.x;
312
+ sumY += position.y;
313
+ }
314
+ const centerX = sumX / nodeKeys.length;
315
+ const centerY = sumY / nodeKeys.length;
316
+ let radius = 0;
317
+ for (const nodeKey of nodeKeys) {
318
+ const position = byKey.get(nodeKey);
319
+ radius = Math.max(radius, Math.hypot(position.x - centerX, position.y - centerY));
320
+ }
321
+ return { nodeKeys, centerX, centerY, radius };
322
+ }
323
+ function packSiteCrawlGraphComponents(positions, edges, nodeSpacing) {
324
+ const components = siteCrawlGraphComponents(positions.map((position) => position.nodeKey), edges);
325
+ if (components.length <= 1) return [...positions];
326
+ const byKey = new Map(positions.map((position) => [position.nodeKey, position]));
327
+ if (positions.some((position) => !Number.isFinite(position.x) || !Number.isFinite(position.y))) {
328
+ return [...positions];
329
+ }
330
+ const described = components.map((nodeKeys) => describeComponent(nodeKeys, byKey));
331
+ const [anchor, ...rest] = described;
332
+ const spacing = Math.max(nodeSpacing, Number.EPSILON) * COMPONENT_PACKING_SPACING_FACTOR;
333
+ const ordered = [...rest].sort((left, right) => right.radius - left.radius || (left.nodeKeys[0] < right.nodeKeys[0] ? -1 : left.nodeKeys[0] > right.nodeKeys[0] ? 1 : 0));
334
+ const moved = /* @__PURE__ */ new Map();
335
+ let ringRadius = anchor.radius + spacing;
336
+ let ringPitch = 0;
337
+ let placedOnRing = 0;
338
+ let ringCapacity = 0;
339
+ for (const component of ordered) {
340
+ if (placedOnRing >= ringCapacity) {
341
+ ringPitch = 2 * component.radius + spacing;
342
+ ringRadius += ringPitch / 2 + (placedOnRing === 0 ? 0 : ringPitch / 2);
343
+ ringCapacity = Math.max(1, Math.floor(2 * Math.PI * ringRadius / ringPitch));
344
+ placedOnRing = 0;
345
+ }
346
+ const angle = placedOnRing / ringCapacity * Math.PI * 2;
347
+ placedOnRing += 1;
348
+ const targetX = anchor.centerX + Math.cos(angle) * ringRadius;
349
+ const targetY = anchor.centerY + Math.sin(angle) * ringRadius;
350
+ const shiftX = targetX - component.centerX;
351
+ const shiftY = targetY - component.centerY;
352
+ for (const nodeKey of component.nodeKeys) {
353
+ const position = byKey.get(nodeKey);
354
+ moved.set(nodeKey, { nodeKey, x: position.x + shiftX, y: position.y + shiftY });
355
+ }
356
+ }
357
+ return positions.map((position) => moved.get(position.nodeKey) ?? position);
358
+ }
359
+ function normalizePositions(seededNodes, computed, rootNodeKey) {
360
+ const byKey = new Map(computed.map((position) => [position.nodeKey, position]));
361
+ if (byKey.size !== seededNodes.length) throw new Error("Graph layout returned an incomplete node set");
362
+ const root = rootNodeKey ? byKey.get(rootNodeKey) : null;
363
+ const centerX = root?.x ?? computed.reduce((sum, position) => sum + position.x, 0) / Math.max(1, computed.length);
364
+ const centerY = root?.y ?? computed.reduce((sum, position) => sum + position.y, 0) / Math.max(1, computed.length);
365
+ if (!Number.isFinite(centerX) || !Number.isFinite(centerY)) throw new Error("Graph layout returned a non-finite center");
366
+ let scale = 0;
367
+ for (const position of computed) {
368
+ if (!Number.isFinite(position.x) || !Number.isFinite(position.y)) throw new Error("Graph layout returned non-finite coordinates");
369
+ scale = Math.max(scale, Math.abs(position.x - centerX), Math.abs(position.y - centerY));
370
+ }
371
+ scale = scale > 0 ? scale : 1;
372
+ return seededNodes.map((node) => {
373
+ const position = byKey.get(node.nodeKey);
374
+ if (!position) throw new Error(`Graph layout omitted ${node.nodeKey}`);
375
+ return {
376
+ nodeKey: node.nodeKey,
377
+ sampleRank: node.sampleRank,
378
+ x: Math.round((position.x - centerX) / scale * 1e6) / 1e6,
379
+ y: Math.round((position.y - centerY) / scale * 1e6) / 1e6
380
+ };
381
+ });
382
+ }
383
+ async function layoutSiteCrawlGraphInput(input, options = {}) {
384
+ options.signal?.throwIfAborted();
385
+ if (input.nodes.length === 0) {
386
+ return {
387
+ state: "unavailable",
388
+ failureCode: "empty-crawl",
389
+ totalNodes: input.totalNodes,
390
+ totalEdges: input.totalEdges,
391
+ totalTemplateEdges: input.totalTemplateEdges,
392
+ nodeCount: 0,
393
+ edgeCount: 0,
394
+ nodes: [],
395
+ edges: []
396
+ };
397
+ }
398
+ const layoutEdges = input.edges.filter((edge) => !edge.isTemplate);
399
+ const seeded = seedSiteCrawlGraphNodes(input.nodes, input.rootNodeKey, input.priorPositions);
400
+ const timeoutMs = options.timeoutMs ?? SITE_CRAWL_GRAPH_LAYOUT_TIMEOUT_MS;
401
+ const deadline = Date.now() + timeoutMs;
402
+ const assertWithinBudget = () => {
403
+ options.signal?.throwIfAborted();
404
+ if (Date.now() > deadline) throw new GraphLayoutTimeoutError(`Graph layout exceeded ${timeoutMs}ms`);
405
+ };
406
+ try {
407
+ const compute = options.computePositions ?? computeForceAtlasPositions;
408
+ const { positions, nodeSize } = await compute({
409
+ nodes: seeded,
410
+ edges: layoutEdges,
411
+ iterations: adaptiveForceAtlasIterations(input.nodes.length, layoutEdges.length)
412
+ }, { signal: options.signal, timeoutMs: Math.max(1, deadline - Date.now()) });
413
+ assertWithinBudget();
414
+ const framed = packSiteCrawlGraphComponents(positions, layoutEdges, nodeSize);
415
+ assertWithinBudget();
416
+ return {
417
+ state: "ready",
418
+ layoutVersion: SITE_CRAWL_GRAPH_LAYOUT_VERSION,
419
+ totalNodes: input.totalNodes,
420
+ totalEdges: input.totalEdges,
421
+ totalTemplateEdges: input.totalTemplateEdges,
422
+ nodeCount: input.nodes.length,
423
+ edgeCount: input.edges.length,
424
+ templateLinksExcluded: true,
425
+ nodes: normalizePositions(seeded, framed, input.rootNodeKey),
426
+ edges: input.edges.map((edge, sampleRank) => ({ ...edge, sampleRank }))
427
+ };
428
+ } catch (error) {
429
+ if (options.signal?.aborted) options.signal.throwIfAborted();
430
+ if (error instanceof DOMException && error.name === "AbortError") throw error;
431
+ return {
432
+ state: "unavailable",
433
+ failureCode: error instanceof GraphLayoutTimeoutError ? "layout-timeout" : "layout-error",
434
+ totalNodes: input.totalNodes,
435
+ totalEdges: input.totalEdges,
436
+ totalTemplateEdges: input.totalTemplateEdges,
437
+ nodeCount: 0,
438
+ edgeCount: 0,
439
+ nodes: [],
440
+ edges: []
441
+ };
442
+ }
443
+ }
444
+ function loadPriorSiteCrawlGraphPositions(db, scope) {
445
+ const prior = db.select({
446
+ runId: siteCrawlSnapshots.runId,
447
+ attemptId: siteCrawlSnapshots.attemptId
448
+ }).from(siteCrawlSnapshots).innerJoin(siteCrawlGraphLayouts, and(
449
+ eq(siteCrawlGraphLayouts.projectId, siteCrawlSnapshots.projectId),
450
+ eq(siteCrawlGraphLayouts.runId, siteCrawlSnapshots.runId),
451
+ eq(siteCrawlGraphLayouts.attemptId, siteCrawlSnapshots.attemptId)
452
+ )).where(and(
453
+ eq(siteCrawlSnapshots.projectId, scope.projectId),
454
+ ne(siteCrawlSnapshots.runId, scope.runId),
455
+ eq(siteCrawlSnapshots.complete, true),
456
+ eq(siteCrawlGraphLayouts.state, "ready"),
457
+ eq(siteCrawlGraphLayouts.layoutVersion, SITE_CRAWL_GRAPH_LAYOUT_VERSION)
458
+ )).orderBy(desc(siteCrawlSnapshots.createdAt), desc(siteCrawlSnapshots.id)).limit(1).get();
459
+ if (!prior?.attemptId) return /* @__PURE__ */ new Map();
460
+ return new Map(db.select({
461
+ nodeKey: siteCrawlGraphNodes.nodeKey,
462
+ x: siteCrawlGraphNodes.x,
463
+ y: siteCrawlGraphNodes.y
464
+ }).from(siteCrawlGraphNodes).where(and(
465
+ eq(siteCrawlGraphNodes.projectId, scope.projectId),
466
+ eq(siteCrawlGraphNodes.runId, prior.runId),
467
+ eq(siteCrawlGraphNodes.attemptId, prior.attemptId)
468
+ )).all().map((node) => [node.nodeKey, { x: node.x, y: node.y }]));
469
+ }
470
+ async function prepareSiteCrawlGraphLayout(db, scope, options = {}) {
471
+ let totalNodes = 0;
472
+ let totalEdges = 0;
473
+ let totalTemplateEdges = 0;
474
+ try {
475
+ const assertNotAborted = () => options.signal?.throwIfAborted();
476
+ assertNotAborted();
477
+ const pageScope = [
478
+ eq(siteCrawlPages.projectId, scope.projectId),
479
+ eq(siteCrawlPages.runId, scope.runId),
480
+ eq(siteCrawlPages.attemptId, scope.attemptId)
481
+ ];
482
+ const edgeScope = [
483
+ eq(siteCrawlEdges.projectId, scope.projectId),
484
+ eq(siteCrawlEdges.runId, scope.runId),
485
+ eq(siteCrawlEdges.attemptId, scope.attemptId),
486
+ eq(siteCrawlEdges.internal, true),
487
+ eq(siteCrawlEdges.relation, "anchor")
488
+ ];
489
+ totalNodes = db.select({ value: count() }).from(siteCrawlPages).where(and(...pageScope)).get()?.value ?? 0;
490
+ assertNotAborted();
491
+ const countGraphCompatibleEdges = (...extra) => db.select({ value: count() }).from(siteCrawlEdges).innerJoin(graphSourcePage, and(
492
+ eq(graphSourcePage.projectId, scope.projectId),
493
+ eq(graphSourcePage.runId, scope.runId),
494
+ eq(graphSourcePage.attemptId, scope.attemptId),
495
+ eq(graphSourcePage.nodeKey, siteCrawlEdges.sourceNodeKey)
496
+ )).innerJoin(graphTargetPage, and(
497
+ eq(graphTargetPage.projectId, scope.projectId),
498
+ eq(graphTargetPage.runId, scope.runId),
499
+ eq(graphTargetPage.attemptId, scope.attemptId),
500
+ eq(graphTargetPage.nodeKey, siteCrawlEdges.targetNodeKey)
501
+ )).where(and(...edgeScope, ...extra)).get()?.value ?? 0;
502
+ totalEdges = countGraphCompatibleEdges();
503
+ assertNotAborted();
504
+ totalTemplateEdges = countGraphCompatibleEdges(eq(siteCrawlEdges.isTemplate, true));
505
+ assertNotAborted();
506
+ const graphSeedColumns = {
507
+ nodeKey: siteCrawlPages.nodeKey,
508
+ path: siteCrawlPages.path,
509
+ depth: siteCrawlPages.depth
510
+ };
511
+ const rootRows = db.select(graphSeedColumns).from(siteCrawlPages).where(and(...pageScope, eq(siteCrawlPages.url, scope.rootUrl))).orderBy(asc(siteCrawlPages.nodeKey)).limit(1).all();
512
+ assertNotAborted();
513
+ const retainedRows = [...rootRows];
514
+ if (retainedRows.length < SITE_CRAWL_GRAPH_MAX_NODES) {
515
+ retainedRows.push(...db.select(graphSeedColumns).from(siteCrawlPages).where(and(...pageScope, eq(siteCrawlPages.depth, 0), ne(siteCrawlPages.url, scope.rootUrl))).orderBy(asc(siteCrawlPages.nodeKey)).limit(SITE_CRAWL_GRAPH_MAX_NODES - retainedRows.length).all());
516
+ }
517
+ assertNotAborted();
518
+ if (retainedRows.length < SITE_CRAWL_GRAPH_MAX_NODES) {
519
+ retainedRows.push(...db.select(graphSeedColumns).from(siteCrawlPages).where(and(
520
+ ...pageScope,
521
+ or(isNull(siteCrawlPages.depth), ne(siteCrawlPages.depth, 0)),
522
+ ne(siteCrawlPages.url, scope.rootUrl)
523
+ )).orderBy(desc(siteCrawlPages.linkScoreNormalized), asc(siteCrawlPages.nodeKey)).limit(SITE_CRAWL_GRAPH_MAX_NODES - retainedRows.length).all());
524
+ }
525
+ assertNotAborted();
526
+ const nodes = retainedRows.map((row, sampleRank) => ({
527
+ nodeKey: row.nodeKey,
528
+ path: row.path,
529
+ depth: row.depth,
530
+ sampleRank
531
+ }));
532
+ const rootNodeKey = rootRows[0]?.nodeKey ?? null;
533
+ const priorPositions = loadPriorSiteCrawlGraphPositions(db, scope);
534
+ assertNotAborted();
535
+ const selectedKeys = JSON.stringify(nodes.map((node) => node.nodeKey));
536
+ const edgeRows = nodes.length === 0 ? [] : db.all(sql`
537
+ WITH selected(node_key) AS (SELECT value FROM json_each(${selectedKeys}))
538
+ SELECT
539
+ edge.edge_key AS edgeKey,
540
+ edge.source_node_key AS sourceNodeKey,
541
+ edge.target_node_key AS targetNodeKey,
542
+ edge.followable AS followable,
543
+ edge.occurrences AS occurrences,
544
+ edge.is_template AS isTemplate
545
+ FROM site_crawl_edges AS edge
546
+ INNER JOIN selected AS source ON source.node_key = edge.source_node_key
547
+ INNER JOIN selected AS target ON target.node_key = edge.target_node_key
548
+ WHERE edge.project_id = ${scope.projectId}
549
+ AND edge.run_id = ${scope.runId}
550
+ AND edge.attempt_id = ${scope.attemptId}
551
+ AND edge.internal = 1
552
+ AND edge.relation = 'anchor'
553
+ ORDER BY edge.occurrences DESC, edge.edge_key ASC
554
+ LIMIT ${SITE_CRAWL_GRAPH_MAX_EDGES}
555
+ `);
556
+ assertNotAborted();
557
+ return await layoutSiteCrawlGraphInput({
558
+ totalNodes,
559
+ totalEdges,
560
+ totalTemplateEdges,
561
+ nodes,
562
+ edges: edgeRows.map((row) => ({ ...row, isTemplate: Boolean(row.isTemplate) })),
563
+ rootNodeKey,
564
+ priorPositions
565
+ }, options);
566
+ } catch (error) {
567
+ if (options.signal?.aborted) options.signal.throwIfAborted();
568
+ if (error instanceof DOMException && error.name === "AbortError") throw error;
569
+ return {
570
+ state: "unavailable",
571
+ failureCode: "layout-error",
572
+ totalNodes,
573
+ totalEdges,
574
+ totalTemplateEdges,
575
+ nodeCount: 0,
576
+ edgeCount: 0,
577
+ nodes: [],
578
+ edges: []
579
+ };
580
+ }
581
+ }
582
+ function persistSiteCrawlGraphLayout(db, scope, layout, now = (/* @__PURE__ */ new Date()).toISOString()) {
583
+ db.transaction((tx) => {
584
+ tx.delete(siteCrawlGraphLayouts).where(and(
585
+ eq(siteCrawlGraphLayouts.projectId, scope.projectId),
586
+ eq(siteCrawlGraphLayouts.runId, scope.runId),
587
+ eq(siteCrawlGraphLayouts.attemptId, scope.attemptId)
588
+ )).run();
589
+ tx.insert(siteCrawlGraphLayouts).values({
590
+ id: crypto.randomUUID(),
591
+ projectId: scope.projectId,
592
+ runId: scope.runId,
593
+ attemptId: scope.attemptId,
594
+ state: layout.state,
595
+ layoutVersion: layout.state === "ready" ? layout.layoutVersion : null,
596
+ failureCode: layout.state === "unavailable" ? layout.failureCode : null,
597
+ totalNodes: layout.totalNodes,
598
+ totalEdges: layout.totalEdges,
599
+ totalTemplateEdges: layout.totalTemplateEdges,
600
+ nodeCount: layout.nodeCount,
601
+ edgeCount: layout.edgeCount,
602
+ templateLinksExcluded: layout.state === "ready",
603
+ createdAt: now,
604
+ updatedAt: now
605
+ }).run();
606
+ if (layout.state !== "ready") return;
607
+ const batchSize = 250;
608
+ for (let offset = 0; offset < layout.nodes.length; offset += batchSize) {
609
+ tx.insert(siteCrawlGraphNodes).values(layout.nodes.slice(offset, offset + batchSize).map((node) => ({
610
+ id: crypto.randomUUID(),
611
+ projectId: scope.projectId,
612
+ runId: scope.runId,
613
+ attemptId: scope.attemptId,
614
+ nodeKey: node.nodeKey,
615
+ sampleRank: node.sampleRank,
616
+ x: node.x,
617
+ y: node.y,
618
+ createdAt: now
619
+ }))).run();
620
+ }
621
+ for (let offset = 0; offset < layout.edges.length; offset += batchSize) {
622
+ tx.insert(siteCrawlGraphEdges).values(layout.edges.slice(offset, offset + batchSize).map((edge) => ({
623
+ id: crypto.randomUUID(),
624
+ projectId: scope.projectId,
625
+ runId: scope.runId,
626
+ attemptId: scope.attemptId,
627
+ edgeKey: edge.edgeKey,
628
+ sampleRank: edge.sampleRank,
629
+ sourceNodeKey: edge.sourceNodeKey,
630
+ targetNodeKey: edge.targetNodeKey,
631
+ followable: edge.followable,
632
+ occurrences: edge.occurrences,
633
+ isTemplate: edge.isTemplate,
634
+ createdAt: now
635
+ }))).run();
636
+ }
637
+ });
638
+ }
639
+ function deleteSiteCrawlGraphLayout(db, scope) {
640
+ db.delete(siteCrawlGraphLayouts).where(and(
641
+ eq(siteCrawlGraphLayouts.projectId, scope.projectId),
642
+ eq(siteCrawlGraphLayouts.runId, scope.runId),
643
+ eq(siteCrawlGraphLayouts.attemptId, scope.attemptId)
644
+ )).run();
645
+ }
646
+
647
+ export {
648
+ layoutSiteCrawlGraphInput,
649
+ prepareSiteCrawlGraphLayout,
650
+ persistSiteCrawlGraphLayout,
651
+ deleteSiteCrawlGraphLayout
652
+ };
@@ -0,0 +1,8 @@
1
+ // src/package-version.ts
2
+ import { createRequire } from "module";
3
+ var _require = createRequire(import.meta.url);
4
+ var PACKAGE_VERSION = _require("../package.json").version;
5
+
6
+ export {
7
+ PACKAGE_VERSION
8
+ };
@@ -1,15 +1,17 @@
1
1
  import {
2
2
  BUNDLED_SKILL_NAMES,
3
- PACKAGE_VERSION,
4
3
  configExists,
5
4
  installSkills,
6
5
  loadConfigRaw,
7
6
  saveConfigPatch
8
- } from "./chunk-AEE5DGTE.js";
7
+ } from "./chunk-IOP36L3C.js";
8
+ import {
9
+ PACKAGE_VERSION
10
+ } from "./chunk-AOJU5LKX.js";
9
11
  import {
10
12
  SKILL_MANIFEST_FILENAME,
11
13
  SkillsClients
12
- } from "./chunk-MMSPU72Z.js";
14
+ } from "./chunk-2U5IW3G5.js";
13
15
 
14
16
  // src/skills-autosync.ts
15
17
  import fs from "fs";