@git-stunts/git-warp 10.1.1

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 (143) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +16 -0
  3. package/README.md +480 -0
  4. package/SECURITY.md +30 -0
  5. package/bin/git-warp +24 -0
  6. package/bin/warp-graph.js +1574 -0
  7. package/index.d.ts +2366 -0
  8. package/index.js +180 -0
  9. package/package.json +129 -0
  10. package/scripts/install-git-warp.sh +258 -0
  11. package/scripts/uninstall-git-warp.sh +139 -0
  12. package/src/domain/WarpGraph.js +3157 -0
  13. package/src/domain/crdt/Dot.js +160 -0
  14. package/src/domain/crdt/LWW.js +154 -0
  15. package/src/domain/crdt/ORSet.js +371 -0
  16. package/src/domain/crdt/VersionVector.js +222 -0
  17. package/src/domain/entities/GraphNode.js +60 -0
  18. package/src/domain/errors/EmptyMessageError.js +47 -0
  19. package/src/domain/errors/ForkError.js +30 -0
  20. package/src/domain/errors/IndexError.js +23 -0
  21. package/src/domain/errors/OperationAbortedError.js +22 -0
  22. package/src/domain/errors/QueryError.js +39 -0
  23. package/src/domain/errors/SchemaUnsupportedError.js +17 -0
  24. package/src/domain/errors/ShardCorruptionError.js +56 -0
  25. package/src/domain/errors/ShardLoadError.js +57 -0
  26. package/src/domain/errors/ShardValidationError.js +61 -0
  27. package/src/domain/errors/StorageError.js +57 -0
  28. package/src/domain/errors/SyncError.js +30 -0
  29. package/src/domain/errors/TraversalError.js +23 -0
  30. package/src/domain/errors/WarpError.js +31 -0
  31. package/src/domain/errors/WormholeError.js +28 -0
  32. package/src/domain/errors/WriterError.js +39 -0
  33. package/src/domain/errors/index.js +21 -0
  34. package/src/domain/services/AnchorMessageCodec.js +99 -0
  35. package/src/domain/services/BitmapIndexBuilder.js +225 -0
  36. package/src/domain/services/BitmapIndexReader.js +435 -0
  37. package/src/domain/services/BoundaryTransitionRecord.js +463 -0
  38. package/src/domain/services/CheckpointMessageCodec.js +147 -0
  39. package/src/domain/services/CheckpointSerializerV5.js +281 -0
  40. package/src/domain/services/CheckpointService.js +384 -0
  41. package/src/domain/services/CommitDagTraversalService.js +156 -0
  42. package/src/domain/services/DagPathFinding.js +712 -0
  43. package/src/domain/services/DagTopology.js +239 -0
  44. package/src/domain/services/DagTraversal.js +245 -0
  45. package/src/domain/services/Frontier.js +108 -0
  46. package/src/domain/services/GCMetrics.js +101 -0
  47. package/src/domain/services/GCPolicy.js +122 -0
  48. package/src/domain/services/GitLogParser.js +205 -0
  49. package/src/domain/services/HealthCheckService.js +246 -0
  50. package/src/domain/services/HookInstaller.js +326 -0
  51. package/src/domain/services/HttpSyncServer.js +262 -0
  52. package/src/domain/services/IndexRebuildService.js +426 -0
  53. package/src/domain/services/IndexStalenessChecker.js +103 -0
  54. package/src/domain/services/JoinReducer.js +582 -0
  55. package/src/domain/services/KeyCodec.js +113 -0
  56. package/src/domain/services/LegacyAnchorDetector.js +67 -0
  57. package/src/domain/services/LogicalTraversal.js +351 -0
  58. package/src/domain/services/MessageCodecInternal.js +132 -0
  59. package/src/domain/services/MessageSchemaDetector.js +145 -0
  60. package/src/domain/services/MigrationService.js +55 -0
  61. package/src/domain/services/ObserverView.js +265 -0
  62. package/src/domain/services/PatchBuilderV2.js +669 -0
  63. package/src/domain/services/PatchMessageCodec.js +140 -0
  64. package/src/domain/services/ProvenanceIndex.js +337 -0
  65. package/src/domain/services/ProvenancePayload.js +242 -0
  66. package/src/domain/services/QueryBuilder.js +835 -0
  67. package/src/domain/services/StateDiff.js +300 -0
  68. package/src/domain/services/StateSerializerV5.js +156 -0
  69. package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
  70. package/src/domain/services/SyncProtocol.js +593 -0
  71. package/src/domain/services/TemporalQuery.js +201 -0
  72. package/src/domain/services/TranslationCost.js +221 -0
  73. package/src/domain/services/TraversalService.js +8 -0
  74. package/src/domain/services/WarpMessageCodec.js +29 -0
  75. package/src/domain/services/WarpStateIndexBuilder.js +127 -0
  76. package/src/domain/services/WormholeService.js +353 -0
  77. package/src/domain/types/TickReceipt.js +285 -0
  78. package/src/domain/types/WarpTypes.js +209 -0
  79. package/src/domain/types/WarpTypesV2.js +200 -0
  80. package/src/domain/utils/CachedValue.js +140 -0
  81. package/src/domain/utils/EventId.js +89 -0
  82. package/src/domain/utils/LRUCache.js +112 -0
  83. package/src/domain/utils/MinHeap.js +114 -0
  84. package/src/domain/utils/RefLayout.js +280 -0
  85. package/src/domain/utils/WriterId.js +205 -0
  86. package/src/domain/utils/cancellation.js +33 -0
  87. package/src/domain/utils/canonicalStringify.js +42 -0
  88. package/src/domain/utils/defaultClock.js +20 -0
  89. package/src/domain/utils/defaultCodec.js +51 -0
  90. package/src/domain/utils/nullLogger.js +21 -0
  91. package/src/domain/utils/roaring.js +181 -0
  92. package/src/domain/utils/shardVersion.js +9 -0
  93. package/src/domain/warp/PatchSession.js +217 -0
  94. package/src/domain/warp/Writer.js +181 -0
  95. package/src/hooks/post-merge.sh +60 -0
  96. package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
  97. package/src/infrastructure/adapters/ClockAdapter.js +57 -0
  98. package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
  99. package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
  100. package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
  101. package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
  102. package/src/infrastructure/adapters/NoOpLogger.js +62 -0
  103. package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
  104. package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
  105. package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
  106. package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
  107. package/src/infrastructure/codecs/CborCodec.js +384 -0
  108. package/src/ports/BlobPort.js +30 -0
  109. package/src/ports/ClockPort.js +25 -0
  110. package/src/ports/CodecPort.js +25 -0
  111. package/src/ports/CommitPort.js +114 -0
  112. package/src/ports/ConfigPort.js +31 -0
  113. package/src/ports/CryptoPort.js +38 -0
  114. package/src/ports/GraphPersistencePort.js +57 -0
  115. package/src/ports/HttpServerPort.js +25 -0
  116. package/src/ports/IndexStoragePort.js +39 -0
  117. package/src/ports/LoggerPort.js +68 -0
  118. package/src/ports/RefPort.js +51 -0
  119. package/src/ports/TreePort.js +51 -0
  120. package/src/visualization/index.js +26 -0
  121. package/src/visualization/layouts/converters.js +75 -0
  122. package/src/visualization/layouts/elkAdapter.js +86 -0
  123. package/src/visualization/layouts/elkLayout.js +95 -0
  124. package/src/visualization/layouts/index.js +29 -0
  125. package/src/visualization/renderers/ascii/box.js +16 -0
  126. package/src/visualization/renderers/ascii/check.js +271 -0
  127. package/src/visualization/renderers/ascii/colors.js +13 -0
  128. package/src/visualization/renderers/ascii/formatters.js +73 -0
  129. package/src/visualization/renderers/ascii/graph.js +344 -0
  130. package/src/visualization/renderers/ascii/history.js +335 -0
  131. package/src/visualization/renderers/ascii/index.js +14 -0
  132. package/src/visualization/renderers/ascii/info.js +245 -0
  133. package/src/visualization/renderers/ascii/materialize.js +255 -0
  134. package/src/visualization/renderers/ascii/path.js +240 -0
  135. package/src/visualization/renderers/ascii/progress.js +32 -0
  136. package/src/visualization/renderers/ascii/symbols.js +33 -0
  137. package/src/visualization/renderers/ascii/table.js +19 -0
  138. package/src/visualization/renderers/browser/index.js +1 -0
  139. package/src/visualization/renderers/svg/index.js +159 -0
  140. package/src/visualization/utils/ansi.js +14 -0
  141. package/src/visualization/utils/time.js +40 -0
  142. package/src/visualization/utils/truncate.js +40 -0
  143. package/src/visualization/utils/unicode.js +52 -0
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Abstract port for graph persistence operations.
3
+ *
4
+ * Defines the contract for reading and writing graph data to a Git-backed
5
+ * storage layer. Concrete adapters (e.g., GitGraphAdapter) implement this
6
+ * interface to provide actual Git operations.
7
+ *
8
+ * This is a **composite port** that implements the union of five focused ports:
9
+ *
10
+ * - {@link CommitPort} — commit creation, reading, logging, counting, ping
11
+ * - {@link BlobPort} — blob read/write
12
+ * - {@link TreePort} — tree read/write, emptyTree getter
13
+ * - {@link RefPort} — ref update/read/delete
14
+ * - {@link ConfigPort} — git config get/set
15
+ *
16
+ * Domain services should document which focused port(s) they actually depend on
17
+ * via JSDoc, even though they accept the full GraphPersistencePort at runtime.
18
+ * This enables future narrowing without breaking backward compatibility.
19
+ *
20
+ * All methods throw by default and must be overridden by implementations.
21
+ *
22
+ * @abstract
23
+ * @implements {CommitPort}
24
+ * @implements {BlobPort}
25
+ * @implements {TreePort}
26
+ * @implements {RefPort}
27
+ * @implements {ConfigPort}
28
+ */
29
+
30
+ import CommitPort from './CommitPort.js';
31
+ import BlobPort from './BlobPort.js';
32
+ import TreePort from './TreePort.js';
33
+ import RefPort from './RefPort.js';
34
+ import ConfigPort from './ConfigPort.js';
35
+
36
+ class GraphPersistencePort {}
37
+
38
+ const focusedPorts = [CommitPort, BlobPort, TreePort, RefPort, ConfigPort];
39
+ const seen = new Map();
40
+
41
+ for (const Port of focusedPorts) {
42
+ const descriptors = Object.getOwnPropertyDescriptors(Port.prototype);
43
+ delete descriptors.constructor;
44
+
45
+ for (const [name, descriptor] of Object.entries(descriptors)) {
46
+ if (seen.has(name)) {
47
+ throw new Error(
48
+ `GraphPersistencePort composition collision: "${name}" defined by both ` +
49
+ `${seen.get(name).name} and ${Port.name}`,
50
+ );
51
+ }
52
+ seen.set(name, Port);
53
+ Object.defineProperty(GraphPersistencePort.prototype, name, descriptor);
54
+ }
55
+ }
56
+
57
+ export default GraphPersistencePort;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Port for HTTP server creation.
3
+ *
4
+ * Abstracts platform-specific HTTP server APIs so domain code
5
+ * doesn't depend on node:http directly.
6
+ */
7
+ export default class HttpServerPort {
8
+ /**
9
+ * Creates an HTTP server with a platform-agnostic request handler.
10
+ *
11
+ * The request handler receives a plain object `{ method, url, headers, body }`
12
+ * and must return `{ status, headers, body }`. No raw req/res objects
13
+ * are exposed to the domain.
14
+ *
15
+ * @param {Function} requestHandler - Async function (request) => response
16
+ * @param {string} requestHandler.method - HTTP method
17
+ * @param {string} requestHandler.url - Request URL
18
+ * @param {Object} requestHandler.headers - Request headers (lowercased keys)
19
+ * @param {Buffer|undefined} requestHandler.body - Request body (undefined if none)
20
+ * @returns {{ listen: Function, close: Function, address: Function }} Server with listen(port, [host], cb(err)), close(cb), and address()
21
+ */
22
+ createServer(_requestHandler) {
23
+ throw new Error('HttpServerPort.createServer() not implemented');
24
+ }
25
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Port interface for bitmap index storage operations.
3
+ *
4
+ * This port defines the contract for persisting and retrieving
5
+ * the sharded bitmap index data. Adapters implement this interface
6
+ * to store indexes in different backends (Git, filesystem, etc.).
7
+ *
8
+ * This port is a subset of the focused ports: it uses methods from
9
+ * {@link BlobPort} (writeBlob, readBlob), {@link TreePort} (writeTree,
10
+ * readTreeOids), and {@link RefPort} (updateRef, readRef).
11
+ *
12
+ * @abstract
13
+ */
14
+
15
+ import BlobPort from './BlobPort.js';
16
+ import TreePort from './TreePort.js';
17
+ import RefPort from './RefPort.js';
18
+
19
+ class IndexStoragePort {}
20
+
21
+ const picks = [
22
+ [BlobPort, ['writeBlob', 'readBlob']],
23
+ [TreePort, ['writeTree', 'readTreeOids']],
24
+ [RefPort, ['updateRef', 'readRef']],
25
+ ];
26
+
27
+ for (const [Port, methods] of picks) {
28
+ const descriptors = Object.getOwnPropertyDescriptors(Port.prototype);
29
+ for (const name of methods) {
30
+ if (!descriptors[name]) {
31
+ throw new Error(
32
+ `IndexStoragePort: "${name}" not found on ${Port.name}.prototype`,
33
+ );
34
+ }
35
+ Object.defineProperty(IndexStoragePort.prototype, name, descriptors[name]);
36
+ }
37
+ }
38
+
39
+ export default IndexStoragePort;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Port interface for structured logging operations.
3
+ *
4
+ * This port defines the contract for logging across the application.
5
+ * Adapters implement this interface to provide different logging
6
+ * backends (console, file, external services, no-op for testing).
7
+ *
8
+ * All methods accept an optional context object for structured metadata.
9
+ * Child loggers inherit and merge parent context.
10
+ *
11
+ * @abstract
12
+ */
13
+ export default class LoggerPort {
14
+ /**
15
+ * Log a debug-level message.
16
+ * @param {string} message - The log message
17
+ * @param {Record<string, unknown>} [context] - Structured metadata
18
+ * @returns {void}
19
+ * @abstract
20
+ */
21
+ debug(_message, _context) {
22
+ throw new Error('Not implemented');
23
+ }
24
+
25
+ /**
26
+ * Log an info-level message.
27
+ * @param {string} message - The log message
28
+ * @param {Record<string, unknown>} [context] - Structured metadata
29
+ * @returns {void}
30
+ * @abstract
31
+ */
32
+ info(_message, _context) {
33
+ throw new Error('Not implemented');
34
+ }
35
+
36
+ /**
37
+ * Log a warning-level message.
38
+ * @param {string} message - The log message
39
+ * @param {Record<string, unknown>} [context] - Structured metadata
40
+ * @returns {void}
41
+ * @abstract
42
+ */
43
+ warn(_message, _context) {
44
+ throw new Error('Not implemented');
45
+ }
46
+
47
+ /**
48
+ * Log an error-level message.
49
+ * @param {string} message - The log message
50
+ * @param {Record<string, unknown>} [context] - Structured metadata
51
+ * @returns {void}
52
+ * @abstract
53
+ */
54
+ error(_message, _context) {
55
+ throw new Error('Not implemented');
56
+ }
57
+
58
+ /**
59
+ * Create a child logger with additional base context.
60
+ * Child loggers inherit parent context and merge with their own.
61
+ * @param {Record<string, unknown>} context - Base context for the child logger
62
+ * @returns {LoggerPort} A new logger instance with merged context
63
+ * @abstract
64
+ */
65
+ child(_context) {
66
+ throw new Error('Not implemented');
67
+ }
68
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Port for Git ref operations.
3
+ *
4
+ * Defines the contract for creating, reading, and deleting Git refs.
5
+ * This is one of five focused ports extracted from GraphPersistencePort.
6
+ *
7
+ * @abstract
8
+ * @see GraphPersistencePort - Composite port implementing all five focused ports
9
+ */
10
+ export default class RefPort {
11
+ /**
12
+ * Updates a ref to point to an OID.
13
+ * @param {string} ref - The ref name (e.g., 'refs/warp/events/writers/alice')
14
+ * @param {string} oid - The OID to point to
15
+ * @returns {Promise<void>}
16
+ * @throws {Error} If not implemented by a concrete adapter
17
+ */
18
+ async updateRef(_ref, _oid) {
19
+ throw new Error('RefPort.updateRef() not implemented');
20
+ }
21
+
22
+ /**
23
+ * Reads the OID a ref points to.
24
+ * @param {string} ref - The ref name
25
+ * @returns {Promise<string|null>} The OID, or null if the ref does not exist
26
+ * @throws {Error} If not implemented by a concrete adapter
27
+ */
28
+ async readRef(_ref) {
29
+ throw new Error('RefPort.readRef() not implemented');
30
+ }
31
+
32
+ /**
33
+ * Deletes a ref.
34
+ * @param {string} ref - The ref name to delete
35
+ * @returns {Promise<void>}
36
+ * @throws {Error} If not implemented by a concrete adapter
37
+ */
38
+ async deleteRef(_ref) {
39
+ throw new Error('RefPort.deleteRef() not implemented');
40
+ }
41
+
42
+ /**
43
+ * Lists refs matching a prefix.
44
+ * @param {string} prefix - The ref prefix to match (e.g., 'refs/warp/events/writers/')
45
+ * @returns {Promise<string[]>} Array of matching ref names
46
+ * @throws {Error} If not implemented by a concrete adapter
47
+ */
48
+ async listRefs(_prefix) {
49
+ throw new Error('RefPort.listRefs() not implemented');
50
+ }
51
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Port for Git tree operations.
3
+ *
4
+ * Defines the contract for writing and reading Git tree objects.
5
+ * This is one of five focused ports extracted from GraphPersistencePort.
6
+ *
7
+ * @abstract
8
+ * @see GraphPersistencePort - Composite port implementing all five focused ports
9
+ */
10
+ export default class TreePort {
11
+ /**
12
+ * Creates a Git tree from mktree-formatted entries.
13
+ * @param {string[]} entries - Lines in git mktree format (e.g., "100644 blob <oid>\t<path>")
14
+ * @returns {Promise<string>} The Git OID of the created tree
15
+ * @throws {Error} If not implemented by a concrete adapter
16
+ */
17
+ async writeTree(_entries) {
18
+ throw new Error('TreePort.writeTree() not implemented');
19
+ }
20
+
21
+ /**
22
+ * Reads a tree and returns a map of path to content.
23
+ * @param {string} treeOid - The tree OID to read
24
+ * @returns {Promise<Record<string, Buffer>>} Map of file path to blob content
25
+ * @throws {Error} If not implemented by a concrete adapter
26
+ */
27
+ async readTree(_treeOid) {
28
+ throw new Error('TreePort.readTree() not implemented');
29
+ }
30
+
31
+ /**
32
+ * Reads a tree and returns a map of path to blob OID.
33
+ * Useful for lazy-loading shards without reading all blob contents.
34
+ * @param {string} treeOid - The tree OID to read
35
+ * @returns {Promise<Record<string, string>>} Map of file path to blob OID
36
+ * @throws {Error} If not implemented by a concrete adapter
37
+ */
38
+ async readTreeOids(_treeOid) {
39
+ throw new Error('TreePort.readTreeOids() not implemented');
40
+ }
41
+
42
+ /**
43
+ * The well-known SHA for Git's empty tree object.
44
+ * All WARP graph commits point to this tree so that no files appear in the working directory.
45
+ * @type {string}
46
+ * @readonly
47
+ */
48
+ get emptyTree() {
49
+ throw new Error('TreePort.emptyTree not implemented');
50
+ }
51
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Visualization module - main exports
3
+ */
4
+
5
+ // ASCII renderers
6
+ export * from './renderers/ascii/index.js';
7
+
8
+ // SVG renderer
9
+ export { renderSvg } from './renderers/svg/index.js';
10
+
11
+ // Layout engine
12
+ export {
13
+ layoutGraph,
14
+ queryResultToGraphData,
15
+ pathResultToGraphData,
16
+ rawGraphToGraphData,
17
+ toElkGraph,
18
+ getDefaultLayoutOptions,
19
+ runLayout,
20
+ } from './layouts/index.js';
21
+
22
+ // Utils
23
+ export { truncate } from './utils/truncate.js';
24
+ export { timeAgo, formatDuration } from './utils/time.js';
25
+ export { padRight, padLeft, center } from './utils/unicode.js';
26
+ export { stripAnsi } from './utils/ansi.js';
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Data converters: transform WarpGraph payloads into a normalized graph-data
3
+ * intermediate format consumed by the ELK adapter.
4
+ *
5
+ * Intermediate format:
6
+ * { nodes: [{ id, label, props? }], edges: [{ from, to, label? }] }
7
+ */
8
+
9
+ /**
10
+ * Converts a query result payload + edge array into graph data.
11
+ * Edges are filtered to only those connecting matched nodes.
12
+ *
13
+ * @param {Object} payload - Query result { nodes: [{id, props}] }
14
+ * @param {Array} edges - Edge array from graph.getEdges()
15
+ * @returns {{ nodes: Array, edges: Array }}
16
+ */
17
+ export function queryResultToGraphData(payload, edges) {
18
+ const nodes = (payload?.nodes ?? []).map((n) => ({
19
+ id: n.id,
20
+ label: n.id,
21
+ props: n.props,
22
+ }));
23
+
24
+ const nodeSet = new Set(nodes.map((n) => n.id));
25
+
26
+ const filtered = (edges ?? [])
27
+ .filter((e) => nodeSet.has(e.from) && nodeSet.has(e.to))
28
+ .map((e) => ({ from: e.from, to: e.to, label: e.label }));
29
+
30
+ return { nodes, edges: filtered };
31
+ }
32
+
33
+ /**
34
+ * Converts a path result payload into graph data.
35
+ * Builds a linear chain of nodes with labelled edges.
36
+ *
37
+ * @param {Object} payload - Path result { path: string[], edges?: string[] }
38
+ * @returns {{ nodes: Array, edges: Array }}
39
+ */
40
+ export function pathResultToGraphData(payload) {
41
+ const pathArr = payload?.path ?? [];
42
+ const edgeLabels = payload?.edges ?? [];
43
+
44
+ const nodes = pathArr.map((id) => ({ id, label: id }));
45
+
46
+ const edges = [];
47
+ for (let i = 0; i < pathArr.length - 1; i++) {
48
+ edges.push({
49
+ from: pathArr[i],
50
+ to: pathArr[i + 1],
51
+ label: edgeLabels[i],
52
+ });
53
+ }
54
+
55
+ return { nodes, edges };
56
+ }
57
+
58
+ /**
59
+ * Converts raw getNodes() + getEdges() output into graph data.
60
+ *
61
+ * @param {string[]} nodeIds - Array of node IDs
62
+ * @param {Array} edges - Edge array from graph.getEdges()
63
+ * @returns {{ nodes: Array, edges: Array }}
64
+ */
65
+ export function rawGraphToGraphData(nodeIds, edges) {
66
+ const nodes = (nodeIds ?? []).map((id) => ({ id, label: id }));
67
+
68
+ const mapped = (edges ?? []).map((e) => ({
69
+ from: e.from,
70
+ to: e.to,
71
+ label: e.label,
72
+ }));
73
+
74
+ return { nodes, edges: mapped };
75
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ELK adapter: converts normalised graph data into ELK JSON input.
3
+ */
4
+
5
+ const LAYOUT_PRESETS = {
6
+ query: {
7
+ 'elk.algorithm': 'layered',
8
+ 'elk.direction': 'DOWN',
9
+ 'elk.spacing.nodeNode': '40',
10
+ 'elk.layered.spacing.nodeNodeBetweenLayers': '60',
11
+ },
12
+ path: {
13
+ 'elk.algorithm': 'layered',
14
+ 'elk.direction': 'RIGHT',
15
+ 'elk.spacing.nodeNode': '40',
16
+ 'elk.layered.spacing.nodeNodeBetweenLayers': '60',
17
+ },
18
+ slice: {
19
+ 'elk.algorithm': 'layered',
20
+ 'elk.direction': 'DOWN',
21
+ 'elk.spacing.nodeNode': '40',
22
+ 'elk.layered.spacing.nodeNodeBetweenLayers': '60',
23
+ },
24
+ };
25
+
26
+ const DEFAULT_PRESET = LAYOUT_PRESETS.query;
27
+
28
+ /**
29
+ * Returns ELK layout options for a given visualisation type.
30
+ *
31
+ * @param {'query'|'path'|'slice'} type
32
+ * @returns {Object} ELK layout options
33
+ */
34
+ export function getDefaultLayoutOptions(type) {
35
+ return LAYOUT_PRESETS[type] ?? DEFAULT_PRESET;
36
+ }
37
+
38
+ /**
39
+ * Estimates pixel width for a node label.
40
+ * Approximates monospace glyph width at ~9px with 24px padding.
41
+ */
42
+ function estimateNodeWidth(label) {
43
+ const charWidth = 9;
44
+ const padding = 24;
45
+ const minWidth = 80;
46
+ return Math.max((label?.length ?? 0) * charWidth + padding, minWidth);
47
+ }
48
+
49
+ const NODE_HEIGHT = 40;
50
+
51
+ /**
52
+ * Converts normalised graph data to an ELK graph JSON object.
53
+ *
54
+ * @param {{ nodes: Array, edges: Array }} graphData
55
+ * @param {{ type?: string, layoutOptions?: Object }} [options]
56
+ * @returns {Object} ELK-format graph
57
+ */
58
+ export function toElkGraph(graphData, options = {}) {
59
+ const { type = 'query', layoutOptions } = options;
60
+
61
+ const children = (graphData.nodes ?? []).map((n) => ({
62
+ id: n.id,
63
+ width: estimateNodeWidth(n.label),
64
+ height: NODE_HEIGHT,
65
+ labels: [{ text: n.label ?? n.id }],
66
+ }));
67
+
68
+ const edges = (graphData.edges ?? []).map((e, i) => {
69
+ const edge = {
70
+ id: `e${i}`,
71
+ sources: [e.from],
72
+ targets: [e.to],
73
+ };
74
+ if (e.label) {
75
+ edge.labels = [{ text: e.label }];
76
+ }
77
+ return edge;
78
+ });
79
+
80
+ return {
81
+ id: 'root',
82
+ layoutOptions: layoutOptions ?? getDefaultLayoutOptions(type),
83
+ children,
84
+ edges,
85
+ };
86
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * ELK layout runner: lazy-loads elkjs and executes layout.
3
+ *
4
+ * The ELK engine (~2.5 MB) is loaded via dynamic import() only when
5
+ * a layout is actually requested, keeping normal CLI startup fast.
6
+ */
7
+
8
+ let elkPromise = null;
9
+
10
+ /**
11
+ * Returns (or creates) a singleton ELK instance.
12
+ * @returns {Promise<Object>} ELK instance
13
+ */
14
+ function getElk() {
15
+ if (!elkPromise) {
16
+ elkPromise = import('elkjs/lib/elk.bundled.js').then((mod) => new mod.default());
17
+ }
18
+ return elkPromise;
19
+ }
20
+
21
+ /**
22
+ * Runs ELK layout on a graph and returns a PositionedGraph.
23
+ *
24
+ * @param {Object} elkGraph - ELK-format graph from toElkGraph()
25
+ * @returns {Promise<Object>} PositionedGraph
26
+ */
27
+ export async function runLayout(elkGraph) {
28
+ let result;
29
+ try {
30
+ const elk = await getElk();
31
+ result = await elk.layout(elkGraph);
32
+ } catch {
33
+ return fallbackLayout(elkGraph);
34
+ }
35
+ return toPositionedGraph(result);
36
+ }
37
+
38
+ /**
39
+ * Converts ELK output to a PositionedGraph.
40
+ */
41
+ function toPositionedGraph(result) {
42
+ const nodes = (result.children ?? []).map((c) => ({
43
+ id: c.id,
44
+ x: c.x ?? 0,
45
+ y: c.y ?? 0,
46
+ width: c.width ?? 80,
47
+ height: c.height ?? 40,
48
+ label: c.labels?.[0]?.text ?? c.id,
49
+ }));
50
+
51
+ const edges = (result.edges ?? []).map((e) => ({
52
+ id: e.id,
53
+ source: e.sources?.[0] ?? '',
54
+ target: e.targets?.[0] ?? '',
55
+ label: e.labels?.[0]?.text,
56
+ sections: e.sections ?? [],
57
+ }));
58
+
59
+ return {
60
+ nodes,
61
+ edges,
62
+ width: result.width ?? 0,
63
+ height: result.height ?? 0,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Fallback: line nodes up horizontally when ELK fails.
69
+ */
70
+ function fallbackLayout(elkGraph) {
71
+ let x = 20;
72
+ const nodes = (elkGraph.children ?? []).map((c) => {
73
+ const node = {
74
+ id: c.id,
75
+ x,
76
+ y: 20,
77
+ width: c.width ?? 80,
78
+ height: c.height ?? 40,
79
+ label: c.labels?.[0]?.text ?? c.id,
80
+ };
81
+ x += (c.width ?? 80) + 40;
82
+ return node;
83
+ });
84
+
85
+ const edges = (elkGraph.edges ?? []).map((e) => ({
86
+ id: e.id,
87
+ source: e.sources?.[0] ?? '',
88
+ target: e.targets?.[0] ?? '',
89
+ label: e.labels?.[0]?.text,
90
+ sections: [],
91
+ }));
92
+
93
+ const totalWidth = x;
94
+ return { nodes, edges, width: totalWidth, height: 80 };
95
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Layout engine facade.
3
+ *
4
+ * Orchestrates: converter → ELK adapter → ELK runner → PositionedGraph.
5
+ */
6
+
7
+ export {
8
+ queryResultToGraphData,
9
+ pathResultToGraphData,
10
+ rawGraphToGraphData,
11
+ } from './converters.js';
12
+
13
+ export { toElkGraph, getDefaultLayoutOptions } from './elkAdapter.js';
14
+ export { runLayout } from './elkLayout.js';
15
+
16
+ import { toElkGraph } from './elkAdapter.js';
17
+ import { runLayout } from './elkLayout.js';
18
+
19
+ /**
20
+ * Full pipeline: graphData → PositionedGraph.
21
+ *
22
+ * @param {{ nodes: Array, edges: Array }} graphData - Normalised graph data
23
+ * @param {{ type?: string, layoutOptions?: Object }} [options]
24
+ * @returns {Promise<Object>} PositionedGraph
25
+ */
26
+ export async function layoutGraph(graphData, options = {}) {
27
+ const elkGraph = toElkGraph(graphData, options);
28
+ return await runLayout(elkGraph);
29
+ }
@@ -0,0 +1,16 @@
1
+ import boxen from 'boxen';
2
+
3
+ /**
4
+ * Wraps content in a bordered box using boxen.
5
+ *
6
+ * @param {string} content - The text content to display inside the box
7
+ * @param {Object} [options] - Options forwarded to boxen (e.g. title, borderColor)
8
+ * @returns {string} The boxed content string
9
+ */
10
+ export function createBox(content, options = {}) {
11
+ return boxen(content, {
12
+ padding: 1,
13
+ borderStyle: 'double',
14
+ ...options,
15
+ });
16
+ }