clawvault 2.5.2 → 2.5.4

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 (71) hide show
  1. package/README.md +159 -200
  2. package/bin/clawvault.js +111 -111
  3. package/bin/command-registration.test.js +166 -166
  4. package/bin/command-runtime.js +93 -93
  5. package/bin/command-runtime.test.js +154 -154
  6. package/bin/help-contract.test.js +39 -39
  7. package/bin/register-config-commands.js +153 -153
  8. package/bin/register-config-route-commands.test.js +121 -121
  9. package/bin/register-core-commands.js +237 -237
  10. package/bin/register-kanban-commands.js +56 -56
  11. package/bin/register-kanban-commands.test.js +83 -83
  12. package/bin/register-maintenance-commands.js +282 -282
  13. package/bin/register-project-commands.js +209 -209
  14. package/bin/register-project-commands.test.js +206 -206
  15. package/bin/register-query-commands.js +317 -317
  16. package/bin/register-query-commands.test.js +65 -65
  17. package/bin/register-resilience-commands.js +182 -182
  18. package/bin/register-resilience-commands.test.js +81 -81
  19. package/bin/register-route-commands.js +114 -114
  20. package/bin/register-session-lifecycle-commands.js +206 -206
  21. package/bin/register-tailscale-commands.js +106 -106
  22. package/bin/register-task-commands.js +348 -348
  23. package/bin/register-task-commands.test.js +69 -69
  24. package/bin/register-template-commands.js +72 -72
  25. package/bin/register-vault-operations-commands.js +300 -300
  26. package/bin/test-helpers/cli-command-fixtures.js +119 -119
  27. package/dashboard/lib/graph-diff.js +104 -104
  28. package/dashboard/lib/graph-diff.test.js +75 -75
  29. package/dashboard/lib/vault-parser.js +556 -556
  30. package/dashboard/lib/vault-parser.test.js +254 -254
  31. package/dashboard/public/app.js +796 -796
  32. package/dashboard/public/index.html +52 -52
  33. package/dashboard/public/styles.css +221 -221
  34. package/dashboard/server.js +374 -374
  35. package/dist/{chunk-3FP5BJ42.js → chunk-4QYGFWRM.js} +1 -1
  36. package/dist/{chunk-M25QVSJM.js → chunk-AXKYDCNN.js} +1 -1
  37. package/dist/{chunk-CLE2HHNT.js → chunk-IVRIKYFE.js} +18 -11
  38. package/dist/{chunk-HRTPQQF2.js → chunk-IZEY5S74.js} +1 -1
  39. package/dist/{chunk-HWUNREDJ.js → chunk-JDLOL2PL.js} +4 -4
  40. package/dist/{chunk-AY4PGUVL.js → chunk-KL4NAOMO.js} +1 -1
  41. package/dist/{chunk-O7XHXF7F.js → chunk-MAKNAHAW.js} +4 -4
  42. package/dist/{chunk-PLZKZW4I.js → chunk-OSMS7QIG.js} +1 -1
  43. package/dist/{chunk-NZ4ZZNSR.js → chunk-THRJVD4L.js} +1 -1
  44. package/dist/{chunk-4GBPTBFJ.js → chunk-TIGW564L.js} +1 -1
  45. package/dist/{chunk-BHO7WSAY.js → chunk-W2HNZC22.js} +3 -3
  46. package/dist/{chunk-GFJ3LIIB.js → chunk-XAVB4GB4.js} +1 -1
  47. package/dist/cli/index.js +10 -10
  48. package/dist/commands/context.js +3 -3
  49. package/dist/commands/doctor.js +4 -4
  50. package/dist/commands/embed.js +2 -2
  51. package/dist/commands/observe.js +2 -2
  52. package/dist/commands/setup.js +2 -2
  53. package/dist/commands/sleep.js +2 -2
  54. package/dist/commands/status.js +3 -3
  55. package/dist/commands/tailscale.js +3 -3
  56. package/dist/commands/wake.js +2 -2
  57. package/dist/index.js +12 -12
  58. package/dist/lib/tailscale.js +2 -2
  59. package/dist/lib/webdav.js +1 -1
  60. package/hooks/clawvault/HOOK.md +83 -74
  61. package/hooks/clawvault/handler.js +816 -816
  62. package/hooks/clawvault/handler.test.js +263 -263
  63. package/package.json +94 -125
  64. package/templates/checkpoint.md +19 -19
  65. package/templates/daily-note.md +19 -19
  66. package/templates/daily.md +19 -19
  67. package/templates/decision.md +17 -17
  68. package/templates/handoff.md +19 -19
  69. package/templates/lesson.md +16 -16
  70. package/templates/person.md +19 -19
  71. package/templates/project.md +23 -23
@@ -1,119 +1,119 @@
1
- import { Command } from 'commander';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import { registerCoreCommands } from '../register-core-commands.js';
5
- import { registerMaintenanceCommands } from '../register-maintenance-commands.js';
6
- import { registerQueryCommands } from '../register-query-commands.js';
7
- import { registerResilienceCommands } from '../register-resilience-commands.js';
8
- import { registerSessionLifecycleCommands } from '../register-session-lifecycle-commands.js';
9
- import { registerTemplateCommands } from '../register-template-commands.js';
10
- import { registerVaultOperationsCommands } from '../register-vault-operations-commands.js';
11
- import { registerConfigCommands } from '../register-config-commands.js';
12
- import { registerRouteCommands } from '../register-route-commands.js';
13
- import { registerTaskCommands } from '../register-task-commands.js';
14
- import { registerKanbanCommands } from '../register-kanban-commands.js';
15
- import { registerProjectCommands } from '../register-project-commands.js';
16
-
17
- export const chalkStub = {
18
- cyan: (value) => value,
19
- green: (value) => value,
20
- red: (value) => value,
21
- dim: (value) => value,
22
- yellow: (value) => value,
23
- white: (value) => value
24
- };
25
-
26
- export function stubResolveVaultPath(value) {
27
- return value ?? '/vault';
28
- }
29
-
30
- export function createVaultStub(overrides = {}) {
31
- return {
32
- store: async () => ({}),
33
- capture: async () => ({}),
34
- find: async () => [],
35
- vsearch: async () => [],
36
- list: async () => [],
37
- get: async () => null,
38
- stats: async () => ({ tags: [], categories: {} }),
39
- sync: async () => ({ copied: [], deleted: [], unchanged: [], errors: [] }),
40
- reindex: async () => 0,
41
- remember: async () => ({ id: '' }),
42
- getQmdCollection: () => '',
43
- createHandoff: async () => ({ id: '', path: '' }),
44
- generateRecap: async () => ({}),
45
- formatRecap: () => '',
46
- ...overrides
47
- };
48
- }
49
-
50
- export function createGetVaultStub(overrides = {}) {
51
- return async () => createVaultStub(overrides);
52
- }
53
-
54
- export function registerAllCommandModules(program = new Command()) {
55
- const getVault = createGetVaultStub();
56
-
57
- registerCoreCommands(program, {
58
- chalk: chalkStub,
59
- path,
60
- fs,
61
- createVault: async () => ({ getCategories: () => [], getQmdRoot: () => '', getQmdCollection: () => '' }),
62
- getVault,
63
- runQmd: async () => {}
64
- });
65
-
66
- registerQueryCommands(program, {
67
- chalk: chalkStub,
68
- getVault,
69
- resolveVaultPath: stubResolveVaultPath,
70
- QmdUnavailableError: class extends Error {},
71
- printQmdMissing: () => {}
72
- });
73
-
74
- registerVaultOperationsCommands(program, {
75
- chalk: chalkStub,
76
- fs,
77
- getVault,
78
- runQmd: async () => {},
79
- resolveVaultPath: stubResolveVaultPath,
80
- path
81
- });
82
-
83
- registerMaintenanceCommands(program, { chalk: chalkStub });
84
- registerResilienceCommands(program, {
85
- chalk: chalkStub,
86
- resolveVaultPath: stubResolveVaultPath
87
- });
88
- registerSessionLifecycleCommands(program, {
89
- chalk: chalkStub,
90
- resolveVaultPath: stubResolveVaultPath,
91
- QmdUnavailableError: class extends Error {},
92
- printQmdMissing: () => {},
93
- getVault,
94
- runQmd: async () => {}
95
- });
96
- registerTemplateCommands(program, { chalk: chalkStub });
97
- registerConfigCommands(program, {
98
- chalk: chalkStub,
99
- resolveVaultPath: stubResolveVaultPath
100
- });
101
- registerRouteCommands(program, {
102
- chalk: chalkStub,
103
- resolveVaultPath: stubResolveVaultPath
104
- });
105
- registerTaskCommands(program, {
106
- chalk: chalkStub,
107
- resolveVaultPath: stubResolveVaultPath
108
- });
109
- registerKanbanCommands(program, {
110
- chalk: chalkStub,
111
- resolveVaultPath: stubResolveVaultPath
112
- });
113
- registerProjectCommands(program, {
114
- chalk: chalkStub,
115
- resolveVaultPath: stubResolveVaultPath
116
- });
117
-
118
- return program;
119
- }
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { registerCoreCommands } from '../register-core-commands.js';
5
+ import { registerMaintenanceCommands } from '../register-maintenance-commands.js';
6
+ import { registerQueryCommands } from '../register-query-commands.js';
7
+ import { registerResilienceCommands } from '../register-resilience-commands.js';
8
+ import { registerSessionLifecycleCommands } from '../register-session-lifecycle-commands.js';
9
+ import { registerTemplateCommands } from '../register-template-commands.js';
10
+ import { registerVaultOperationsCommands } from '../register-vault-operations-commands.js';
11
+ import { registerConfigCommands } from '../register-config-commands.js';
12
+ import { registerRouteCommands } from '../register-route-commands.js';
13
+ import { registerTaskCommands } from '../register-task-commands.js';
14
+ import { registerKanbanCommands } from '../register-kanban-commands.js';
15
+ import { registerProjectCommands } from '../register-project-commands.js';
16
+
17
+ export const chalkStub = {
18
+ cyan: (value) => value,
19
+ green: (value) => value,
20
+ red: (value) => value,
21
+ dim: (value) => value,
22
+ yellow: (value) => value,
23
+ white: (value) => value
24
+ };
25
+
26
+ export function stubResolveVaultPath(value) {
27
+ return value ?? '/vault';
28
+ }
29
+
30
+ export function createVaultStub(overrides = {}) {
31
+ return {
32
+ store: async () => ({}),
33
+ capture: async () => ({}),
34
+ find: async () => [],
35
+ vsearch: async () => [],
36
+ list: async () => [],
37
+ get: async () => null,
38
+ stats: async () => ({ tags: [], categories: {} }),
39
+ sync: async () => ({ copied: [], deleted: [], unchanged: [], errors: [] }),
40
+ reindex: async () => 0,
41
+ remember: async () => ({ id: '' }),
42
+ getQmdCollection: () => '',
43
+ createHandoff: async () => ({ id: '', path: '' }),
44
+ generateRecap: async () => ({}),
45
+ formatRecap: () => '',
46
+ ...overrides
47
+ };
48
+ }
49
+
50
+ export function createGetVaultStub(overrides = {}) {
51
+ return async () => createVaultStub(overrides);
52
+ }
53
+
54
+ export function registerAllCommandModules(program = new Command()) {
55
+ const getVault = createGetVaultStub();
56
+
57
+ registerCoreCommands(program, {
58
+ chalk: chalkStub,
59
+ path,
60
+ fs,
61
+ createVault: async () => ({ getCategories: () => [], getQmdRoot: () => '', getQmdCollection: () => '' }),
62
+ getVault,
63
+ runQmd: async () => {}
64
+ });
65
+
66
+ registerQueryCommands(program, {
67
+ chalk: chalkStub,
68
+ getVault,
69
+ resolveVaultPath: stubResolveVaultPath,
70
+ QmdUnavailableError: class extends Error {},
71
+ printQmdMissing: () => {}
72
+ });
73
+
74
+ registerVaultOperationsCommands(program, {
75
+ chalk: chalkStub,
76
+ fs,
77
+ getVault,
78
+ runQmd: async () => {},
79
+ resolveVaultPath: stubResolveVaultPath,
80
+ path
81
+ });
82
+
83
+ registerMaintenanceCommands(program, { chalk: chalkStub });
84
+ registerResilienceCommands(program, {
85
+ chalk: chalkStub,
86
+ resolveVaultPath: stubResolveVaultPath
87
+ });
88
+ registerSessionLifecycleCommands(program, {
89
+ chalk: chalkStub,
90
+ resolveVaultPath: stubResolveVaultPath,
91
+ QmdUnavailableError: class extends Error {},
92
+ printQmdMissing: () => {},
93
+ getVault,
94
+ runQmd: async () => {}
95
+ });
96
+ registerTemplateCommands(program, { chalk: chalkStub });
97
+ registerConfigCommands(program, {
98
+ chalk: chalkStub,
99
+ resolveVaultPath: stubResolveVaultPath
100
+ });
101
+ registerRouteCommands(program, {
102
+ chalk: chalkStub,
103
+ resolveVaultPath: stubResolveVaultPath
104
+ });
105
+ registerTaskCommands(program, {
106
+ chalk: chalkStub,
107
+ resolveVaultPath: stubResolveVaultPath
108
+ });
109
+ registerKanbanCommands(program, {
110
+ chalk: chalkStub,
111
+ resolveVaultPath: stubResolveVaultPath
112
+ });
113
+ registerProjectCommands(program, {
114
+ chalk: chalkStub,
115
+ resolveVaultPath: stubResolveVaultPath
116
+ });
117
+
118
+ return program;
119
+ }
@@ -1,104 +1,104 @@
1
- function toNodeSignature(node) {
2
- return JSON.stringify({
3
- title: node.title,
4
- category: node.category,
5
- tags: Array.isArray(node.tags) ? [...node.tags].sort() : [],
6
- path: node.path,
7
- missing: Boolean(node.missing),
8
- degree: Number(node.degree ?? 0)
9
- });
10
- }
11
-
12
- function toEdgeKey(edge) {
13
- const type = edge.type ?? '';
14
- const label = edge.label ?? '';
15
- return `${edge.source}=>${edge.target}:${type}:${label}`;
16
- }
17
-
18
- /**
19
- * Compute an efficient patch between graph snapshots.
20
- * @param {{nodes: Array<object>, edges: Array<object>, stats?: object}} previousGraph
21
- * @param {{nodes: Array<object>, edges: Array<object>, stats?: object}} nextGraph
22
- */
23
- export function diffGraphs(previousGraph, nextGraph) {
24
- const previousNodes = previousGraph?.nodes ?? [];
25
- const nextNodes = nextGraph?.nodes ?? [];
26
- const previousEdges = previousGraph?.edges ?? [];
27
- const nextEdges = nextGraph?.edges ?? [];
28
-
29
- const previousNodeById = new Map(previousNodes.map((node) => [node.id, node]));
30
- const nextNodeById = new Map(nextNodes.map((node) => [node.id, node]));
31
-
32
- const addedNodes = [];
33
- const updatedNodes = [];
34
- const removedNodeIds = [];
35
-
36
- for (const [nodeId, nextNode] of nextNodeById.entries()) {
37
- const previousNode = previousNodeById.get(nodeId);
38
- if (!previousNode) {
39
- addedNodes.push(nextNode);
40
- continue;
41
- }
42
- if (toNodeSignature(previousNode) !== toNodeSignature(nextNode)) {
43
- updatedNodes.push(nextNode);
44
- }
45
- }
46
-
47
- for (const nodeId of previousNodeById.keys()) {
48
- if (!nextNodeById.has(nodeId)) {
49
- removedNodeIds.push(nodeId);
50
- }
51
- }
52
-
53
- const previousEdgeByKey = new Map(previousEdges.map((edge) => [toEdgeKey(edge), edge]));
54
- const nextEdgeByKey = new Map(nextEdges.map((edge) => [toEdgeKey(edge), edge]));
55
- const addedEdges = [];
56
- const removedEdges = [];
57
-
58
- for (const [edgeKey, edge] of nextEdgeByKey.entries()) {
59
- if (!previousEdgeByKey.has(edgeKey)) {
60
- addedEdges.push(edge);
61
- }
62
- }
63
-
64
- for (const [edgeKey, edge] of previousEdgeByKey.entries()) {
65
- if (!nextEdgeByKey.has(edgeKey)) {
66
- removedEdges.push(edge);
67
- }
68
- }
69
-
70
- const touchedNodeIds = new Set();
71
- for (const node of addedNodes) {
72
- touchedNodeIds.add(node.id);
73
- }
74
- for (const node of updatedNodes) {
75
- touchedNodeIds.add(node.id);
76
- }
77
- for (const nodeId of removedNodeIds) {
78
- touchedNodeIds.add(nodeId);
79
- }
80
- for (const edge of addedEdges) {
81
- touchedNodeIds.add(edge.source);
82
- touchedNodeIds.add(edge.target);
83
- }
84
- for (const edge of removedEdges) {
85
- touchedNodeIds.add(edge.source);
86
- touchedNodeIds.add(edge.target);
87
- }
88
-
89
- return {
90
- addedNodes,
91
- updatedNodes,
92
- removedNodeIds,
93
- addedEdges,
94
- removedEdges,
95
- changedNodeIds: Array.from(touchedNodeIds).sort((a, b) => a.localeCompare(b)),
96
- stats: nextGraph?.stats ?? null,
97
- hasChanges:
98
- addedNodes.length > 0 ||
99
- updatedNodes.length > 0 ||
100
- removedNodeIds.length > 0 ||
101
- addedEdges.length > 0 ||
102
- removedEdges.length > 0
103
- };
104
- }
1
+ function toNodeSignature(node) {
2
+ return JSON.stringify({
3
+ title: node.title,
4
+ category: node.category,
5
+ tags: Array.isArray(node.tags) ? [...node.tags].sort() : [],
6
+ path: node.path,
7
+ missing: Boolean(node.missing),
8
+ degree: Number(node.degree ?? 0)
9
+ });
10
+ }
11
+
12
+ function toEdgeKey(edge) {
13
+ const type = edge.type ?? '';
14
+ const label = edge.label ?? '';
15
+ return `${edge.source}=>${edge.target}:${type}:${label}`;
16
+ }
17
+
18
+ /**
19
+ * Compute an efficient patch between graph snapshots.
20
+ * @param {{nodes: Array<object>, edges: Array<object>, stats?: object}} previousGraph
21
+ * @param {{nodes: Array<object>, edges: Array<object>, stats?: object}} nextGraph
22
+ */
23
+ export function diffGraphs(previousGraph, nextGraph) {
24
+ const previousNodes = previousGraph?.nodes ?? [];
25
+ const nextNodes = nextGraph?.nodes ?? [];
26
+ const previousEdges = previousGraph?.edges ?? [];
27
+ const nextEdges = nextGraph?.edges ?? [];
28
+
29
+ const previousNodeById = new Map(previousNodes.map((node) => [node.id, node]));
30
+ const nextNodeById = new Map(nextNodes.map((node) => [node.id, node]));
31
+
32
+ const addedNodes = [];
33
+ const updatedNodes = [];
34
+ const removedNodeIds = [];
35
+
36
+ for (const [nodeId, nextNode] of nextNodeById.entries()) {
37
+ const previousNode = previousNodeById.get(nodeId);
38
+ if (!previousNode) {
39
+ addedNodes.push(nextNode);
40
+ continue;
41
+ }
42
+ if (toNodeSignature(previousNode) !== toNodeSignature(nextNode)) {
43
+ updatedNodes.push(nextNode);
44
+ }
45
+ }
46
+
47
+ for (const nodeId of previousNodeById.keys()) {
48
+ if (!nextNodeById.has(nodeId)) {
49
+ removedNodeIds.push(nodeId);
50
+ }
51
+ }
52
+
53
+ const previousEdgeByKey = new Map(previousEdges.map((edge) => [toEdgeKey(edge), edge]));
54
+ const nextEdgeByKey = new Map(nextEdges.map((edge) => [toEdgeKey(edge), edge]));
55
+ const addedEdges = [];
56
+ const removedEdges = [];
57
+
58
+ for (const [edgeKey, edge] of nextEdgeByKey.entries()) {
59
+ if (!previousEdgeByKey.has(edgeKey)) {
60
+ addedEdges.push(edge);
61
+ }
62
+ }
63
+
64
+ for (const [edgeKey, edge] of previousEdgeByKey.entries()) {
65
+ if (!nextEdgeByKey.has(edgeKey)) {
66
+ removedEdges.push(edge);
67
+ }
68
+ }
69
+
70
+ const touchedNodeIds = new Set();
71
+ for (const node of addedNodes) {
72
+ touchedNodeIds.add(node.id);
73
+ }
74
+ for (const node of updatedNodes) {
75
+ touchedNodeIds.add(node.id);
76
+ }
77
+ for (const nodeId of removedNodeIds) {
78
+ touchedNodeIds.add(nodeId);
79
+ }
80
+ for (const edge of addedEdges) {
81
+ touchedNodeIds.add(edge.source);
82
+ touchedNodeIds.add(edge.target);
83
+ }
84
+ for (const edge of removedEdges) {
85
+ touchedNodeIds.add(edge.source);
86
+ touchedNodeIds.add(edge.target);
87
+ }
88
+
89
+ return {
90
+ addedNodes,
91
+ updatedNodes,
92
+ removedNodeIds,
93
+ addedEdges,
94
+ removedEdges,
95
+ changedNodeIds: Array.from(touchedNodeIds).sort((a, b) => a.localeCompare(b)),
96
+ stats: nextGraph?.stats ?? null,
97
+ hasChanges:
98
+ addedNodes.length > 0 ||
99
+ updatedNodes.length > 0 ||
100
+ removedNodeIds.length > 0 ||
101
+ addedEdges.length > 0 ||
102
+ removedEdges.length > 0
103
+ };
104
+ }
@@ -1,75 +1,75 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { diffGraphs } from './graph-diff.js';
3
-
4
- describe('diffGraphs', () => {
5
- it('detects node and edge additions, updates, and removals', () => {
6
- const previous = {
7
- nodes: [
8
- { id: 'a', title: 'A', category: 'root', tags: [], path: 'a.md', missing: false, degree: 1 },
9
- { id: 'b', title: 'B', category: 'root', tags: ['x'], path: 'b.md', missing: false, degree: 1 },
10
- { id: 'c', title: 'C', category: 'root', tags: [], path: null, missing: true, degree: 0 }
11
- ],
12
- edges: [{ source: 'a', target: 'b' }],
13
- stats: { nodeCount: 3, edgeCount: 1 }
14
- };
15
-
16
- const next = {
17
- nodes: [
18
- { id: 'a', title: 'A Updated', category: 'root', tags: [], path: 'a.md', missing: false, degree: 1 },
19
- { id: 'b', title: 'B', category: 'root', tags: ['x'], path: 'b.md', missing: false, degree: 2 },
20
- { id: 'd', title: 'D', category: 'projects', tags: [], path: 'd.md', missing: false, degree: 1 }
21
- ],
22
- edges: [
23
- { source: 'a', target: 'b' },
24
- { source: 'b', target: 'd' }
25
- ],
26
- stats: { nodeCount: 3, edgeCount: 2 }
27
- };
28
-
29
- const patch = diffGraphs(previous, next);
30
-
31
- expect(patch.addedNodes).toEqual([next.nodes[2]]);
32
- expect(patch.updatedNodes).toEqual(expect.arrayContaining([next.nodes[0], next.nodes[1]]));
33
- expect(patch.removedNodeIds).toEqual(['c']);
34
- expect(patch.addedEdges).toEqual([{ source: 'b', target: 'd' }]);
35
- expect(patch.removedEdges).toEqual([]);
36
- expect(patch.changedNodeIds).toEqual(['a', 'b', 'c', 'd']);
37
- expect(patch.hasChanges).toBe(true);
38
- });
39
-
40
- it('returns hasChanges=false for equivalent graphs', () => {
41
- const graph = {
42
- nodes: [{ id: 'a', title: 'A', category: 'root', tags: ['t'], path: 'a.md', missing: false, degree: 0 }],
43
- edges: [],
44
- stats: { nodeCount: 1, edgeCount: 0 }
45
- };
46
-
47
- const patch = diffGraphs(graph, structuredClone(graph));
48
-
49
- expect(patch.hasChanges).toBe(false);
50
- expect(patch.addedNodes).toEqual([]);
51
- expect(patch.updatedNodes).toEqual([]);
52
- expect(patch.removedNodeIds).toEqual([]);
53
- expect(patch.addedEdges).toEqual([]);
54
- expect(patch.removedEdges).toEqual([]);
55
- });
56
-
57
- it('treats edge type changes as edge diff', () => {
58
- const previous = {
59
- nodes: [
60
- { id: 'a', title: 'A', category: 'root', tags: [], path: 'a.md', missing: false, degree: 1 },
61
- { id: 'b', title: 'B', category: 'root', tags: [], path: 'b.md', missing: false, degree: 1 }
62
- ],
63
- edges: [{ source: 'a', target: 'b', type: 'wiki_link' }]
64
- };
65
- const next = {
66
- nodes: previous.nodes,
67
- edges: [{ source: 'a', target: 'b', type: 'frontmatter_relation', label: 'related' }]
68
- };
69
-
70
- const patch = diffGraphs(previous, next);
71
- expect(patch.addedEdges).toEqual([{ source: 'a', target: 'b', type: 'frontmatter_relation', label: 'related' }]);
72
- expect(patch.removedEdges).toEqual([{ source: 'a', target: 'b', type: 'wiki_link' }]);
73
- expect(patch.hasChanges).toBe(true);
74
- });
75
- });
1
+ import { describe, expect, it } from 'vitest';
2
+ import { diffGraphs } from './graph-diff.js';
3
+
4
+ describe('diffGraphs', () => {
5
+ it('detects node and edge additions, updates, and removals', () => {
6
+ const previous = {
7
+ nodes: [
8
+ { id: 'a', title: 'A', category: 'root', tags: [], path: 'a.md', missing: false, degree: 1 },
9
+ { id: 'b', title: 'B', category: 'root', tags: ['x'], path: 'b.md', missing: false, degree: 1 },
10
+ { id: 'c', title: 'C', category: 'root', tags: [], path: null, missing: true, degree: 0 }
11
+ ],
12
+ edges: [{ source: 'a', target: 'b' }],
13
+ stats: { nodeCount: 3, edgeCount: 1 }
14
+ };
15
+
16
+ const next = {
17
+ nodes: [
18
+ { id: 'a', title: 'A Updated', category: 'root', tags: [], path: 'a.md', missing: false, degree: 1 },
19
+ { id: 'b', title: 'B', category: 'root', tags: ['x'], path: 'b.md', missing: false, degree: 2 },
20
+ { id: 'd', title: 'D', category: 'projects', tags: [], path: 'd.md', missing: false, degree: 1 }
21
+ ],
22
+ edges: [
23
+ { source: 'a', target: 'b' },
24
+ { source: 'b', target: 'd' }
25
+ ],
26
+ stats: { nodeCount: 3, edgeCount: 2 }
27
+ };
28
+
29
+ const patch = diffGraphs(previous, next);
30
+
31
+ expect(patch.addedNodes).toEqual([next.nodes[2]]);
32
+ expect(patch.updatedNodes).toEqual(expect.arrayContaining([next.nodes[0], next.nodes[1]]));
33
+ expect(patch.removedNodeIds).toEqual(['c']);
34
+ expect(patch.addedEdges).toEqual([{ source: 'b', target: 'd' }]);
35
+ expect(patch.removedEdges).toEqual([]);
36
+ expect(patch.changedNodeIds).toEqual(['a', 'b', 'c', 'd']);
37
+ expect(patch.hasChanges).toBe(true);
38
+ });
39
+
40
+ it('returns hasChanges=false for equivalent graphs', () => {
41
+ const graph = {
42
+ nodes: [{ id: 'a', title: 'A', category: 'root', tags: ['t'], path: 'a.md', missing: false, degree: 0 }],
43
+ edges: [],
44
+ stats: { nodeCount: 1, edgeCount: 0 }
45
+ };
46
+
47
+ const patch = diffGraphs(graph, structuredClone(graph));
48
+
49
+ expect(patch.hasChanges).toBe(false);
50
+ expect(patch.addedNodes).toEqual([]);
51
+ expect(patch.updatedNodes).toEqual([]);
52
+ expect(patch.removedNodeIds).toEqual([]);
53
+ expect(patch.addedEdges).toEqual([]);
54
+ expect(patch.removedEdges).toEqual([]);
55
+ });
56
+
57
+ it('treats edge type changes as edge diff', () => {
58
+ const previous = {
59
+ nodes: [
60
+ { id: 'a', title: 'A', category: 'root', tags: [], path: 'a.md', missing: false, degree: 1 },
61
+ { id: 'b', title: 'B', category: 'root', tags: [], path: 'b.md', missing: false, degree: 1 }
62
+ ],
63
+ edges: [{ source: 'a', target: 'b', type: 'wiki_link' }]
64
+ };
65
+ const next = {
66
+ nodes: previous.nodes,
67
+ edges: [{ source: 'a', target: 'b', type: 'frontmatter_relation', label: 'related' }]
68
+ };
69
+
70
+ const patch = diffGraphs(previous, next);
71
+ expect(patch.addedEdges).toEqual([{ source: 'a', target: 'b', type: 'frontmatter_relation', label: 'related' }]);
72
+ expect(patch.removedEdges).toEqual([{ source: 'a', target: 'b', type: 'wiki_link' }]);
73
+ expect(patch.hasChanges).toBe(true);
74
+ });
75
+ });