wendkeep 0.57.2 → 0.58.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.en.md +42 -8
- package/README.md +42 -8
- package/bin/wendkeep.mjs +16 -4
- package/hooks/brain-inject.mjs +186 -19
- package/hooks/change-core.mjs +42 -5
- package/hooks/lessons-core.mjs +8 -2
- package/hooks/memory-handoff.mjs +199 -0
- package/hooks/memory-mode.mjs +39 -0
- package/hooks/memory-schema.mjs +310 -0
- package/hooks/memory-store.mjs +660 -0
- package/hooks/obsidian-common.mjs +241 -6
- package/hooks/session-ensure.mjs +20 -0
- package/hooks/session-start.mjs +13 -4
- package/hooks/session-stop.mjs +138 -16
- package/hooks/vault-health.mjs +161 -1
- package/package.json +2 -2
- package/src/init.mjs +2 -0
- package/src/memory.mjs +235 -0
- package/src/taxonomy.mjs +3 -0
- package/src/validate-memory.mjs +115 -0
package/hooks/vault-health.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { pathToFileURL } from 'url';
|
|
5
5
|
import {
|
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
wikilinkFromRel,
|
|
12
12
|
} from './obsidian-common.mjs';
|
|
13
13
|
import { getLocale } from './locale.mjs';
|
|
14
|
+
import { parseSharedMemory, validateMemoryEvent } from './memory-schema.mjs';
|
|
15
|
+
import { detectMemoryMode, LEGACY_MEMORY_WARNING } from './memory-mode.mjs';
|
|
16
|
+
import { reduceMemoryEvents } from './memory-store.mjs';
|
|
17
|
+
import { validateMemoryBundle } from '../src/validate-memory.mjs';
|
|
14
18
|
|
|
15
19
|
const DEFAULT_PENDING_PATTERNS = [
|
|
16
20
|
/^- \[ \] Revisar resumo da sessão$/i,
|
|
@@ -93,6 +97,145 @@ function linkedNotesFromSession(content) {
|
|
|
93
97
|
return notes;
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
const MEMORY_STATUS_COMMAND = 'wendkeep memory status --gate --vault <vault>';
|
|
101
|
+
const MEMORY_REPAIR_COMMAND = 'wendkeep memory repair --vault <vault>';
|
|
102
|
+
|
|
103
|
+
function readJsonLines(path, label) {
|
|
104
|
+
if (!existsSync(path)) return { items: [], errors: [] };
|
|
105
|
+
let raw;
|
|
106
|
+
try { raw = readFileSync(path, 'utf8').replace(/\r\n/g, '\n'); }
|
|
107
|
+
catch (error) { return { items: [], errors: [`${label} ilegível: ${error?.message || error}`] }; }
|
|
108
|
+
const lines = raw.endsWith('\n') ? raw.split('\n').slice(0, -1) : raw.split('\n');
|
|
109
|
+
const items = [];
|
|
110
|
+
const errors = [];
|
|
111
|
+
lines.forEach((line, index) => {
|
|
112
|
+
if (!line.trim()) {
|
|
113
|
+
if (raw) errors.push(`${label} linha ${index + 1} está vazia.`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
try { items.push(JSON.parse(line)); }
|
|
117
|
+
catch (error) { errors.push(`${label} linha ${index + 1} contém JSON inválido/parcial: ${error.message}`); }
|
|
118
|
+
});
|
|
119
|
+
return { items, errors };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function inspectOutbox(vaultBase, projectId) {
|
|
123
|
+
const dir = join(vaultBase, '.brain', 'memory-outbox');
|
|
124
|
+
if (!existsSync(dir)) return { count: 0, errors: [] };
|
|
125
|
+
const files = readdirSync(dir).filter((name) => name.endsWith('.json')).sort();
|
|
126
|
+
const errors = [];
|
|
127
|
+
for (const name of files) {
|
|
128
|
+
const path = join(dir, name);
|
|
129
|
+
try {
|
|
130
|
+
const event = JSON.parse(readFileSync(path, 'utf8'));
|
|
131
|
+
const validation = validateMemoryEvent(event, projectId ? { projectId } : {});
|
|
132
|
+
if (!validation.ok) errors.push(`${name}: ${validation.errors.join(' ')}`);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
errors.push(`${name}: JSON inválido: ${error.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { count: files.length, errors };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Read-only consistency check for the local memory-v2 bundle. It intentionally
|
|
142
|
+
* does not acquire MEMORY.lock or invoke the projector/repair paths.
|
|
143
|
+
*/
|
|
144
|
+
export function checkMemoryBundle(vaultBase) {
|
|
145
|
+
const brain = join(vaultBase, '.brain');
|
|
146
|
+
const mode = detectMemoryMode(vaultBase);
|
|
147
|
+
if (mode.mode === 'legacy') {
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
status: 'legacy',
|
|
151
|
+
failures: [],
|
|
152
|
+
warnings: [LEGACY_MEMORY_WARNING],
|
|
153
|
+
metrics: {
|
|
154
|
+
schemaVersion: null,
|
|
155
|
+
revision: null,
|
|
156
|
+
eventCursor: null,
|
|
157
|
+
stateHash: null,
|
|
158
|
+
ledgerEvents: 0,
|
|
159
|
+
pendingOutbox: 0,
|
|
160
|
+
candidates: 0,
|
|
161
|
+
activeConflicts: 0,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const bundle = validateMemoryBundle(vaultBase);
|
|
166
|
+
const failures = [];
|
|
167
|
+
const warnings = [];
|
|
168
|
+
const parsedShared = typeof bundle.shared?.content === 'string'
|
|
169
|
+
? parseSharedMemory(bundle.shared.content)
|
|
170
|
+
: { metadata: {} };
|
|
171
|
+
const metadata = parsedShared.metadata || {};
|
|
172
|
+
const outbox = inspectOutbox(vaultBase, bundle.project?.projectId);
|
|
173
|
+
const candidates = readJsonLines(join(brain, 'MEMORY_CANDIDATES.jsonl'), 'MEMORY_CANDIDATES.jsonl');
|
|
174
|
+
|
|
175
|
+
const ledgerCorrupt = (bundle.ledger?.errors || []).length > 0;
|
|
176
|
+
if (ledgerCorrupt) {
|
|
177
|
+
for (const error of bundle.ledger.errors) {
|
|
178
|
+
failures.push(`${error} Execute com segurança: ${MEMORY_REPAIR_COMMAND}.`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const error of bundle.errors || []) {
|
|
182
|
+
if (error.startsWith('ledger:')) continue;
|
|
183
|
+
failures.push(`${error} Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (outbox.errors.length) {
|
|
187
|
+
failures.push(`Outbox corrompida (${outbox.errors.join('; ')}). Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
188
|
+
}
|
|
189
|
+
if (candidates.errors.length) {
|
|
190
|
+
failures.push(`${candidates.errors.join('; ')} Execute com segurança: ${MEMORY_REPAIR_COMMAND}.`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let replay = null;
|
|
194
|
+
if (bundle.ledger?.ok) {
|
|
195
|
+
try { replay = reduceMemoryEvents(bundle.ledger.events); }
|
|
196
|
+
catch (error) {
|
|
197
|
+
failures.push(`Ledger não pode ser reduzido: ${error.message}. Execute com segurança: ${MEMORY_REPAIR_COMMAND}.`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (replay && bundle.shared?.ok) {
|
|
201
|
+
const divergences = [];
|
|
202
|
+
if (metadata.revision !== replay.revision) divergences.push(`revision ${metadata.revision} != ${replay.revision}`);
|
|
203
|
+
if (metadata.event_cursor !== replay.eventCursor) divergences.push(`event_cursor ${metadata.event_cursor} != ${replay.eventCursor}`);
|
|
204
|
+
if (metadata.state_hash !== replay.stateHash) divergences.push(`state_hash ${metadata.state_hash} != ${replay.stateHash}`);
|
|
205
|
+
if (divergences.length) {
|
|
206
|
+
failures.push(`Projeção SHARED stale/lag (${divergences.join('; ')}). Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const unresolved = candidates.items.filter((item) => !['resolved', 'rejected', 'superseded'].includes(item?.status));
|
|
211
|
+
const activeConflicts = unresolved.filter((item) => item?.reason === 'conflict');
|
|
212
|
+
const ordinaryCandidates = unresolved.filter((item) => item?.reason !== 'conflict');
|
|
213
|
+
if (activeConflicts.length) {
|
|
214
|
+
failures.push(`${activeConflicts.length} conflito ativo em chave operacional (${activeConflicts.map((item) => item.memory_key || item.candidate_id).join(', ')}). Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
215
|
+
}
|
|
216
|
+
if (outbox.count) warnings.push(`${outbox.count} evento(s) pendente(s) na outbox; execute o projector quando seguro.`);
|
|
217
|
+
if (ordinaryCandidates.length) warnings.push(`${ordinaryCandidates.length} candidate(s) aguardando curadoria humana.`);
|
|
218
|
+
for (const warning of bundle.warnings || []) warnings.push(warning);
|
|
219
|
+
|
|
220
|
+
const ok = failures.length === 0;
|
|
221
|
+
return {
|
|
222
|
+
ok,
|
|
223
|
+
status: ok ? (warnings.length ? 'warning' : 'healthy') : 'blocked',
|
|
224
|
+
failures,
|
|
225
|
+
warnings,
|
|
226
|
+
metrics: {
|
|
227
|
+
schemaVersion: metadata.schema_version ?? null,
|
|
228
|
+
revision: metadata.revision ?? null,
|
|
229
|
+
eventCursor: metadata.event_cursor ?? null,
|
|
230
|
+
stateHash: metadata.state_hash ?? null,
|
|
231
|
+
ledgerEvents: bundle.ledger?.events?.length || 0,
|
|
232
|
+
pendingOutbox: outbox.count,
|
|
233
|
+
candidates: candidates.items.length,
|
|
234
|
+
activeConflicts: activeConflicts.length,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
96
239
|
function checkSession({ vaultBase, sessionRel, control, registry }) {
|
|
97
240
|
const failures = [];
|
|
98
241
|
const warnings = [];
|
|
@@ -181,6 +324,21 @@ export function runVaultHealth({ vaultBase, session = '' }) {
|
|
|
181
324
|
return total + (existsSync(dir) ? listMarkdownFiles(dir).length : 0);
|
|
182
325
|
}, 0);
|
|
183
326
|
|
|
327
|
+
const memoryMarkers = [
|
|
328
|
+
join(vaultBase, '.brain', 'SHARED_MEMORY.md'),
|
|
329
|
+
join(vaultBase, '.brain', 'MEMORY_EVENTS.jsonl'),
|
|
330
|
+
join(vaultBase, '.brain', 'MEMORY_CANDIDATES.jsonl'),
|
|
331
|
+
join(vaultBase, '.brain', 'memory-outbox'),
|
|
332
|
+
];
|
|
333
|
+
let memory = { status: 'legacy', metrics: {} };
|
|
334
|
+
if (memoryMarkers.some((path) => existsSync(path))) {
|
|
335
|
+
memory = checkMemoryBundle(vaultBase);
|
|
336
|
+
failures.push(...memory.failures.map((item) => `Memória: ${item}`));
|
|
337
|
+
warnings.push(...memory.warnings.map((item) => `Memória: ${item}`));
|
|
338
|
+
} else {
|
|
339
|
+
warnings.push(`Bundle de memória v2 ausente (vault legado); inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
340
|
+
}
|
|
341
|
+
|
|
184
342
|
return {
|
|
185
343
|
ok: failures.length === 0,
|
|
186
344
|
session: sessionRel,
|
|
@@ -190,7 +348,9 @@ export function runVaultHealth({ vaultBase, session = '' }) {
|
|
|
190
348
|
...sessionResult.metrics,
|
|
191
349
|
registrySessions: Object.keys(registry.sessions || {}).length,
|
|
192
350
|
derivedNotes: derivedCount,
|
|
351
|
+
memory: memory.metrics,
|
|
193
352
|
},
|
|
353
|
+
memoryStatus: memory.status,
|
|
194
354
|
};
|
|
195
355
|
}
|
|
196
356
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.1",
|
|
4
4
|
"description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -49,6 +49,6 @@
|
|
|
49
49
|
"url": "https://github.com/rogersialves/wendkeep/issues"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"wendkeep": "^0.57.
|
|
52
|
+
"wendkeep": "^0.57.2"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/src/init.mjs
CHANGED
|
@@ -39,6 +39,7 @@ import { LOCALES, DEFAULT_LOCALE, getLocale, clearLocaleCache, vaultFolders } fr
|
|
|
39
39
|
import { seedDotcontext, globalHasDotcontext, resolveDotcontextSkipMcp, renderSensorsJson } from './dotcontext-seed.mjs';
|
|
40
40
|
import { adoptSpecsState, ensureSpecsReadme, SPECS_STATE_FILE } from '../hooks/spec-core.mjs';
|
|
41
41
|
import { bindProjectVault, readProjectBinding } from './project-vault.mjs';
|
|
42
|
+
import { seedMemoryV2 } from './memory.mjs';
|
|
42
43
|
|
|
43
44
|
function parseArgs(argv) {
|
|
44
45
|
const args = { mcp: true, yes: false, force: false };
|
|
@@ -506,6 +507,7 @@ export async function runInit(argv) {
|
|
|
506
507
|
if (!existsSync(corePath)) writeFileSync(corePath, renderCoreSkeleton(loc.id), 'utf8');
|
|
507
508
|
const protoPath = join(brainDir, 'COMPACTION_PROTOCOL.md');
|
|
508
509
|
if (!existsSync(protoPath)) writeFileSync(protoPath, renderCompactionProtocol(), 'utf8');
|
|
510
|
+
seedMemoryV2(vaultPath);
|
|
509
511
|
// Seed the definitions layer (.brain/agents + .brain/skills): versioned source of
|
|
510
512
|
// truth for custom agents/skills. `wendkeep sync-defs` copies them to the agent dirs.
|
|
511
513
|
seedDefinitions(brainDir);
|
package/src/memory.mjs
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync,
|
|
4
|
+
} from 'node:fs';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { sanitizeMemoryText, renderSharedMemory, validateSharedMemory } from '../hooks/memory-schema.mjs';
|
|
7
|
+
import {
|
|
8
|
+
enqueueMemoryEvent, projectMemoryOutbox, repairMemoryLedger,
|
|
9
|
+
} from '../hooks/memory-store.mjs';
|
|
10
|
+
import { writeFileAtomic } from '../hooks/session-note-io.mjs';
|
|
11
|
+
import { validateMemoryBundle } from './validate-memory.mjs';
|
|
12
|
+
import { checkMemoryBundle } from '../hooks/vault-health.mjs';
|
|
13
|
+
|
|
14
|
+
const BRAIN = '.brain';
|
|
15
|
+
const LEDGER = 'MEMORY_EVENTS.jsonl';
|
|
16
|
+
const SHARED = 'SHARED_MEMORY.md';
|
|
17
|
+
const CANDIDATES = 'MEMORY_CANDIDATES.jsonl';
|
|
18
|
+
|
|
19
|
+
function brainPath(vault, name) { return join(vault, BRAIN, name); }
|
|
20
|
+
function hash(value) { return createHash('sha256').update(String(value)).digest('hex'); }
|
|
21
|
+
|
|
22
|
+
function projectId(vault) {
|
|
23
|
+
const marker = JSON.parse(readFileSync(brainPath(vault, 'PROJECT.json'), 'utf8'));
|
|
24
|
+
if (!marker?.projectId) throw new Error('PROJECT.json inválido: projectId ausente.');
|
|
25
|
+
return marker.projectId;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function legacyCandidates(content) {
|
|
29
|
+
const safe = sanitizeMemoryText(content).replace(/\r\n/g, '\n');
|
|
30
|
+
const candidates = [];
|
|
31
|
+
let section = 'Legacy';
|
|
32
|
+
for (const line of safe.split('\n')) {
|
|
33
|
+
const heading = line.match(/^#{1,3}\s+(.+)/);
|
|
34
|
+
if (heading) { section = heading[1].trim(); continue; }
|
|
35
|
+
const value = line.replace(/^[-*]\s+/, '').trim();
|
|
36
|
+
if (!value) continue;
|
|
37
|
+
const candidateId = `legacy-${hash(`${section}\0${value}`).slice(0, 16)}`;
|
|
38
|
+
candidates.push({
|
|
39
|
+
v: 1,
|
|
40
|
+
candidate_id: candidateId,
|
|
41
|
+
reason: 'legacy_shared',
|
|
42
|
+
memory_key: `legacy.${hash(section).slice(0, 8)}.${candidates.length + 1}`,
|
|
43
|
+
value,
|
|
44
|
+
section: sanitizeMemoryText(section),
|
|
45
|
+
source: 'SHARED_MEMORY.md',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return candidates;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function candidateText(candidates) {
|
|
52
|
+
return candidates.map((item) => `${JSON.stringify(item)}\n`).join('');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function seedMemoryV2(vault) {
|
|
56
|
+
const brain = join(vault, BRAIN);
|
|
57
|
+
mkdirSync(brain, { recursive: true });
|
|
58
|
+
const created = [];
|
|
59
|
+
const artifacts = [
|
|
60
|
+
[LEDGER, ''],
|
|
61
|
+
[CANDIDATES, ''],
|
|
62
|
+
[SHARED, renderSharedMemory()],
|
|
63
|
+
];
|
|
64
|
+
for (const [name, content] of artifacts) {
|
|
65
|
+
const path = join(brain, name);
|
|
66
|
+
if (!existsSync(path)) {
|
|
67
|
+
writeFileSync(path, content, 'utf8');
|
|
68
|
+
created.push(name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { status: created.length ? 'seeded' : 'unchanged', created };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function memoryStatus(vault) {
|
|
75
|
+
return checkMemoryBundle(vault);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function migrateMemory(vault, {
|
|
79
|
+
apply = false,
|
|
80
|
+
validateBundle = validateMemoryBundle,
|
|
81
|
+
publishArtifact = writeFileAtomic,
|
|
82
|
+
} = {}) {
|
|
83
|
+
projectId(vault);
|
|
84
|
+
const sharedPath = brainPath(vault, SHARED);
|
|
85
|
+
const hadShared = existsSync(sharedPath);
|
|
86
|
+
const legacy = hadShared ? readFileSync(sharedPath, 'utf8') : '';
|
|
87
|
+
const parsed = validateSharedMemory(legacy);
|
|
88
|
+
const alreadyV2 = parsed.ok && parsed.metadata.schema_version === 2;
|
|
89
|
+
const candidates = alreadyV2 ? [] : legacyCandidates(legacy);
|
|
90
|
+
const backupPath = hadShared ? `${sharedPath}.legacy-${hash(legacy).slice(0, 12)}.bak` : null;
|
|
91
|
+
if (!apply) return { status: 'dry-run', alreadyV2, candidates: candidates.length, backupPath };
|
|
92
|
+
if (alreadyV2) {
|
|
93
|
+
seedMemoryV2(vault);
|
|
94
|
+
return { status: 'unchanged', alreadyV2: true, candidates: 0, backupPath: null };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate every generated byte before the first publication. CORE is never written.
|
|
98
|
+
const emptyShared = renderSharedMemory();
|
|
99
|
+
const sharedValidation = validateSharedMemory(emptyShared, { eventIds: new Set() });
|
|
100
|
+
if (!sharedValidation.ok) throw new Error(`Migração inválida: ${sharedValidation.errors.join(' ')}`);
|
|
101
|
+
if (backupPath && !existsSync(backupPath)) copyFileSync(sharedPath, backupPath);
|
|
102
|
+
|
|
103
|
+
// Build and validate a complete candidate vault away from the live paths. This makes
|
|
104
|
+
// the validation callback incapable of observing a half-published live bundle.
|
|
105
|
+
const stagingVault = mkdtempSync(join(dirname(vault), '.wendkeep-memory-stage-'));
|
|
106
|
+
const stagingBrain = join(stagingVault, BRAIN);
|
|
107
|
+
let stagedValidation;
|
|
108
|
+
try {
|
|
109
|
+
mkdirSync(stagingBrain, { recursive: true });
|
|
110
|
+
copyFileSync(brainPath(vault, 'CORE.md'), join(stagingBrain, 'CORE.md'));
|
|
111
|
+
copyFileSync(brainPath(vault, 'PROJECT.json'), join(stagingBrain, 'PROJECT.json'));
|
|
112
|
+
writeFileSync(join(stagingBrain, LEDGER), '', 'utf8');
|
|
113
|
+
writeFileSync(join(stagingBrain, SHARED), emptyShared, 'utf8');
|
|
114
|
+
writeFileSync(join(stagingBrain, CANDIDATES), candidateText(candidates), 'utf8');
|
|
115
|
+
stagedValidation = validateBundle(stagingVault);
|
|
116
|
+
if (!stagedValidation.ok) {
|
|
117
|
+
throw new Error(`Bundle migrado inválido: ${(stagedValidation.errors || []).join(' ')}`);
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
rmSync(stagingVault, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const targets = [LEDGER, SHARED, CANDIDATES].map((name) => brainPath(vault, name));
|
|
124
|
+
const before = new Map(targets.map((path) => [path, {
|
|
125
|
+
existed: existsSync(path),
|
|
126
|
+
content: existsSync(path) ? readFileSync(path, 'utf8') : '',
|
|
127
|
+
}]));
|
|
128
|
+
try {
|
|
129
|
+
if (!existsSync(brainPath(vault, LEDGER))) publishArtifact(brainPath(vault, LEDGER), '');
|
|
130
|
+
publishArtifact(sharedPath, emptyShared);
|
|
131
|
+
publishArtifact(brainPath(vault, CANDIDATES), candidateText(candidates));
|
|
132
|
+
const validation = validateMemoryBundle(vault);
|
|
133
|
+
if (!validation.ok) throw new Error(`Bundle migrado inválido: ${validation.errors.join(' ')}`);
|
|
134
|
+
return { status: 'migrated', alreadyV2: false, candidates: candidates.length, backupPath, validation };
|
|
135
|
+
} catch (error) {
|
|
136
|
+
for (const [path, snapshot] of before) {
|
|
137
|
+
if (snapshot.existed) writeFileAtomic(path, snapshot.content);
|
|
138
|
+
else if (existsSync(path)) unlinkSync(path);
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readCandidates(vault) {
|
|
145
|
+
const path = brainPath(vault, CANDIDATES);
|
|
146
|
+
if (!existsSync(path)) return [];
|
|
147
|
+
return readFileSync(path, 'utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function decideMemoryCandidate(vault, { action, candidateId, value } = {}) {
|
|
151
|
+
if (!['promote', 'reject'].includes(action)) throw new TypeError('action deve ser promote ou reject.');
|
|
152
|
+
if (!candidateId) throw new TypeError('candidateId é obrigatório.');
|
|
153
|
+
const candidates = readCandidates(vault);
|
|
154
|
+
const candidate = candidates.find((item) => item.candidate_id === candidateId);
|
|
155
|
+
if (!candidate) throw new Error(`Candidate não encontrado: ${candidateId}`);
|
|
156
|
+
const now = new Date().toISOString();
|
|
157
|
+
const event = {
|
|
158
|
+
v: 1,
|
|
159
|
+
event_id: `cli-${action}-${hash(candidateId).slice(0, 20)}`,
|
|
160
|
+
project_id: projectId(vault),
|
|
161
|
+
memory_key: action === 'promote' ? candidate.memory_key : `candidate.rejected.${candidateId}`,
|
|
162
|
+
operation: 'assert',
|
|
163
|
+
value: sanitizeMemoryText(action === 'promote' ? (value ?? candidate.value ?? candidate.values?.[0] ?? '') : 'rejected'),
|
|
164
|
+
authority: 'verified',
|
|
165
|
+
activation_id: 'wendkeep-memory-cli',
|
|
166
|
+
turn_sequence: 0,
|
|
167
|
+
observed_at: now,
|
|
168
|
+
evidence: [`candidate:${candidateId}`],
|
|
169
|
+
};
|
|
170
|
+
enqueueMemoryEvent(vault, event);
|
|
171
|
+
const projection = projectMemoryOutbox(vault);
|
|
172
|
+
if (projection.status === 'busy') return { status: 'busy', candidateId };
|
|
173
|
+
const remaining = candidates.filter((item) => item.candidate_id !== candidateId);
|
|
174
|
+
const projected = readCandidates(vault);
|
|
175
|
+
const merged = new Map([...remaining, ...projected].map((item) => [item.candidate_id, item]));
|
|
176
|
+
writeFileAtomic(brainPath(vault, CANDIDATES), candidateText([...merged.values()]));
|
|
177
|
+
return { status: action === 'promote' ? 'promoted' : 'rejected', candidateId, eventId: event.event_id, projection };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function repairMemory(vault) {
|
|
181
|
+
const repaired = repairMemoryLedger(vault);
|
|
182
|
+
if (repaired.status === 'busy') return repaired;
|
|
183
|
+
const projection = projectMemoryOutbox(vault);
|
|
184
|
+
return { status: projection.status === 'busy' ? 'busy' : 'repaired', repaired, projection };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function runValidateMemoryBundle(argv) {
|
|
188
|
+
const vault = option(argv, '--vault');
|
|
189
|
+
if (!vault) {
|
|
190
|
+
process.stderr.write('wendkeep validate-memory: --vault requer um path.\n');
|
|
191
|
+
process.exitCode = 2;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!existsSync(vault)) {
|
|
195
|
+
process.stderr.write(`wendkeep validate-memory: not found: ${vault}\n`);
|
|
196
|
+
process.exitCode = 2;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const result = validateMemoryBundle(vault);
|
|
200
|
+
if (!result.ok) {
|
|
201
|
+
process.stderr.write(`❌ bundle de memória inválido (${result.errors.length} erro(s)):\n`);
|
|
202
|
+
for (const error of result.errors) process.stderr.write(` - ${error}\n`);
|
|
203
|
+
process.exitCode = 1;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
process.stdout.write('✅ bundle de memória v2 OK (CORE + ledger + SHARED).\n');
|
|
207
|
+
process.exitCode = 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function option(argv, name) {
|
|
211
|
+
const index = argv.indexOf(name);
|
|
212
|
+
if (index >= 0) return argv[index + 1] || '';
|
|
213
|
+
return argv.find((item) => item.startsWith(`${name}=`))?.slice(name.length + 1) || '';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function runMemory(argv) {
|
|
217
|
+
const [sub, positional] = argv;
|
|
218
|
+
const vault = option(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
219
|
+
if (!vault) { process.stderr.write('wendkeep memory: passe --vault <path>.\n'); process.exitCode = 2; return; }
|
|
220
|
+
try {
|
|
221
|
+
let result;
|
|
222
|
+
if (sub === 'status') result = memoryStatus(vault);
|
|
223
|
+
else if (sub === 'migrate') result = migrateMemory(vault, { apply: argv.includes('--apply') });
|
|
224
|
+
else if (sub === 'repair') result = repairMemory(vault);
|
|
225
|
+
else if (sub === 'promote' || sub === 'reject') result = decideMemoryCandidate(vault, { action: sub, candidateId: positional });
|
|
226
|
+
else { process.stderr.write('wendkeep memory: use status | migrate [--apply] | repair | promote <candidate> | reject <candidate>.\n'); process.exitCode = 2; return; }
|
|
227
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
228
|
+
process.exitCode = sub === 'status' && argv.includes('--gate')
|
|
229
|
+
? (result.status === 'blocked' ? 1 : 0)
|
|
230
|
+
: (result.ok === false ? 1 : 0);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
process.stderr.write(`wendkeep memory: ${error.message}\n`);
|
|
233
|
+
process.exitCode = 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/taxonomy.mjs
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { validateMemoryEvent, validateSharedMemory } from '../hooks/memory-schema.mjs';
|
|
4
|
+
import { validateCore } from './validate-core.mjs';
|
|
5
|
+
|
|
6
|
+
function failedComponent(errors, extra = {}) {
|
|
7
|
+
return { ok: false, errors: Array.isArray(errors) ? errors : [errors], warnings: [], ...extra };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readRequired(path, label) {
|
|
11
|
+
if (!existsSync(path)) return { ok: false, error: `${label} ausente: ${path}` };
|
|
12
|
+
try {
|
|
13
|
+
return { ok: true, content: readFileSync(path, 'utf8') };
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return { ok: false, error: `${label} ilegível: ${error?.message || error}` };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readProjectForValidation(vaultBase) {
|
|
20
|
+
const path = join(vaultBase, '.brain', 'PROJECT.json');
|
|
21
|
+
const read = readRequired(path, 'PROJECT.json');
|
|
22
|
+
if (!read.ok) return failedComponent(read.error, { projectId: '', path });
|
|
23
|
+
try {
|
|
24
|
+
const marker = JSON.parse(read.content);
|
|
25
|
+
if (!marker || typeof marker.projectId !== 'string' || !marker.projectId) {
|
|
26
|
+
return failedComponent('PROJECT.json inválido: projectId ausente.', { projectId: '', path });
|
|
27
|
+
}
|
|
28
|
+
return { ok: true, errors: [], warnings: [], projectId: marker.projectId, marker, path };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return failedComponent(`PROJECT.json contém JSON inválido: ${error?.message || error}`, { projectId: '', path });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Read and validate the append-only JSONL authority without repairing or mutating it. */
|
|
35
|
+
export function readLedgerForValidation(vaultBase, { projectId } = {}) {
|
|
36
|
+
const path = join(vaultBase, '.brain', 'MEMORY_EVENTS.jsonl');
|
|
37
|
+
const read = readRequired(path, 'MEMORY_EVENTS.jsonl');
|
|
38
|
+
if (!read.ok) return failedComponent(read.error, { events: [], eventIds: new Set(), path });
|
|
39
|
+
|
|
40
|
+
const errors = [];
|
|
41
|
+
const warnings = [];
|
|
42
|
+
const events = [];
|
|
43
|
+
const eventIds = new Set();
|
|
44
|
+
const normalized = read.content.replace(/\r\n/g, '\n');
|
|
45
|
+
const lines = normalized.split('\n');
|
|
46
|
+
const logicalLines = normalized === '' ? [] : (normalized.endsWith('\n') ? lines.slice(0, -1) : lines);
|
|
47
|
+
logicalLines.forEach((line, index) => {
|
|
48
|
+
if (!line.trim()) {
|
|
49
|
+
errors.push(`MEMORY_EVENTS.jsonl linha ${index + 1} está vazia no meio do ledger.`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let event;
|
|
53
|
+
try {
|
|
54
|
+
event = JSON.parse(line);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
errors.push(`MEMORY_EVENTS.jsonl linha ${index + 1} contém JSON inválido/parcial: ${error?.message || error}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const validation = validateMemoryEvent(event, projectId ? { projectId } : {});
|
|
60
|
+
for (const error of validation.errors) errors.push(`MEMORY_EVENTS.jsonl linha ${index + 1}: ${error}`);
|
|
61
|
+
for (const warning of validation.warnings) warnings.push(`MEMORY_EVENTS.jsonl linha ${index + 1}: ${warning}`);
|
|
62
|
+
if (typeof event?.event_id === 'string' && event.event_id) {
|
|
63
|
+
if (eventIds.has(event.event_id)) errors.push(`MEMORY_EVENTS.jsonl event_id duplicado: ${event.event_id}.`);
|
|
64
|
+
eventIds.add(event.event_id);
|
|
65
|
+
}
|
|
66
|
+
events.push(event);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
ok: errors.length === 0,
|
|
71
|
+
errors,
|
|
72
|
+
warnings,
|
|
73
|
+
events,
|
|
74
|
+
eventIds,
|
|
75
|
+
lineCount: logicalLines.length,
|
|
76
|
+
path,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validateCoreArtifact(vaultBase) {
|
|
81
|
+
const path = join(vaultBase, '.brain', 'CORE.md');
|
|
82
|
+
const read = readRequired(path, 'CORE.md');
|
|
83
|
+
if (!read.ok) return failedComponent(read.error, { lineCount: 0, path });
|
|
84
|
+
return { ...validateCore(read.content), path, content: read.content };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function validateSharedArtifact(vaultBase, eventIds) {
|
|
88
|
+
const path = join(vaultBase, '.brain', 'SHARED_MEMORY.md');
|
|
89
|
+
const read = readRequired(path, 'SHARED_MEMORY.md');
|
|
90
|
+
if (!read.ok) return failedComponent(read.error, { path });
|
|
91
|
+
return { ...validateSharedMemory(read.content, { eventIds }), path, content: read.content };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function combineMemoryResults({ project, core, ledger, shared }) {
|
|
95
|
+
const components = { project, core, ledger, shared };
|
|
96
|
+
const errors = [];
|
|
97
|
+
const warnings = [];
|
|
98
|
+
for (const [name, result] of Object.entries(components)) {
|
|
99
|
+
for (const error of result?.errors || []) errors.push(`${name}: ${error}`);
|
|
100
|
+
for (const warning of result?.warnings || []) warnings.push(`${name}: ${warning}`);
|
|
101
|
+
}
|
|
102
|
+
return { ok: errors.length === 0, errors, warnings, ...components };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate the v2 local memory bundle as a read-only composition. Missing/corrupt
|
|
107
|
+
* artifacts remain explicit failures; they are never silently treated as empty.
|
|
108
|
+
*/
|
|
109
|
+
export function validateMemoryBundle(vaultBase) {
|
|
110
|
+
const project = readProjectForValidation(vaultBase);
|
|
111
|
+
const core = validateCoreArtifact(vaultBase);
|
|
112
|
+
const ledger = readLedgerForValidation(vaultBase, { projectId: project.projectId });
|
|
113
|
+
const shared = validateSharedArtifact(vaultBase, ledger.eventIds);
|
|
114
|
+
return combineMemoryResults({ project, core, ledger, shared });
|
|
115
|
+
}
|