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.
- package/README.md +159 -200
- package/bin/clawvault.js +111 -111
- package/bin/command-registration.test.js +166 -166
- package/bin/command-runtime.js +93 -93
- package/bin/command-runtime.test.js +154 -154
- package/bin/help-contract.test.js +39 -39
- package/bin/register-config-commands.js +153 -153
- package/bin/register-config-route-commands.test.js +121 -121
- package/bin/register-core-commands.js +237 -237
- package/bin/register-kanban-commands.js +56 -56
- package/bin/register-kanban-commands.test.js +83 -83
- package/bin/register-maintenance-commands.js +282 -282
- package/bin/register-project-commands.js +209 -209
- package/bin/register-project-commands.test.js +206 -206
- package/bin/register-query-commands.js +317 -317
- package/bin/register-query-commands.test.js +65 -65
- package/bin/register-resilience-commands.js +182 -182
- package/bin/register-resilience-commands.test.js +81 -81
- package/bin/register-route-commands.js +114 -114
- package/bin/register-session-lifecycle-commands.js +206 -206
- package/bin/register-tailscale-commands.js +106 -106
- package/bin/register-task-commands.js +348 -348
- package/bin/register-task-commands.test.js +69 -69
- package/bin/register-template-commands.js +72 -72
- package/bin/register-vault-operations-commands.js +300 -300
- package/bin/test-helpers/cli-command-fixtures.js +119 -119
- package/dashboard/lib/graph-diff.js +104 -104
- package/dashboard/lib/graph-diff.test.js +75 -75
- package/dashboard/lib/vault-parser.js +556 -556
- package/dashboard/lib/vault-parser.test.js +254 -254
- package/dashboard/public/app.js +796 -796
- package/dashboard/public/index.html +52 -52
- package/dashboard/public/styles.css +221 -221
- package/dashboard/server.js +374 -374
- package/dist/{chunk-3FP5BJ42.js → chunk-4QYGFWRM.js} +1 -1
- package/dist/{chunk-M25QVSJM.js → chunk-AXKYDCNN.js} +1 -1
- package/dist/{chunk-CLE2HHNT.js → chunk-IVRIKYFE.js} +18 -11
- package/dist/{chunk-HRTPQQF2.js → chunk-IZEY5S74.js} +1 -1
- package/dist/{chunk-HWUNREDJ.js → chunk-JDLOL2PL.js} +4 -4
- package/dist/{chunk-AY4PGUVL.js → chunk-KL4NAOMO.js} +1 -1
- package/dist/{chunk-O7XHXF7F.js → chunk-MAKNAHAW.js} +4 -4
- package/dist/{chunk-PLZKZW4I.js → chunk-OSMS7QIG.js} +1 -1
- package/dist/{chunk-NZ4ZZNSR.js → chunk-THRJVD4L.js} +1 -1
- package/dist/{chunk-4GBPTBFJ.js → chunk-TIGW564L.js} +1 -1
- package/dist/{chunk-BHO7WSAY.js → chunk-W2HNZC22.js} +3 -3
- package/dist/{chunk-GFJ3LIIB.js → chunk-XAVB4GB4.js} +1 -1
- package/dist/cli/index.js +10 -10
- package/dist/commands/context.js +3 -3
- package/dist/commands/doctor.js +4 -4
- package/dist/commands/embed.js +2 -2
- package/dist/commands/observe.js +2 -2
- package/dist/commands/setup.js +2 -2
- package/dist/commands/sleep.js +2 -2
- package/dist/commands/status.js +3 -3
- package/dist/commands/tailscale.js +3 -3
- package/dist/commands/wake.js +2 -2
- package/dist/index.js +12 -12
- package/dist/lib/tailscale.js +2 -2
- package/dist/lib/webdav.js +1 -1
- package/hooks/clawvault/HOOK.md +83 -74
- package/hooks/clawvault/handler.js +816 -816
- package/hooks/clawvault/handler.test.js +263 -263
- package/package.json +94 -125
- package/templates/checkpoint.md +19 -19
- package/templates/daily-note.md +19 -19
- package/templates/daily.md +19 -19
- package/templates/decision.md +17 -17
- package/templates/handoff.md +19 -19
- package/templates/lesson.md +16 -16
- package/templates/person.md +19 -19
- 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
|
+
});
|