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.
@@ -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.57.2",
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.1"
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
@@ -34,6 +34,9 @@ export const HOOK_FILES = [
34
34
  'subagent-usage.mjs',
35
35
  'pricing.json',
36
36
  'brain-core.mjs',
37
+ 'memory-schema.mjs',
38
+ 'memory-store.mjs',
39
+ 'memory-handoff.mjs',
37
40
  'change-core.mjs',
38
41
  'spec-core.mjs',
39
42
  'sensors-core.mjs',
@@ -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
+ }