wendkeep 0.60.0 → 0.62.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.
@@ -0,0 +1,181 @@
1
+ // Memory-compaction protocol for the curated .brain/CORE.md layer.
2
+ // Ported from NutriGym-Vision's scripts/validate-brain-core.js to ESM:
3
+ // - cap 25 lines (hard), 22 (soft warning) — 1 durable item per line
4
+ // - 3 required sections
5
+ // - no secrets / no real-provider PII emails
6
+ // Plus the seeded skeleton and the protocol reference doc.
7
+
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import { isAbsolute, join, resolve } from 'node:path';
10
+
11
+ const HARD_LIMIT = 25;
12
+ const SOFT_LIMIT = 22;
13
+
14
+ // Bilingual (0.8.0): a CORE is valid when it carries the COMPLETE section set of either
15
+ // locale — pt-BR or en. Mixed/partial sets fail (the 3 sections are one contract).
16
+ const SECTION_SETS = {
17
+ 'pt-BR': [
18
+ { label: 'Preferências do Usuário', regex: /^##\s+Prefer[êe]ncias\s+do\s+Usu[áa]rio\s*$/im },
19
+ { label: 'Padrões Ativos', regex: /^##\s+Padr[õo]es\s+Ativos\s*$/im },
20
+ { label: 'Pendências Abertas', regex: /^##\s+Pend[êe]ncias\s+Abertas\s*$/im },
21
+ ],
22
+ en: [
23
+ { label: 'User Preferences', regex: /^##\s+User\s+Preferences\s*$/im },
24
+ { label: 'Active Patterns', regex: /^##\s+Active\s+Patterns\s*$/im },
25
+ { label: 'Open Items', regex: /^##\s+Open\s+Items\s*$/im },
26
+ ],
27
+ };
28
+
29
+ // Secret patterns reject only "real" values (length floor); abstract mentions like
30
+ // `sk_*` / `whsec_*` (trailing asterisk) are allowed.
31
+ const SECRET_PATTERNS = [
32
+ { name: 'Stripe secret key', regex: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/ },
33
+ { name: 'Stripe webhook secret', regex: /\bwhsec_[A-Za-z0-9]{20,}\b/ },
34
+ { name: 'JWT token', regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
35
+ { name: 'Bearer token', regex: /\bBearer\s+[A-Za-z0-9._-]{20,}\b/i },
36
+ { name: 'OpenAI API key', regex: /\bsk-[A-Za-z0-9]{40,}\b/ },
37
+ { name: 'Anthropic API key', regex: /\bsk-ant-[A-Za-z0-9_-]{40,}\b/ },
38
+ { name: 'Google API key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/ },
39
+ ];
40
+
41
+ const PII_EMAIL_REGEX = /\b[A-Za-z0-9._%+-]+@(?!example\.(?:com|org|net)\b)(?:gmail|hotmail|yahoo|outlook|live|icloud|protonmail)\.[A-Za-z]{2,}\b/i;
42
+
43
+ // Validate CORE.md content. Returns { ok, errors, warnings, lineCount }.
44
+ export function validateCore(content) {
45
+ const text = String(content ?? '');
46
+ const lines = text.split('\n');
47
+ const lineCount = text.endsWith('\n') ? lines.length - 1 : lines.length;
48
+ const errors = [];
49
+
50
+ if (lineCount > HARD_LIMIT) {
51
+ errors.push(`Tamanho ${lineCount} > ${HARD_LIMIT} linhas (hard limit). Curar: remover itens resolvidos (detalhe vive no vault/git).`);
52
+ }
53
+ // Pick the locale set that matches best; require it to be complete.
54
+ const missingBySet = Object.values(SECTION_SETS).map((set) => set.filter(({ regex }) => !regex.test(text)));
55
+ const best = missingBySet.reduce((a, b) => (b.length < a.length ? b : a));
56
+ for (const { label } of best) errors.push(`Seção obrigatória ausente: ## ${label}`);
57
+ for (const { name, regex } of SECRET_PATTERNS) {
58
+ const m = text.match(regex);
59
+ if (m) errors.push(`Possível ${name} detectado: "${m[0].slice(0, 30)}..." — substituir por [REDACTED_SECRET].`);
60
+ }
61
+ const em = text.match(PII_EMAIL_REGEX);
62
+ if (em) errors.push(`Email real detectado: "${em[0]}" — usar user@example.com.`);
63
+
64
+ const warnings = [];
65
+ if (lineCount >= SOFT_LIMIT && lineCount <= HARD_LIMIT) {
66
+ warnings.push(`Tamanho ${lineCount}/${HARD_LIMIT} linhas — perto do limite; remover itens resolvidos (≥${SOFT_LIMIT}).`);
67
+ }
68
+
69
+ return { ok: errors.length === 0, errors, warnings, lineCount };
70
+ }
71
+
72
+ // The seeded CORE.md (must pass validateCore). Bootstraps the 3 sections so the
73
+ // curated hot layer exists with the right shape from day one.
74
+ export function renderCoreSkeleton(localeId = 'pt-BR') {
75
+ if (localeId === 'en') {
76
+ return `# CORE — curated memory core (.brain)
77
+
78
+ > RULE #1 — the project's canonical memory. Hand-curated, 25-line cap (validate: \`wendkeep validate-memory\`). Volatile facts live in DIGEST.md (auto). Depth: /brain-recall <topic>.
79
+
80
+ ## User Preferences
81
+ - (durable preferences: language, style, conventions)
82
+
83
+ ## Active Patterns
84
+ - (active patterns/architecture another agent must know)
85
+
86
+ ## Open Items
87
+ - (open items/decisions — remove when resolved)
88
+ `;
89
+ }
90
+ return `# CORE — núcleo curado da memória (.brain)
91
+
92
+ > REGRA #1 — memória canônica do projeto. Curado à mão, cap 25 linhas (valide: \`wendkeep validate-memory\`). Volátil vive no DIGEST.md (auto). Profundidade: /brain-recall <tópico>.
93
+
94
+ ## Preferências do Usuário
95
+ - (preferências duráveis: idioma, estilo, convenções)
96
+
97
+ ## Padrões Ativos
98
+ - (padrões/arquitetura ativos que outro agente precise saber)
99
+
100
+ ## Pendências Abertas
101
+ - (pendências/decisões em aberto — remova quando resolvidas)
102
+ `;
103
+ }
104
+
105
+ // The compaction-protocol reference doc dropped into the vault.
106
+ export function renderCompactionProtocol() {
107
+ return `# Protocolo de Memória — núcleo curado + digest automático (.brain)
108
+
109
+ > Como cada agente recebe, consulta e persiste memória entre sessões no seu vault.
110
+
111
+ ## 1. Duas camadas
112
+
113
+ - **QUENTE** (auto-injetada por sessão, budget ~45 linhas):
114
+ - \`.brain/CORE.md\` — curado à mão, **≤25 linhas** (1 item/linha): preferências, padrões, pendências.
115
+ - \`.brain/DIGEST.md\` — auto-gerado (0 token LLM, ≤15 linhas): decisões/sessões/bugs/aprendizados recentes.
116
+ - **FRIA** (sob demanda):
117
+ - \`.brain/index.jsonl\` — índice de todas as sessões (1/linha, frontmatter).
118
+ - Vault: \`02-Sessões/**\`, \`04-Decisões/**\`, \`05-Bugs/**\`, \`06-Aprendizados/**\`. Desce via \`/brain-recall <tópico>\`.
119
+
120
+ ## 2. Compactação = regra de geração (sem trabalho manual)
121
+
122
+ - **DIGEST se auto-compacta**: caps determinísticos (5 decisões, 4 sessões, 2 bugs, 2 aprendizados + \`+N mais\`). O velho cai do quente sozinho e permanece no índice/vault. **NUNCA editar** \`DIGEST.md\`/\`index.jsonl\`.
123
+ - **CORE**: quando ≥22 linhas (soft warning), remover itens resolvidos/obsoletos — o detalhe já vive no vault e no histórico do git.
124
+
125
+ ## 3. O que escrever no CORE
126
+
127
+ Só estado **durável** que outro agente precise saber — preferência, padrão ativo, pendência aberta. 1 linha por item. Nunca log de sessão (isso é automático no vault).
128
+
129
+ 3 seções fixas (obrigatórias): \`## Preferências do Usuário\`, \`## Padrões Ativos\`, \`## Pendências Abertas\`.
130
+
131
+ ## 4. Sem segredos / PII
132
+
133
+ \`CORE.md\` nunca contém tokens (\`sk_*\`, \`whsec_*\`, JWT, Bearer), API keys, senhas ou email/telefone real. Use \`[REDACTED_SECRET]\` / \`user@example.com\`.
134
+
135
+ ## 5. Validação
136
+
137
+ \`\`\`bash
138
+ wendkeep validate-memory # valida <vault>/.brain/CORE.md
139
+ wendkeep validate-memory <path> # valida outro arquivo
140
+ \`\`\`
141
+
142
+ Checa: cap 25 (soft 22), 3 seções, sem segredos/PII. Exit 0 = OK, 1 = falha.
143
+ `;
144
+ }
145
+
146
+ // CLI entry for `wendkeep validate-memory [path]`. Resolves the target from an
147
+ // explicit path, else <vault>/.brain/CORE.md (--vault or OBSIDIAN_VAULT_PATH).
148
+ export function runValidateMemory(argv) {
149
+ let target;
150
+ let vault;
151
+ for (let i = 0; i < argv.length; i += 1) {
152
+ const a = argv[i];
153
+ if (a === '--vault') vault = argv[++i];
154
+ else if (a.startsWith('--vault=')) vault = a.slice(8);
155
+ else if (!a.startsWith('-')) target = a;
156
+ }
157
+ if (!target) {
158
+ const base = vault || process.env.OBSIDIAN_VAULT_PATH;
159
+ if (!base) {
160
+ process.stderr.write('wendkeep validate-memory: no target. Pass a path, --vault <path>, or set OBSIDIAN_VAULT_PATH.\n');
161
+ process.exit(2);
162
+ }
163
+ target = join(base, '.brain', 'CORE.md');
164
+ }
165
+ const abs = isAbsolute(target) ? target : resolve(process.cwd(), target);
166
+ if (!existsSync(abs)) {
167
+ process.stderr.write(`wendkeep validate-memory: not found: ${abs}\n`);
168
+ process.exit(2);
169
+ }
170
+ const res = validateCore(readFileSync(abs, 'utf8'));
171
+ if (!res.ok) {
172
+ process.stderr.write(`❌ CORE.md viola protocolo (${res.errors.length} erro${res.errors.length > 1 ? 's' : ''}):\n`);
173
+ for (const e of res.errors) process.stderr.write(` - ${e}\n`);
174
+ process.stderr.write('\nProtocolo: .brain/COMPACTION_PROTOCOL.md\n');
175
+ process.exit(1);
176
+ }
177
+ let msg = `✅ CORE.md OK (${res.lineCount} linhas, 3/3 seções, sem segredos).`;
178
+ for (const w of res.warnings) msg += `\n ⚠ ${w}`;
179
+ process.stdout.write(`${msg}\n`);
180
+ process.exit(0);
181
+ }
@@ -0,0 +1,128 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { validateMemoryEvent, validateSharedMemory } from './memory-schema.mjs';
4
+ import { assertVaultPathSafe } from './vault-path-safety.mjs';
5
+ import { validateCore } from './validate-core.mjs';
6
+
7
+ function failedComponent(errors, extra = {}) {
8
+ return { ok: false, errors: Array.isArray(errors) ? errors : [errors], warnings: [], ...extra };
9
+ }
10
+
11
+ function readRequired(vaultBase, path, label) {
12
+ let checked;
13
+ try {
14
+ checked = assertVaultPathSafe(vaultBase, path, {
15
+ expectedType: 'file', label: `artefato ${label}`,
16
+ });
17
+ } catch (error) {
18
+ return { ok: false, error: `${label} inseguro: ${error?.message || error}` };
19
+ }
20
+ if (!checked.exists) return { ok: false, error: `${label} ausente: ${path}` };
21
+ try {
22
+ // Deliberately adjacent to the open performed by readFileSync.
23
+ checked = assertVaultPathSafe(vaultBase, checked.target, {
24
+ allowMissing: false, expectedType: 'file', label: `artefato ${label}`,
25
+ });
26
+ return { ok: true, content: readFileSync(checked.target, 'utf8') };
27
+ } catch (error) {
28
+ return { ok: false, error: `${label} ilegível: ${error?.message || error}` };
29
+ }
30
+ }
31
+
32
+ export function readProjectForValidation(vaultBase) {
33
+ const path = join(vaultBase, '.brain', 'PROJECT.json');
34
+ const read = readRequired(vaultBase, path, 'PROJECT.json');
35
+ if (!read.ok) return failedComponent(read.error, { projectId: '', path });
36
+ try {
37
+ const marker = JSON.parse(read.content);
38
+ if (!marker || typeof marker.projectId !== 'string' || !marker.projectId) {
39
+ return failedComponent('PROJECT.json inválido: projectId ausente.', { projectId: '', path });
40
+ }
41
+ return { ok: true, errors: [], warnings: [], projectId: marker.projectId, marker, path };
42
+ } catch (error) {
43
+ return failedComponent(`PROJECT.json contém JSON inválido: ${error?.message || error}`, { projectId: '', path });
44
+ }
45
+ }
46
+
47
+ /** Read and validate the append-only JSONL authority without repairing or mutating it. */
48
+ export function readLedgerForValidation(vaultBase, { projectId } = {}) {
49
+ const path = join(vaultBase, '.brain', 'MEMORY_EVENTS.jsonl');
50
+ const read = readRequired(vaultBase, path, 'MEMORY_EVENTS.jsonl');
51
+ if (!read.ok) return failedComponent(read.error, { events: [], eventIds: new Set(), path });
52
+
53
+ const errors = [];
54
+ const warnings = [];
55
+ const events = [];
56
+ const eventIds = new Set();
57
+ const normalized = read.content.replace(/\r\n/g, '\n');
58
+ const lines = normalized.split('\n');
59
+ const logicalLines = normalized === '' ? [] : (normalized.endsWith('\n') ? lines.slice(0, -1) : lines);
60
+ logicalLines.forEach((line, index) => {
61
+ if (!line.trim()) {
62
+ errors.push(`MEMORY_EVENTS.jsonl linha ${index + 1} está vazia no meio do ledger.`);
63
+ return;
64
+ }
65
+ let event;
66
+ try {
67
+ event = JSON.parse(line);
68
+ } catch (error) {
69
+ errors.push(`MEMORY_EVENTS.jsonl linha ${index + 1} contém JSON inválido/parcial: ${error?.message || error}`);
70
+ return;
71
+ }
72
+ const validation = validateMemoryEvent(event, projectId ? { projectId } : {});
73
+ for (const error of validation.errors) errors.push(`MEMORY_EVENTS.jsonl linha ${index + 1}: ${error}`);
74
+ for (const warning of validation.warnings) warnings.push(`MEMORY_EVENTS.jsonl linha ${index + 1}: ${warning}`);
75
+ if (typeof event?.event_id === 'string' && event.event_id) {
76
+ if (eventIds.has(event.event_id)) errors.push(`MEMORY_EVENTS.jsonl event_id duplicado: ${event.event_id}.`);
77
+ eventIds.add(event.event_id);
78
+ }
79
+ events.push(event);
80
+ });
81
+
82
+ return {
83
+ ok: errors.length === 0,
84
+ errors,
85
+ warnings,
86
+ events,
87
+ eventIds,
88
+ lineCount: logicalLines.length,
89
+ path,
90
+ };
91
+ }
92
+
93
+ function validateCoreArtifact(vaultBase) {
94
+ const path = join(vaultBase, '.brain', 'CORE.md');
95
+ const read = readRequired(vaultBase, path, 'CORE.md');
96
+ if (!read.ok) return failedComponent(read.error, { lineCount: 0, path });
97
+ return { ...validateCore(read.content), path, content: read.content };
98
+ }
99
+
100
+ function validateSharedArtifact(vaultBase, eventIds) {
101
+ const path = join(vaultBase, '.brain', 'SHARED_MEMORY.md');
102
+ const read = readRequired(vaultBase, path, 'SHARED_MEMORY.md');
103
+ if (!read.ok) return failedComponent(read.error, { path });
104
+ return { ...validateSharedMemory(read.content, { eventIds }), path, content: read.content };
105
+ }
106
+
107
+ export function combineMemoryResults({ project, core, ledger, shared }) {
108
+ const components = { project, core, ledger, shared };
109
+ const errors = [];
110
+ const warnings = [];
111
+ for (const [name, result] of Object.entries(components)) {
112
+ for (const error of result?.errors || []) errors.push(`${name}: ${error}`);
113
+ for (const warning of result?.warnings || []) warnings.push(`${name}: ${warning}`);
114
+ }
115
+ return { ok: errors.length === 0, errors, warnings, ...components };
116
+ }
117
+
118
+ /**
119
+ * Validate the v2 local memory bundle as a read-only composition. Missing/corrupt
120
+ * artifacts remain explicit failures; they are never silently treated as empty.
121
+ */
122
+ export function validateMemoryBundle(vaultBase) {
123
+ const project = readProjectForValidation(vaultBase);
124
+ const core = validateCoreArtifact(vaultBase);
125
+ const ledger = readLedgerForValidation(vaultBase, { projectId: project.projectId });
126
+ const shared = validateSharedArtifact(vaultBase, ledger.eventIds);
127
+ return combineMemoryResults({ project, core, ledger, shared });
128
+ }
package/src/memory.mjs CHANGED
@@ -295,6 +295,14 @@ function attemptFingerprint(attempt) {
295
295
  return hash(canonicalMemoryJson(attempt || null));
296
296
  }
297
297
 
298
+ function memoryCheckpointFingerprint(entry) {
299
+ const present = Object.prototype.hasOwnProperty.call(entry || {}, 'memory_checkpoint');
300
+ return hash(canonicalMemoryJson({
301
+ present,
302
+ checkpoint: present ? (entry.memory_checkpoint ?? null) : null,
303
+ }));
304
+ }
305
+
298
306
  function matchingAppliedReconciliation(entry, request) {
299
307
  const attempt = entry?.last_memory_attempt;
300
308
  if (attempt?.memory_mode !== 'v2' || attempt?.state !== 'skipped' || attempt?.disposition !== 'superseded') return null;
@@ -723,27 +731,81 @@ function checkpointShape(checkpoint) {
723
731
  && typeof checkpoint.state_hash === 'string' && checkpoint.state_hash;
724
732
  }
725
733
 
726
- function legacyCheckpointMigration(sessionId, attempt, authority, fullReplay) {
734
+ function legacyEventOrder(left, right) {
735
+ return (Number(left.base_revision ?? 0) - Number(right.base_revision ?? 0))
736
+ || String(left.effective_at || left.observed_at).localeCompare(String(right.effective_at || right.observed_at))
737
+ || Number(left.turn_sequence ?? 0) - Number(right.turn_sequence ?? 0)
738
+ || String(left.event_id).localeCompare(String(right.event_id));
739
+ }
740
+
741
+ function historicalAssertOnlyCheckpoint(vault, attempt, authority, checkpoint) {
742
+ const requiredEventIds = Array.isArray(attempt?.event_ids) ? [...attempt.event_ids] : [];
743
+ if (!requiredEventIds.includes(checkpoint.event_cursor)) return null;
744
+
745
+ const cursorIndex = authority.ledgerEvents
746
+ .findIndex((event) => event.event_id === checkpoint.event_cursor);
747
+ if (cursorIndex < 0) return null;
748
+ const prefix = authority.ledgerEvents.slice(0, cursorIndex + 1);
749
+ const prefixIds = new Set(prefix.map((event) => event.event_id));
750
+ if (requiredEventIds.some((eventId) => !prefixIds.has(eventId))) return null;
751
+ if (prefix.some((event) => event.operation !== 'assert')) return null;
752
+
753
+ const firstByKey = [];
754
+ const priorByKey = new Map();
755
+ for (const event of [...prefix].sort(legacyEventOrder)) {
756
+ const prior = priorByKey.get(event.memory_key);
757
+ if (prior) {
758
+ const sameActivation = Boolean(event.canonical_session_id)
759
+ && event.canonical_session_id === prior.canonical_session_id
760
+ && event.activation_id === prior.activation_id;
761
+ if (!sameActivation || !Number.isInteger(event.turn_sequence)
762
+ || event.turn_sequence <= prior.turn_sequence) return null;
763
+ } else {
764
+ firstByKey.push(event);
765
+ }
766
+ priorByKey.set(event.memory_key, event);
767
+ }
768
+
769
+ const legacyState = deriveMemoryProjection(vault, firstByKey);
770
+ const currentPrefix = deriveMemoryProjection(vault, prefix);
771
+ if (legacyState.candidates.length || currentPrefix.candidates.length) return null;
772
+ const legacyCheckpoint = {
773
+ revision: legacyState.revision,
774
+ event_cursor: currentPrefix.eventCursor,
775
+ state_hash: legacyState.stateHash,
776
+ };
777
+ if (!sameCheckpoint(checkpoint, legacyCheckpoint)) return null;
778
+ return currentPrefix.checkpoint;
779
+ }
780
+
781
+ function legacyCheckpointMigration(vault, sessionId, entry, authority, fullReplay) {
782
+ const attempt = entry?.last_memory_attempt;
727
783
  const checkpoint = attempt?.checkpoint;
728
784
  const requiredEventIds = Array.isArray(attempt?.event_ids) ? [...attempt.event_ids] : [];
729
785
  if (!checkpointShape(checkpoint) || !requiredEventIds.length) return null;
786
+ if (entry?.memory_checkpoint !== undefined
787
+ && !sameCheckpoint(entry.memory_checkpoint, checkpoint)) return null;
730
788
  // The pre-physical format had no explicit causal_event_cursor: event_cursor itself
731
789
  // named the reducer's causal tail while revision/hash described the full authority
732
- // snapshot. Restricting automatic migration to that exact snapshot is both O(n) and
733
- // fail-closed: historical/ambiguous prefixes require explicit human reconciliation.
790
+ // snapshot. Historical prefixes are accepted only by the narrower assert-only proof below;
791
+ // every other ambiguous tuple remains fail-closed for explicit human reconciliation.
734
792
  if (checkpoint.causal_event_cursor !== undefined) return null;
735
793
  if (requiredEventIds.some((eventId) => !authority.ledgerById.has(eventId))) return null;
736
- if (checkpoint.revision !== fullReplay.revision
737
- || checkpoint.state_hash !== fullReplay.stateHash
738
- || checkpoint.event_cursor !== fullReplay.eventCursor) return null;
739
- if (sameCheckpoint(checkpoint, fullReplay.checkpoint)) return null;
794
+ const matchesFullReplay = checkpoint.revision === fullReplay.revision
795
+ && checkpoint.state_hash === fullReplay.stateHash
796
+ && checkpoint.event_cursor === fullReplay.eventCursor;
797
+ const nextCheckpoint = matchesFullReplay
798
+ ? fullReplay.checkpoint
799
+ : historicalAssertOnlyCheckpoint(vault, attempt, authority, checkpoint);
800
+ if (!nextCheckpoint || sameCheckpoint(checkpoint, nextCheckpoint)) return null;
740
801
 
741
802
  const proof = validateSuccessorProof({ bySessionId: sessionId }, attempt, authority);
742
803
  return {
743
804
  sessionId,
744
805
  expectedFingerprint: attemptFingerprint(attempt),
806
+ expectedMemoryCheckpointFingerprint: memoryCheckpointFingerprint(entry),
745
807
  originalCheckpoint: cloneJson(checkpoint),
746
- checkpoint: cloneJson(fullReplay.checkpoint),
808
+ checkpoint: cloneJson(nextCheckpoint),
747
809
  proof,
748
810
  };
749
811
  }
@@ -759,7 +821,7 @@ export function migrateLegacyMemoryCheckpoints(vault, {
759
821
  const fullReplay = deriveMemoryProjection(vault, authority.ledgerEvents);
760
822
  const plans = Object.entries(inspected.sessions || {})
761
823
  .map(([sessionId, entry]) => legacyCheckpointMigration(
762
- sessionId, entry?.last_memory_attempt, authority, fullReplay,
824
+ vault, sessionId, entry, authority, fullReplay,
763
825
  ))
764
826
  .filter(Boolean);
765
827
  assertAuthorityMatches(expectedAuthority, readMemoryAuthority(vault));
@@ -773,10 +835,14 @@ export function migrateLegacyMemoryCheckpoints(vault, {
773
835
  return mutateSessionRegistry(vault, (registry) => {
774
836
  // Validate every CAS before changing the first entry, making the batch atomic.
775
837
  for (const plan of plans) {
776
- const attempt = registry.sessions?.[plan.sessionId]?.last_memory_attempt;
838
+ const entry = registry.sessions?.[plan.sessionId];
839
+ const attempt = entry?.last_memory_attempt;
777
840
  if (attemptFingerprint(attempt) !== plan.expectedFingerprint) {
778
841
  throw new Error(`CAS perdido: checkpoint da sessão ${plan.sessionId} mudou durante a migração estrutural.`);
779
842
  }
843
+ if (memoryCheckpointFingerprint(entry) !== plan.expectedMemoryCheckpointFingerprint) {
844
+ throw new Error(`CAS perdido: memory_checkpoint da sessão ${plan.sessionId} mudou durante a migração estrutural.`);
845
+ }
780
846
  }
781
847
 
782
848
  const path = registryPath(vault);
@@ -795,7 +861,7 @@ export function migrateLegacyMemoryCheckpoints(vault, {
795
861
 
796
862
  for (const plan of plans) {
797
863
  const entry = registry.sessions[plan.sessionId];
798
- const reconciliationId = `memcp-${hash(`${plan.sessionId}\0${plan.expectedFingerprint}\0${canonicalMemoryJson(plan.checkpoint)}`).slice(0, 20)}`;
864
+ const reconciliationId = `memcp-${hash(`${plan.sessionId}\0${plan.expectedFingerprint}\0${plan.expectedMemoryCheckpointFingerprint}\0${canonicalMemoryJson(plan.checkpoint)}`).slice(0, 20)}`;
799
865
  entry.memory_reconciliations = [
800
866
  ...(Array.isArray(entry.memory_reconciliations) ? entry.memory_reconciliations : []),
801
867
  {
@@ -1,133 +1 @@
1
- export const OPERATING_PROFILES = Object.freeze([
2
- 'OFF',
3
- 'FLOW',
4
- 'GUIDE',
5
- 'GOVERN',
6
- 'ASSURE',
7
- ]);
8
- export const DEFAULT_OPERATING_PROFILE = 'GOVERN';
9
-
10
- const PROFILE_SET = new Set(OPERATING_PROFILES);
11
-
12
- function policy(profile, route, options) {
13
- return Object.freeze({
14
- profile,
15
- route: Object.freeze(route),
16
- keepCore: true,
17
- ...options,
18
- });
19
- }
20
-
21
- export const OPERATING_PROFILE_POLICIES = Object.freeze({
22
- OFF: policy('OFF', ['LLM'], {
23
- harness: false,
24
- contract: 'native',
25
- requiresChange: false,
26
- requiresReview: false,
27
- requiresConfirmation: false,
28
- }),
29
- FLOW: policy('FLOW', ['E', 'V'], {
30
- harness: true,
31
- contract: 'flow',
32
- requiresChange: false,
33
- requiresReview: false,
34
- requiresConfirmation: false,
35
- }),
36
- GUIDE: policy('GUIDE', ['P', 'E', 'V'], {
37
- harness: true,
38
- contract: 'simple-change',
39
- requiresChange: true,
40
- requiresReview: false,
41
- requiresConfirmation: false,
42
- }),
43
- GOVERN: policy('GOVERN', ['P', 'R', 'E', 'V'], {
44
- harness: true,
45
- contract: 'change',
46
- requiresChange: true,
47
- requiresReview: true,
48
- requiresConfirmation: false,
49
- }),
50
- ASSURE: policy('ASSURE', ['P', 'R', 'E', 'V', 'C'], {
51
- harness: true,
52
- contract: 'change',
53
- requiresChange: true,
54
- requiresReview: true,
55
- requiresConfirmation: true,
56
- }),
57
- });
58
-
59
- function invalidProfileError(value) {
60
- const rendered = typeof value === 'string' ? `"${value}"` : String(value);
61
- const error = new Error(
62
- `Perfil de Operação inválido: ${rendered}. Use ${OPERATING_PROFILES.join(', ')}.`,
63
- );
64
- error.code = 'WENDKEEP_OPERATING_PROFILE_INVALID';
65
- return error;
66
- }
67
-
68
- function canonicalProfile(value) {
69
- if (typeof value !== 'string') return '';
70
- return value.trim().toUpperCase();
71
- }
72
-
73
- export function normalizeOperatingProfile(value, { strict = false } = {}) {
74
- const normalized = canonicalProfile(value);
75
- if (PROFILE_SET.has(normalized)) return normalized;
76
- if (strict) throw invalidProfileError(value);
77
- return DEFAULT_OPERATING_PROFILE;
78
- }
79
-
80
- export function resolveOperatingProfile(config = {}) {
81
- const harness = config && typeof config === 'object' && !Array.isArray(config)
82
- && config.harness && typeof config.harness === 'object' && !Array.isArray(config.harness)
83
- ? config.harness
84
- : null;
85
- const configured = !!harness && Object.prototype.hasOwnProperty.call(harness, 'profile');
86
- if (!configured) {
87
- return {
88
- profile: DEFAULT_OPERATING_PROFILE,
89
- source: 'default',
90
- valid: true,
91
- configured: false,
92
- raw: null,
93
- };
94
- }
95
-
96
- const raw = harness.profile;
97
- const normalized = canonicalProfile(raw);
98
- if (PROFILE_SET.has(normalized)) {
99
- return {
100
- profile: normalized,
101
- source: 'project-binding',
102
- valid: true,
103
- configured: true,
104
- raw,
105
- };
106
- }
107
- return {
108
- profile: DEFAULT_OPERATING_PROFILE,
109
- source: 'default-invalid',
110
- valid: false,
111
- configured: true,
112
- raw,
113
- };
114
- }
115
-
116
- export function operatingProfilePolicy(value) {
117
- return OPERATING_PROFILE_POLICIES[normalizeOperatingProfile(value)];
118
- }
119
-
120
- export function setOperatingProfile(config = {}, value) {
121
- const profile = normalizeOperatingProfile(value, { strict: true });
122
- const base = config && typeof config === 'object' && !Array.isArray(config) ? config : {};
123
- const harness = base.harness && typeof base.harness === 'object' && !Array.isArray(base.harness)
124
- ? base.harness
125
- : {};
126
- return {
127
- ...base,
128
- harness: {
129
- ...harness,
130
- profile,
131
- },
132
- };
133
- }
1
+ export * from '../packages/harness/src/operating-profile.mjs';