clawvault 1.11.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -1
- package/bin/clawvault.js +51 -1252
- package/bin/command-registration.test.js +148 -0
- package/bin/command-runtime.js +42 -0
- package/bin/command-runtime.test.js +102 -0
- package/bin/help-contract.test.js +23 -0
- package/bin/register-core-commands.js +139 -0
- package/bin/register-maintenance-commands.js +137 -0
- package/bin/register-query-commands.js +225 -0
- package/bin/register-resilience-commands.js +147 -0
- package/bin/register-session-lifecycle-commands.js +204 -0
- package/bin/register-template-commands.js +72 -0
- package/bin/register-vault-operations-commands.js +295 -0
- package/bin/test-helpers/cli-command-fixtures.js +94 -0
- package/dashboard/lib/graph-diff.js +3 -1
- package/dashboard/lib/graph-diff.test.js +19 -0
- package/dashboard/lib/vault-parser.js +330 -26
- package/dashboard/lib/vault-parser.test.js +191 -11
- package/dashboard/public/app.js +22 -9
- package/dist/chunk-MXSSG3QU.js +42 -0
- package/dist/chunk-O5V7SD5C.js +398 -0
- package/dist/chunk-PAYUH64O.js +284 -0
- package/dist/{chunk-3HFB7EMU.js → chunk-QFBKWDYR.js} +12 -0
- package/dist/{chunk-UBRYOIII.js → chunk-TBVI4N53.js} +210 -21
- package/dist/chunk-TXO34J3O.js +56 -0
- package/dist/commands/compat.d.ts +28 -0
- package/dist/commands/compat.js +10 -0
- package/dist/commands/context.d.ts +2 -33
- package/dist/commands/context.js +3 -2
- package/dist/commands/doctor.js +61 -3
- package/dist/commands/entities.d.ts +1 -0
- package/dist/commands/entities.js +4 -4
- package/dist/commands/graph.d.ts +21 -0
- package/dist/commands/graph.js +10 -0
- package/dist/commands/link.d.ts +1 -0
- package/dist/commands/link.js +14 -5
- package/dist/commands/sleep.js +7 -6
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +63 -3
- package/dist/commands/wake.js +5 -4
- package/dist/context-COo8oq1k.d.ts +45 -0
- package/dist/index.d.ts +63 -2
- package/dist/index.js +53 -15
- package/dist/lib/config.d.ts +6 -1
- package/dist/lib/config.js +7 -3
- package/hooks/clawvault/HOOK.md +6 -1
- package/hooks/clawvault/handler.js +44 -3
- package/hooks/clawvault/handler.test.js +161 -0
- package/package.json +34 -2
- package/dashboard/public/graph.js +0 -376
- package/dashboard/public/style.css +0 -154
- package/dist/chunk-4KDZZW4X.js +0 -13
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault operation command registrations (browse/sync/reindex/remember/shell-init/dashboard).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function registerVaultOperationsCommands(
|
|
6
|
+
program,
|
|
7
|
+
{
|
|
8
|
+
chalk,
|
|
9
|
+
fs,
|
|
10
|
+
getVault,
|
|
11
|
+
runQmd,
|
|
12
|
+
resolveVaultPath,
|
|
13
|
+
path
|
|
14
|
+
}
|
|
15
|
+
) {
|
|
16
|
+
// === LIST ===
|
|
17
|
+
program
|
|
18
|
+
.command('list [category]')
|
|
19
|
+
.description('List documents')
|
|
20
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
21
|
+
.option('--json', 'Output as JSON')
|
|
22
|
+
.action(async (category, options) => {
|
|
23
|
+
try {
|
|
24
|
+
const vault = await getVault(options.vault);
|
|
25
|
+
const docs = await vault.list(category);
|
|
26
|
+
|
|
27
|
+
if (options.json) {
|
|
28
|
+
console.log(JSON.stringify(docs.map((doc) => ({
|
|
29
|
+
id: doc.id,
|
|
30
|
+
title: doc.title,
|
|
31
|
+
category: doc.category,
|
|
32
|
+
tags: doc.tags,
|
|
33
|
+
modified: doc.modified
|
|
34
|
+
})), null, 2));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (docs.length === 0) {
|
|
39
|
+
console.log(chalk.yellow('No documents found.'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(chalk.cyan(`\n📚 ${docs.length} document(s)${category ? ` in ${category}` : ''}:\n`));
|
|
44
|
+
|
|
45
|
+
const grouped = {};
|
|
46
|
+
for (const doc of docs) {
|
|
47
|
+
grouped[doc.category] = grouped[doc.category] || [];
|
|
48
|
+
grouped[doc.category].push(doc);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const [cat, catDocs] of Object.entries(grouped)) {
|
|
52
|
+
console.log(chalk.yellow(`${cat}/`));
|
|
53
|
+
for (const doc of catDocs) {
|
|
54
|
+
console.log(chalk.dim(` - ${doc.title}`));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
console.log();
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// === GET ===
|
|
65
|
+
program
|
|
66
|
+
.command('get <id>')
|
|
67
|
+
.description('Get a document by ID')
|
|
68
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
69
|
+
.option('--json', 'Output as JSON')
|
|
70
|
+
.action(async (id, options) => {
|
|
71
|
+
try {
|
|
72
|
+
const vault = await getVault(options.vault);
|
|
73
|
+
const doc = await vault.get(id);
|
|
74
|
+
|
|
75
|
+
if (!doc) {
|
|
76
|
+
console.error(chalk.red(`Document not found: ${id}`));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (options.json) {
|
|
81
|
+
console.log(JSON.stringify(doc, null, 2));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(chalk.cyan(`\n📄 ${doc.title}\n`));
|
|
86
|
+
console.log(chalk.dim(`Category: ${doc.category}`));
|
|
87
|
+
console.log(chalk.dim(`Path: ${doc.path}`));
|
|
88
|
+
console.log(chalk.dim(`Tags: ${doc.tags.join(', ') || 'none'}`));
|
|
89
|
+
console.log(chalk.dim(`Links: ${doc.links.join(', ') || 'none'}`));
|
|
90
|
+
console.log(chalk.dim(`Modified: ${doc.modified.toISOString()}`));
|
|
91
|
+
console.log(chalk.dim('---'));
|
|
92
|
+
console.log(doc.content);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// === STATS ===
|
|
100
|
+
program
|
|
101
|
+
.command('stats')
|
|
102
|
+
.description('Show vault statistics')
|
|
103
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
104
|
+
.option('--json', 'Output as JSON')
|
|
105
|
+
.action(async (options) => {
|
|
106
|
+
try {
|
|
107
|
+
const vault = await getVault(options.vault);
|
|
108
|
+
const stats = await vault.stats();
|
|
109
|
+
|
|
110
|
+
if (options.json) {
|
|
111
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(chalk.cyan(`\n🐘 ${vault.getName()} Stats\n`));
|
|
116
|
+
console.log(chalk.dim(`Path: ${vault.getPath()}`));
|
|
117
|
+
console.log(`Documents: ${chalk.green(stats.documents)}`);
|
|
118
|
+
console.log(`Links: ${chalk.blue(stats.links)}`);
|
|
119
|
+
console.log(`Tags: ${chalk.yellow(stats.tags.length)}`);
|
|
120
|
+
console.log();
|
|
121
|
+
console.log(chalk.dim('By category:'));
|
|
122
|
+
for (const [cat, count] of Object.entries(stats.categories)) {
|
|
123
|
+
console.log(chalk.dim(` ${cat}: ${count}`));
|
|
124
|
+
}
|
|
125
|
+
console.log();
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// === SYNC ===
|
|
133
|
+
program
|
|
134
|
+
.command('sync <target>')
|
|
135
|
+
.description('Sync vault files to target path')
|
|
136
|
+
.option('--delete', 'Delete orphan files in target')
|
|
137
|
+
.option('--dry-run', 'Show what would be synced without syncing')
|
|
138
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
139
|
+
.action(async (target, options) => {
|
|
140
|
+
try {
|
|
141
|
+
const vault = await getVault(options.vault);
|
|
142
|
+
console.log(chalk.cyan(`\n🔄 Syncing to ${target}...\n`));
|
|
143
|
+
|
|
144
|
+
const result = await vault.sync({
|
|
145
|
+
target,
|
|
146
|
+
deleteOrphans: options.delete,
|
|
147
|
+
dryRun: options.dryRun
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (options.dryRun) {
|
|
151
|
+
console.log(chalk.yellow('DRY RUN - no changes made\n'));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (result.copied.length > 0) {
|
|
155
|
+
console.log(chalk.green(`Copied: ${result.copied.length} files`));
|
|
156
|
+
for (const filePath of result.copied.slice(0, 5)) {
|
|
157
|
+
console.log(chalk.dim(` + ${filePath}`));
|
|
158
|
+
}
|
|
159
|
+
if (result.copied.length > 5) {
|
|
160
|
+
console.log(chalk.dim(` ... and ${result.copied.length - 5} more`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (result.deleted.length > 0) {
|
|
165
|
+
console.log(chalk.red(`Deleted: ${result.deleted.length} files`));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (result.unchanged.length > 0) {
|
|
169
|
+
console.log(chalk.dim(`Unchanged: ${result.unchanged.length} files`));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (result.errors.length > 0) {
|
|
173
|
+
console.log(chalk.red('\nErrors:'));
|
|
174
|
+
for (const error of result.errors) {
|
|
175
|
+
console.log(chalk.red(` ${error}`));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log();
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// === REINDEX ===
|
|
187
|
+
program
|
|
188
|
+
.command('reindex')
|
|
189
|
+
.description('Rebuild the search index')
|
|
190
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
191
|
+
.option('--qmd', 'Also update qmd embeddings')
|
|
192
|
+
.action(async (options) => {
|
|
193
|
+
try {
|
|
194
|
+
const vault = await getVault(options.vault);
|
|
195
|
+
console.log(chalk.cyan('\n🔄 Reindexing...\n'));
|
|
196
|
+
|
|
197
|
+
const count = await vault.reindex();
|
|
198
|
+
console.log(chalk.green(`✓ Indexed ${count} documents`));
|
|
199
|
+
|
|
200
|
+
if (options.qmd) {
|
|
201
|
+
console.log(chalk.cyan('Updating qmd embeddings...'));
|
|
202
|
+
const collection = vault.getQmdCollection();
|
|
203
|
+
await runQmd(collection ? ['update', '-c', collection] : ['update']);
|
|
204
|
+
console.log(chalk.green('✓ qmd updated'));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log();
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// === REMEMBER ===
|
|
215
|
+
program
|
|
216
|
+
.command('remember <type> <title>')
|
|
217
|
+
.description('Store a memory with type classification (fact|feeling|decision|lesson|commitment|preference|relationship|project)')
|
|
218
|
+
.option('--content <content>', 'Content body')
|
|
219
|
+
.option('-f, --file <file>', 'Read content from file')
|
|
220
|
+
.option('--stdin', 'Read content from stdin')
|
|
221
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
222
|
+
.option('--no-index', 'Skip qmd index update')
|
|
223
|
+
.action(async (type, title, options) => {
|
|
224
|
+
const validTypes = ['fact', 'feeling', 'decision', 'lesson', 'commitment', 'preference', 'relationship', 'project'];
|
|
225
|
+
if (!validTypes.includes(type)) {
|
|
226
|
+
console.error(chalk.red(`Invalid type: ${type}`));
|
|
227
|
+
console.error(chalk.dim(`Valid types: ${validTypes.join(', ')}`));
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const vault = await getVault(options.vault);
|
|
233
|
+
let content = options.content || '';
|
|
234
|
+
if (options.file) {
|
|
235
|
+
content = fs.readFileSync(options.file, 'utf-8');
|
|
236
|
+
} else if (options.stdin) {
|
|
237
|
+
content = fs.readFileSync(0, 'utf-8');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const doc = await vault.remember(type, title, content);
|
|
241
|
+
console.log(chalk.green(`✓ Remembered (${type}): ${doc.id}`));
|
|
242
|
+
|
|
243
|
+
if (options.index !== false) {
|
|
244
|
+
const collection = vault.getQmdCollection();
|
|
245
|
+
await runQmd(collection ? ['update', '-c', collection] : ['update']);
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// === SHELL INIT ===
|
|
254
|
+
program
|
|
255
|
+
.command('shell-init')
|
|
256
|
+
.description('Output shell integration for ClawVault')
|
|
257
|
+
.action(async () => {
|
|
258
|
+
try {
|
|
259
|
+
const { shellInit } = await import('../dist/commands/shell-init.js');
|
|
260
|
+
console.log(shellInit());
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// === DASHBOARD ===
|
|
268
|
+
program
|
|
269
|
+
.command('dashboard')
|
|
270
|
+
.description('Run local vault graph dashboard')
|
|
271
|
+
.option('-p, --port <port>', 'Dashboard port', '3377')
|
|
272
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
273
|
+
.action(async (options) => {
|
|
274
|
+
try {
|
|
275
|
+
const parsedPort = Number.parseInt(options.port, 10);
|
|
276
|
+
if (Number.isNaN(parsedPort)) {
|
|
277
|
+
console.error(chalk.red(`Error: Invalid port: ${options.port}`));
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const vaultPath = options.vault
|
|
282
|
+
? path.resolve(options.vault)
|
|
283
|
+
: resolveVaultPath(undefined);
|
|
284
|
+
|
|
285
|
+
const { startDashboard } = await import('../dashboard/server.js');
|
|
286
|
+
await startDashboard({
|
|
287
|
+
port: parsedPort,
|
|
288
|
+
vaultPath
|
|
289
|
+
});
|
|
290
|
+
} catch (err) {
|
|
291
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
|
|
12
|
+
export const chalkStub = {
|
|
13
|
+
cyan: (value) => value,
|
|
14
|
+
green: (value) => value,
|
|
15
|
+
red: (value) => value,
|
|
16
|
+
dim: (value) => value,
|
|
17
|
+
yellow: (value) => value,
|
|
18
|
+
white: (value) => value
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function stubResolveVaultPath(value) {
|
|
22
|
+
return value ?? '/vault';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createVaultStub(overrides = {}) {
|
|
26
|
+
return {
|
|
27
|
+
store: async () => ({}),
|
|
28
|
+
capture: async () => ({}),
|
|
29
|
+
find: async () => [],
|
|
30
|
+
vsearch: async () => [],
|
|
31
|
+
list: async () => [],
|
|
32
|
+
get: async () => null,
|
|
33
|
+
stats: async () => ({ tags: [], categories: {} }),
|
|
34
|
+
sync: async () => ({ copied: [], deleted: [], unchanged: [], errors: [] }),
|
|
35
|
+
reindex: async () => 0,
|
|
36
|
+
remember: async () => ({ id: '' }),
|
|
37
|
+
getQmdCollection: () => '',
|
|
38
|
+
createHandoff: async () => ({ id: '', path: '' }),
|
|
39
|
+
generateRecap: async () => ({}),
|
|
40
|
+
formatRecap: () => '',
|
|
41
|
+
...overrides
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createGetVaultStub(overrides = {}) {
|
|
46
|
+
return async () => createVaultStub(overrides);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function registerAllCommandModules(program = new Command()) {
|
|
50
|
+
const getVault = createGetVaultStub();
|
|
51
|
+
|
|
52
|
+
registerCoreCommands(program, {
|
|
53
|
+
chalk: chalkStub,
|
|
54
|
+
path,
|
|
55
|
+
fs,
|
|
56
|
+
createVault: async () => ({ getCategories: () => [], getQmdRoot: () => '', getQmdCollection: () => '' }),
|
|
57
|
+
getVault,
|
|
58
|
+
runQmd: async () => {}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
registerQueryCommands(program, {
|
|
62
|
+
chalk: chalkStub,
|
|
63
|
+
getVault,
|
|
64
|
+
resolveVaultPath: stubResolveVaultPath,
|
|
65
|
+
QmdUnavailableError: class extends Error {},
|
|
66
|
+
printQmdMissing: () => {}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
registerVaultOperationsCommands(program, {
|
|
70
|
+
chalk: chalkStub,
|
|
71
|
+
fs,
|
|
72
|
+
getVault,
|
|
73
|
+
runQmd: async () => {},
|
|
74
|
+
resolveVaultPath: stubResolveVaultPath,
|
|
75
|
+
path
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
registerMaintenanceCommands(program, { chalk: chalkStub });
|
|
79
|
+
registerResilienceCommands(program, {
|
|
80
|
+
chalk: chalkStub,
|
|
81
|
+
resolveVaultPath: stubResolveVaultPath
|
|
82
|
+
});
|
|
83
|
+
registerSessionLifecycleCommands(program, {
|
|
84
|
+
chalk: chalkStub,
|
|
85
|
+
resolveVaultPath: stubResolveVaultPath,
|
|
86
|
+
QmdUnavailableError: class extends Error {},
|
|
87
|
+
printQmdMissing: () => {},
|
|
88
|
+
getVault,
|
|
89
|
+
runQmd: async () => {}
|
|
90
|
+
});
|
|
91
|
+
registerTemplateCommands(program, { chalk: chalkStub });
|
|
92
|
+
|
|
93
|
+
return program;
|
|
94
|
+
}
|
|
@@ -10,7 +10,9 @@ function toNodeSignature(node) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function toEdgeKey(edge) {
|
|
13
|
-
|
|
13
|
+
const type = edge.type ?? '';
|
|
14
|
+
const label = edge.label ?? '';
|
|
15
|
+
return `${edge.source}=>${edge.target}:${type}:${label}`;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -53,4 +53,23 @@ describe('diffGraphs', () => {
|
|
|
53
53
|
expect(patch.addedEdges).toEqual([]);
|
|
54
54
|
expect(patch.removedEdges).toEqual([]);
|
|
55
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
|
+
});
|
|
56
75
|
});
|