clawvault 3.4.1 → 3.5.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/CHANGELOG.md +543 -0
- package/LICENSE +21 -0
- package/SKILL.md +369 -0
- package/dist/{chunk-X3SPPUFG.js → chunk-JI7VUQV7.js} +118 -132
- package/dist/{chunk-PLNK37JD.js → chunk-QUFQBAHP.js} +114 -217
- package/dist/cli/index.js +1 -1
- package/dist/commands/compat.js +1 -1
- package/dist/commands/observe.js +1 -1
- package/dist/commands/status.js +4 -4
- package/dist/index.js +11 -8
- package/dist/openclaw-plugin.js +6 -1
- package/docs/clawhub-security-release-playbook.md +75 -0
- package/docs/getting-started/installation.md +99 -0
- package/docs/openclaw-plugin-usage.md +152 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +26 -8
- package/bin/command-registration.test.js +0 -179
- package/bin/command-runtime.test.js +0 -154
- package/bin/help-contract.test.js +0 -55
- package/bin/register-config-route-commands.test.js +0 -121
- package/bin/register-core-commands.test.js +0 -80
- package/bin/register-kanban-commands.test.js +0 -83
- package/bin/register-project-commands.test.js +0 -206
- package/bin/register-query-commands.test.js +0 -80
- package/bin/register-resilience-commands.test.js +0 -81
- package/bin/register-task-commands.test.js +0 -69
- package/bin/register-template-commands.test.js +0 -87
- package/bin/test-helpers/cli-command-fixtures.js +0 -120
- package/dashboard/lib/graph-diff.test.js +0 -75
- package/dashboard/lib/vault-parser.test.js +0 -254
- package/hooks/clawvault/HOOK.md +0 -130
- package/hooks/clawvault/handler.js +0 -1696
- package/hooks/clawvault/handler.test.js +0 -576
- package/hooks/clawvault/integrity.js +0 -112
- package/hooks/clawvault/integrity.test.js +0 -32
- package/hooks/clawvault/openclaw.plugin.json +0 -190
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import { registerResilienceCommands } from './register-resilience-commands.js';
|
|
4
|
-
import { chalkStub } from './test-helpers/cli-command-fixtures.js';
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
recoverMock,
|
|
8
|
-
formatRecoveryInfoMock,
|
|
9
|
-
checkRecoveryStatusMock,
|
|
10
|
-
formatRecoveryCheckStatusMock,
|
|
11
|
-
listCheckpointsMock,
|
|
12
|
-
formatCheckpointListMock
|
|
13
|
-
} = vi.hoisted(() => ({
|
|
14
|
-
recoverMock: vi.fn(),
|
|
15
|
-
formatRecoveryInfoMock: vi.fn(),
|
|
16
|
-
checkRecoveryStatusMock: vi.fn(),
|
|
17
|
-
formatRecoveryCheckStatusMock: vi.fn(),
|
|
18
|
-
listCheckpointsMock: vi.fn(),
|
|
19
|
-
formatCheckpointListMock: vi.fn()
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
vi.mock('../dist/commands/recover.js', () => ({
|
|
23
|
-
recover: recoverMock,
|
|
24
|
-
formatRecoveryInfo: formatRecoveryInfoMock,
|
|
25
|
-
checkRecoveryStatus: checkRecoveryStatusMock,
|
|
26
|
-
formatRecoveryCheckStatus: formatRecoveryCheckStatusMock,
|
|
27
|
-
listCheckpoints: listCheckpointsMock,
|
|
28
|
-
formatCheckpointList: formatCheckpointListMock
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
function buildProgram() {
|
|
32
|
-
const program = new Command();
|
|
33
|
-
registerResilienceCommands(program, {
|
|
34
|
-
chalk: chalkStub,
|
|
35
|
-
resolveVaultPath: (value) => value ?? '/vault'
|
|
36
|
-
});
|
|
37
|
-
return program;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function runCommand(args) {
|
|
41
|
-
const program = buildProgram();
|
|
42
|
-
await program.parseAsync(args, { from: 'user' });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
vi.clearAllMocks();
|
|
47
|
-
recoverMock.mockResolvedValue({ died: false });
|
|
48
|
-
formatRecoveryInfoMock.mockReturnValue('recover');
|
|
49
|
-
checkRecoveryStatusMock.mockResolvedValue({ died: false, deathTime: null, checkpoint: null });
|
|
50
|
-
formatRecoveryCheckStatusMock.mockReturnValue('check');
|
|
51
|
-
listCheckpointsMock.mockReturnValue([]);
|
|
52
|
-
formatCheckpointListMock.mockReturnValue('list');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe('register-resilience-commands', () => {
|
|
56
|
-
it('routes recover --check through check helpers', async () => {
|
|
57
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
58
|
-
try {
|
|
59
|
-
await runCommand(['recover', '--check']);
|
|
60
|
-
expect(checkRecoveryStatusMock).toHaveBeenCalledWith('/vault');
|
|
61
|
-
expect(formatRecoveryCheckStatusMock).toHaveBeenCalled();
|
|
62
|
-
expect(recoverMock).not.toHaveBeenCalled();
|
|
63
|
-
expect(logSpy).toHaveBeenCalledWith('check');
|
|
64
|
-
} finally {
|
|
65
|
-
logSpy.mockRestore();
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('routes recover --list through checkpoint listing helpers', async () => {
|
|
70
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
71
|
-
try {
|
|
72
|
-
await runCommand(['recover', '--list']);
|
|
73
|
-
expect(listCheckpointsMock).toHaveBeenCalledWith('/vault');
|
|
74
|
-
expect(formatCheckpointListMock).toHaveBeenCalled();
|
|
75
|
-
expect(recoverMock).not.toHaveBeenCalled();
|
|
76
|
-
expect(logSpy).toHaveBeenCalledWith('list');
|
|
77
|
-
} finally {
|
|
78
|
-
logSpy.mockRestore();
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import { registerTaskCommands } from './register-task-commands.js';
|
|
4
|
-
import { chalkStub, stubResolveVaultPath } from './test-helpers/cli-command-fixtures.js';
|
|
5
|
-
|
|
6
|
-
describe('register-task-commands', () => {
|
|
7
|
-
it('adds enriched task add/list/update options', () => {
|
|
8
|
-
const program = new Command();
|
|
9
|
-
registerTaskCommands(program, {
|
|
10
|
-
chalk: chalkStub,
|
|
11
|
-
resolveVaultPath: stubResolveVaultPath
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const taskCommand = program.commands.find((command) => command.name() === 'task');
|
|
15
|
-
expect(taskCommand).toBeDefined();
|
|
16
|
-
|
|
17
|
-
const addCommand = taskCommand?.commands.find((command) => command.name() === 'add');
|
|
18
|
-
const addFlags = addCommand?.options.map((option) => option.flags) ?? [];
|
|
19
|
-
expect(addFlags).toEqual(expect.arrayContaining([
|
|
20
|
-
'--due <date>',
|
|
21
|
-
'--tags <tags>',
|
|
22
|
-
'--description <description>',
|
|
23
|
-
'--estimate <estimate>',
|
|
24
|
-
'--parent <slug>',
|
|
25
|
-
'--depends-on <slugs>'
|
|
26
|
-
]));
|
|
27
|
-
|
|
28
|
-
const listCommand = taskCommand?.commands.find((command) => command.name() === 'list');
|
|
29
|
-
const listFlags = listCommand?.options.map((option) => option.flags) ?? [];
|
|
30
|
-
expect(listFlags).toEqual(expect.arrayContaining([
|
|
31
|
-
'--due',
|
|
32
|
-
'--tag <tag>',
|
|
33
|
-
'--overdue'
|
|
34
|
-
]));
|
|
35
|
-
|
|
36
|
-
const updateCommand = taskCommand?.commands.find((command) => command.name() === 'update');
|
|
37
|
-
const updateFlags = updateCommand?.options.map((option) => option.flags) ?? [];
|
|
38
|
-
expect(updateFlags).toEqual(expect.arrayContaining([
|
|
39
|
-
'--tags <tags>',
|
|
40
|
-
'--description <description>',
|
|
41
|
-
'--estimate <estimate>',
|
|
42
|
-
'--parent <slug>',
|
|
43
|
-
'--depends-on <slugs>',
|
|
44
|
-
'--clear-due',
|
|
45
|
-
'--clear-tags',
|
|
46
|
-
'--clear-description',
|
|
47
|
-
'--clear-estimate',
|
|
48
|
-
'--clear-parent',
|
|
49
|
-
'--clear-depends-on'
|
|
50
|
-
]));
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('adds simplified canvas flags', () => {
|
|
54
|
-
const program = new Command();
|
|
55
|
-
registerTaskCommands(program, {
|
|
56
|
-
chalk: chalkStub,
|
|
57
|
-
resolveVaultPath: stubResolveVaultPath
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const canvasCommand = program.commands.find((command) => command.name() === 'canvas');
|
|
61
|
-
expect(canvasCommand).toBeDefined();
|
|
62
|
-
|
|
63
|
-
const optionFlags = canvasCommand?.options.map((option) => option.flags) ?? [];
|
|
64
|
-
expect(optionFlags).toEqual(expect.arrayContaining([
|
|
65
|
-
'-v, --vault <path>',
|
|
66
|
-
'--output <path>'
|
|
67
|
-
]));
|
|
68
|
-
});
|
|
69
|
-
});
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import { registerTemplateCommands } from './register-template-commands.js';
|
|
4
|
-
import { chalkStub } from './test-helpers/cli-command-fixtures.js';
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
listTemplateDefinitionsMock,
|
|
8
|
-
createFromTemplateMock,
|
|
9
|
-
addTemplateMock
|
|
10
|
-
} = vi.hoisted(() => ({
|
|
11
|
-
listTemplateDefinitionsMock: vi.fn(),
|
|
12
|
-
createFromTemplateMock: vi.fn(),
|
|
13
|
-
addTemplateMock: vi.fn()
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
vi.mock('../dist/commands/template.js', () => ({
|
|
17
|
-
listTemplateDefinitions: listTemplateDefinitionsMock,
|
|
18
|
-
createFromTemplate: createFromTemplateMock,
|
|
19
|
-
addTemplate: addTemplateMock
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
function buildProgram() {
|
|
23
|
-
const program = new Command();
|
|
24
|
-
registerTemplateCommands(program, { chalk: chalkStub });
|
|
25
|
-
return program;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function runCommand(args) {
|
|
29
|
-
const program = buildProgram();
|
|
30
|
-
await program.parseAsync(args, { from: 'user' });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe('register-template-commands', () => {
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
vi.clearAllMocks();
|
|
36
|
-
listTemplateDefinitionsMock.mockReturnValue([]);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('registers list/create/add template subcommands', () => {
|
|
40
|
-
const program = buildProgram();
|
|
41
|
-
const templateCommand = program.commands.find((command) => command.name() === 'template');
|
|
42
|
-
expect(templateCommand).toBeDefined();
|
|
43
|
-
expect(templateCommand?.commands.map((command) => command.name())).toEqual(
|
|
44
|
-
expect.arrayContaining(['list', 'create', 'add'])
|
|
45
|
-
);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('prints template fields in list output', async () => {
|
|
49
|
-
listTemplateDefinitionsMock.mockReturnValue([
|
|
50
|
-
{ name: 'task', fields: ['status', 'owner'] },
|
|
51
|
-
{ name: 'project', fields: ['status', 'client'] }
|
|
52
|
-
]);
|
|
53
|
-
|
|
54
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
55
|
-
try {
|
|
56
|
-
await runCommand(['template', 'list']);
|
|
57
|
-
expect(logSpy).toHaveBeenCalledWith('- task (status, owner)');
|
|
58
|
-
expect(logSpy).toHaveBeenCalledWith('- project (status, client)');
|
|
59
|
-
} finally {
|
|
60
|
-
logSpy.mockRestore();
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('dispatches create and add operations to template command handlers', async () => {
|
|
65
|
-
createFromTemplateMock.mockReturnValue({
|
|
66
|
-
outputPath: '/tmp/task.md',
|
|
67
|
-
templatePath: '/templates/task.md',
|
|
68
|
-
variables: { title: 'Task', type: 'task', date: '2026-02-16', datetime: '2026-02-16T00:00:00.000Z' }
|
|
69
|
-
});
|
|
70
|
-
addTemplateMock.mockReturnValue({
|
|
71
|
-
templatePath: '/vault/templates/custom.md',
|
|
72
|
-
name: 'custom'
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
await runCommand(['template', 'create', 'task', '--title', 'Ship It']);
|
|
76
|
-
expect(createFromTemplateMock).toHaveBeenCalledWith('task', {
|
|
77
|
-
title: 'Ship It',
|
|
78
|
-
vaultPath: undefined
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
await runCommand(['template', 'add', 'source.md', '--name', 'custom']);
|
|
82
|
-
expect(addTemplateMock).toHaveBeenCalledWith('source.md', {
|
|
83
|
-
name: 'custom',
|
|
84
|
-
vaultPath: undefined
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
});
|
|
@@ -1,120 +0,0 @@
|
|
|
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
|
-
patch: async () => ({}),
|
|
34
|
-
capture: async () => ({}),
|
|
35
|
-
find: async () => [],
|
|
36
|
-
vsearch: async () => [],
|
|
37
|
-
list: async () => [],
|
|
38
|
-
get: async () => null,
|
|
39
|
-
stats: async () => ({ tags: [], categories: {} }),
|
|
40
|
-
sync: async () => ({ copied: [], deleted: [], unchanged: [], errors: [] }),
|
|
41
|
-
reindex: async () => 0,
|
|
42
|
-
remember: async () => ({ id: '' }),
|
|
43
|
-
getQmdCollection: () => '',
|
|
44
|
-
createHandoff: async () => ({ id: '', path: '' }),
|
|
45
|
-
generateRecap: async () => ({}),
|
|
46
|
-
formatRecap: () => '',
|
|
47
|
-
...overrides
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function createGetVaultStub(overrides = {}) {
|
|
52
|
-
return async () => createVaultStub(overrides);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function registerAllCommandModules(program = new Command()) {
|
|
56
|
-
const getVault = createGetVaultStub();
|
|
57
|
-
|
|
58
|
-
registerCoreCommands(program, {
|
|
59
|
-
chalk: chalkStub,
|
|
60
|
-
path,
|
|
61
|
-
fs,
|
|
62
|
-
createVault: async () => ({ getCategories: () => [], getQmdRoot: () => '', getQmdCollection: () => '' }),
|
|
63
|
-
getVault,
|
|
64
|
-
runQmd: async () => {}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
registerQueryCommands(program, {
|
|
68
|
-
chalk: chalkStub,
|
|
69
|
-
getVault,
|
|
70
|
-
resolveVaultPath: stubResolveVaultPath,
|
|
71
|
-
QmdUnavailableError: class extends Error {},
|
|
72
|
-
printQmdMissing: () => {}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
registerVaultOperationsCommands(program, {
|
|
76
|
-
chalk: chalkStub,
|
|
77
|
-
fs,
|
|
78
|
-
getVault,
|
|
79
|
-
runQmd: async () => {},
|
|
80
|
-
resolveVaultPath: stubResolveVaultPath,
|
|
81
|
-
path
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
registerMaintenanceCommands(program, { chalk: chalkStub });
|
|
85
|
-
registerResilienceCommands(program, {
|
|
86
|
-
chalk: chalkStub,
|
|
87
|
-
resolveVaultPath: stubResolveVaultPath
|
|
88
|
-
});
|
|
89
|
-
registerSessionLifecycleCommands(program, {
|
|
90
|
-
chalk: chalkStub,
|
|
91
|
-
resolveVaultPath: stubResolveVaultPath,
|
|
92
|
-
QmdUnavailableError: class extends Error {},
|
|
93
|
-
printQmdMissing: () => {},
|
|
94
|
-
getVault,
|
|
95
|
-
runQmd: async () => {}
|
|
96
|
-
});
|
|
97
|
-
registerTemplateCommands(program, { chalk: chalkStub });
|
|
98
|
-
registerConfigCommands(program, {
|
|
99
|
-
chalk: chalkStub,
|
|
100
|
-
resolveVaultPath: stubResolveVaultPath
|
|
101
|
-
});
|
|
102
|
-
registerRouteCommands(program, {
|
|
103
|
-
chalk: chalkStub,
|
|
104
|
-
resolveVaultPath: stubResolveVaultPath
|
|
105
|
-
});
|
|
106
|
-
registerTaskCommands(program, {
|
|
107
|
-
chalk: chalkStub,
|
|
108
|
-
resolveVaultPath: stubResolveVaultPath
|
|
109
|
-
});
|
|
110
|
-
registerKanbanCommands(program, {
|
|
111
|
-
chalk: chalkStub,
|
|
112
|
-
resolveVaultPath: stubResolveVaultPath
|
|
113
|
-
});
|
|
114
|
-
registerProjectCommands(program, {
|
|
115
|
-
chalk: chalkStub,
|
|
116
|
-
resolveVaultPath: stubResolveVaultPath
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
return program;
|
|
120
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
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,254 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as os from 'node:os';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import { buildVaultGraph } from './vault-parser.js';
|
|
6
|
-
|
|
7
|
-
function makeTempVault() {
|
|
8
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'clawvault-dashboard-'));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function writeVaultFile(root, relativePath, content) {
|
|
12
|
-
const fullPath = path.join(root, relativePath);
|
|
13
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
14
|
-
fs.writeFileSync(fullPath, content, 'utf8');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('buildVaultGraph', () => {
|
|
18
|
-
it('builds nodes and edges from markdown wiki-links', async () => {
|
|
19
|
-
const vaultPath = makeTempVault();
|
|
20
|
-
try {
|
|
21
|
-
writeVaultFile(
|
|
22
|
-
vaultPath,
|
|
23
|
-
'decisions/use-clawvault.md',
|
|
24
|
-
`---
|
|
25
|
-
title: Use ClawVault
|
|
26
|
-
tags: [architecture, memory]
|
|
27
|
-
---
|
|
28
|
-
Linked to [[projects/clawvault|ClawVault Project]] and [[missing-note]].
|
|
29
|
-
`
|
|
30
|
-
);
|
|
31
|
-
writeVaultFile(vaultPath, 'projects/clawvault.md', '# ClawVault');
|
|
32
|
-
|
|
33
|
-
const graph = await buildVaultGraph(vaultPath);
|
|
34
|
-
const decisionNode = graph.nodes.find((node) => node.id === 'decisions/use-clawvault');
|
|
35
|
-
const unresolvedNode = graph.nodes.find((node) => node.id === 'missing-note');
|
|
36
|
-
|
|
37
|
-
expect(decisionNode).toMatchObject({
|
|
38
|
-
title: 'Use ClawVault',
|
|
39
|
-
category: 'decisions',
|
|
40
|
-
tags: ['architecture', 'memory'],
|
|
41
|
-
type: 'decision'
|
|
42
|
-
});
|
|
43
|
-
expect(graph.edges).toEqual(expect.arrayContaining([
|
|
44
|
-
expect.objectContaining({
|
|
45
|
-
source: 'decisions/use-clawvault',
|
|
46
|
-
target: 'projects/clawvault',
|
|
47
|
-
type: 'wiki_link'
|
|
48
|
-
}),
|
|
49
|
-
expect.objectContaining({
|
|
50
|
-
source: 'decisions/use-clawvault',
|
|
51
|
-
target: 'missing-note',
|
|
52
|
-
type: 'wiki_link'
|
|
53
|
-
}),
|
|
54
|
-
expect.objectContaining({
|
|
55
|
-
source: 'decisions/use-clawvault',
|
|
56
|
-
target: 'tag:architecture',
|
|
57
|
-
type: 'tag'
|
|
58
|
-
})
|
|
59
|
-
]));
|
|
60
|
-
expect(unresolvedNode).toMatchObject({
|
|
61
|
-
missing: true,
|
|
62
|
-
category: 'unresolved'
|
|
63
|
-
});
|
|
64
|
-
expect(graph.stats.edgeTypeCounts.wiki_link).toBeGreaterThanOrEqual(2);
|
|
65
|
-
expect(graph.stats.edgeTypeCounts.tag).toBeGreaterThanOrEqual(1);
|
|
66
|
-
} finally {
|
|
67
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('resolves basename links when there is a unique match', async () => {
|
|
72
|
-
const vaultPath = makeTempVault();
|
|
73
|
-
try {
|
|
74
|
-
writeVaultFile(vaultPath, 'research/notes.md', 'See [[clawvault]].');
|
|
75
|
-
writeVaultFile(vaultPath, 'projects/clawvault.md', '# ClawVault');
|
|
76
|
-
|
|
77
|
-
const graph = await buildVaultGraph(vaultPath);
|
|
78
|
-
|
|
79
|
-
expect(graph.edges).toEqual(expect.arrayContaining([
|
|
80
|
-
expect.objectContaining({
|
|
81
|
-
source: 'research/notes',
|
|
82
|
-
target: 'projects/clawvault',
|
|
83
|
-
type: 'wiki_link'
|
|
84
|
-
})
|
|
85
|
-
]));
|
|
86
|
-
} finally {
|
|
87
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('emits frontmatter relation edges with labels', async () => {
|
|
92
|
-
const vaultPath = makeTempVault();
|
|
93
|
-
try {
|
|
94
|
-
writeVaultFile(
|
|
95
|
-
vaultPath,
|
|
96
|
-
'decisions/db.md',
|
|
97
|
-
`---
|
|
98
|
-
related:
|
|
99
|
-
- projects/clawvault
|
|
100
|
-
owner: people/alice
|
|
101
|
-
---
|
|
102
|
-
Decision details`
|
|
103
|
-
);
|
|
104
|
-
writeVaultFile(vaultPath, 'projects/clawvault.md', '# ClawVault');
|
|
105
|
-
writeVaultFile(vaultPath, 'people/alice.md', '# Alice');
|
|
106
|
-
|
|
107
|
-
const graph = await buildVaultGraph(vaultPath);
|
|
108
|
-
expect(graph.edges).toEqual(expect.arrayContaining([
|
|
109
|
-
expect.objectContaining({
|
|
110
|
-
source: 'decisions/db',
|
|
111
|
-
target: 'projects/clawvault',
|
|
112
|
-
type: 'frontmatter_relation',
|
|
113
|
-
label: 'related'
|
|
114
|
-
}),
|
|
115
|
-
expect.objectContaining({
|
|
116
|
-
source: 'decisions/db',
|
|
117
|
-
target: 'people/alice',
|
|
118
|
-
type: 'frontmatter_relation',
|
|
119
|
-
label: 'owner'
|
|
120
|
-
})
|
|
121
|
-
]));
|
|
122
|
-
} finally {
|
|
123
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('loads graph data from memory graph index when present', async () => {
|
|
128
|
-
const vaultPath = makeTempVault();
|
|
129
|
-
try {
|
|
130
|
-
writeVaultFile(vaultPath, 'decisions/use-clawvault.md', '# Placeholder');
|
|
131
|
-
writeVaultFile(vaultPath, 'projects/clawvault.md', '# Placeholder project');
|
|
132
|
-
const decisionMtime = fs.statSync(path.join(vaultPath, 'decisions/use-clawvault.md')).mtimeMs;
|
|
133
|
-
const projectMtime = fs.statSync(path.join(vaultPath, 'projects/clawvault.md')).mtimeMs;
|
|
134
|
-
const indexPath = path.join(vaultPath, '.clawvault', 'graph-index.json');
|
|
135
|
-
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
136
|
-
fs.writeFileSync(
|
|
137
|
-
indexPath,
|
|
138
|
-
JSON.stringify({
|
|
139
|
-
schemaVersion: 1,
|
|
140
|
-
files: {
|
|
141
|
-
'decisions/use-clawvault.md': {
|
|
142
|
-
relativePath: 'decisions/use-clawvault.md',
|
|
143
|
-
mtimeMs: decisionMtime
|
|
144
|
-
},
|
|
145
|
-
'projects/clawvault.md': {
|
|
146
|
-
relativePath: 'projects/clawvault.md',
|
|
147
|
-
mtimeMs: projectMtime
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
graph: {
|
|
151
|
-
nodes: [
|
|
152
|
-
{
|
|
153
|
-
id: 'note:decisions/use-clawvault',
|
|
154
|
-
title: 'Use ClawVault',
|
|
155
|
-
type: 'decision',
|
|
156
|
-
category: 'decisions',
|
|
157
|
-
tags: ['architecture'],
|
|
158
|
-
path: 'decisions/use-clawvault.md',
|
|
159
|
-
missing: false,
|
|
160
|
-
degree: 1
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
id: 'note:projects/clawvault',
|
|
164
|
-
title: 'ClawVault Project',
|
|
165
|
-
type: 'project',
|
|
166
|
-
category: 'projects',
|
|
167
|
-
tags: [],
|
|
168
|
-
path: 'projects/clawvault.md',
|
|
169
|
-
missing: false,
|
|
170
|
-
degree: 1
|
|
171
|
-
}
|
|
172
|
-
],
|
|
173
|
-
edges: [
|
|
174
|
-
{
|
|
175
|
-
source: 'note:decisions/use-clawvault',
|
|
176
|
-
target: 'note:projects/clawvault',
|
|
177
|
-
type: 'frontmatter_relation',
|
|
178
|
-
label: 'related'
|
|
179
|
-
}
|
|
180
|
-
],
|
|
181
|
-
stats: { generatedAt: '2026-02-13T00:00:00.000Z' }
|
|
182
|
-
}
|
|
183
|
-
}),
|
|
184
|
-
'utf8'
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
const graph = await buildVaultGraph(vaultPath);
|
|
188
|
-
expect(graph.nodes.find((node) => node.id === 'decisions/use-clawvault')).toBeTruthy();
|
|
189
|
-
expect(graph.edges).toEqual([
|
|
190
|
-
{
|
|
191
|
-
source: 'decisions/use-clawvault',
|
|
192
|
-
target: 'projects/clawvault',
|
|
193
|
-
type: 'frontmatter_relation',
|
|
194
|
-
label: 'related'
|
|
195
|
-
}
|
|
196
|
-
]);
|
|
197
|
-
expect(graph.stats.fileCount).toBe(2);
|
|
198
|
-
} finally {
|
|
199
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('falls back to markdown parsing when memory graph index is stale', async () => {
|
|
204
|
-
const vaultPath = makeTempVault();
|
|
205
|
-
try {
|
|
206
|
-
writeVaultFile(vaultPath, 'projects/clawvault.md', '# ClawVault');
|
|
207
|
-
writeVaultFile(vaultPath, 'decisions/use-clawvault.md', 'See [[projects/clawvault]].');
|
|
208
|
-
|
|
209
|
-
const indexPath = path.join(vaultPath, '.clawvault', 'graph-index.json');
|
|
210
|
-
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
211
|
-
fs.writeFileSync(
|
|
212
|
-
indexPath,
|
|
213
|
-
JSON.stringify({
|
|
214
|
-
schemaVersion: 1,
|
|
215
|
-
generatedAt: '2026-02-13T00:00:00.000Z',
|
|
216
|
-
files: {
|
|
217
|
-
'decisions/use-clawvault.md': { relativePath: 'decisions/use-clawvault.md', mtimeMs: 1 },
|
|
218
|
-
'projects/clawvault.md': { relativePath: 'projects/clawvault.md', mtimeMs: 1 }
|
|
219
|
-
},
|
|
220
|
-
graph: {
|
|
221
|
-
nodes: [
|
|
222
|
-
{
|
|
223
|
-
id: 'note:decisions/use-clawvault',
|
|
224
|
-
title: 'Old node',
|
|
225
|
-
type: 'decision',
|
|
226
|
-
category: 'decisions',
|
|
227
|
-
tags: [],
|
|
228
|
-
path: 'decisions/use-clawvault.md',
|
|
229
|
-
missing: false,
|
|
230
|
-
degree: 0
|
|
231
|
-
}
|
|
232
|
-
],
|
|
233
|
-
edges: [],
|
|
234
|
-
stats: { generatedAt: '2026-02-13T00:00:00.000Z' }
|
|
235
|
-
}
|
|
236
|
-
}),
|
|
237
|
-
'utf8'
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
const graph = await buildVaultGraph(vaultPath);
|
|
241
|
-
const node = graph.nodes.find((candidate) => candidate.id === 'decisions/use-clawvault');
|
|
242
|
-
expect(node?.title).not.toBe('Old node');
|
|
243
|
-
expect(graph.edges).toEqual(expect.arrayContaining([
|
|
244
|
-
expect.objectContaining({
|
|
245
|
-
source: 'decisions/use-clawvault',
|
|
246
|
-
target: 'projects/clawvault',
|
|
247
|
-
type: 'wiki_link'
|
|
248
|
-
})
|
|
249
|
-
]));
|
|
250
|
-
} finally {
|
|
251
|
-
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
});
|