wendkeep 0.73.0 → 0.75.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 +66 -0
- package/README.en.md +13 -9
- package/README.md +13 -9
- package/docs/en/commands/memory.md +16 -1
- package/docs/en/commands/observer.md +29 -11
- package/docs/en/commands/operating-profiles.md +1 -1
- package/docs/pt-BR/commands/memory.md +16 -1
- package/docs/pt-BR/commands/observer.md +28 -11
- package/docs/pt-BR/commands/operating-profiles.md +1 -1
- package/hooks/brain-core.mjs +2 -0
- package/hooks/brain-recall.mjs +5 -1
- package/hooks/evidence-context.mjs +41 -0
- package/hooks/evidence-recall.mjs +1 -0
- package/hooks/memory-scope.mjs +1 -0
- package/package.json +2 -2
- package/packages/cli/src/index.mjs +2 -2
- package/packages/integrations/src/host-hooks.mjs +1 -0
- package/packages/vault/src/evidence-recall.mjs +343 -0
- package/packages/vault/src/index.mjs +2 -0
- package/packages/vault/src/memory-handoff.mjs +58 -3
- package/packages/vault/src/memory-schema.mjs +12 -2
- package/packages/vault/src/memory-scope.mjs +119 -0
- package/packages/vault/src/memory-store.mjs +86 -24
- package/schema/observer/004-evidence-recall.sql +25 -0
- package/schema/observer/005-project-scoped-identities.sql +217 -0
- package/src/change.mjs +41 -1
- package/src/doctor.mjs +5 -0
- package/src/init.mjs +2 -2
- package/src/memory.mjs +95 -2
- package/src/note.mjs +8 -1
- package/src/observer-publish.mjs +9 -34
- package/src/observer-server.mjs +38 -63
- package/src/observer-sql-migrate.mjs +1 -1
- package/src/observer-sql-publish.mjs +372 -12
- package/src/observer-sql-store.mjs +248 -32
- package/src/observer-store.mjs +15 -3
- package/src/observer.mjs +104 -14
- package/src/taxonomy.mjs +4 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// UserPromptSubmit: bounded, read-only retrieval from the local evidence index.
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { readHookInput, writeHookOutput } from './obsidian-common.mjs';
|
|
5
|
+
import { loadEvidenceIndex, recallEvidence, renderEvidenceContext } from './evidence-recall.mjs';
|
|
6
|
+
import { sanitizeMemoryText } from './memory-schema.mjs';
|
|
7
|
+
import { resolveHookOperatingProfile } from './operating-profile-runtime.mjs';
|
|
8
|
+
import { isBootstrapPrompt } from '../packages/integrations/src/prompt-content.mjs';
|
|
9
|
+
|
|
10
|
+
export function buildPromptEvidenceContext(vaultBase, prompt, {
|
|
11
|
+
topK = 3, maxBytes = 3072, rows = null,
|
|
12
|
+
} = {}) {
|
|
13
|
+
const query = sanitizeMemoryText(String(prompt || '')).trim();
|
|
14
|
+
if (!query || isBootstrapPrompt(query)) return '';
|
|
15
|
+
const evidence = rows || loadEvidenceIndex(vaultBase);
|
|
16
|
+
if (!evidence.length) return '';
|
|
17
|
+
return sanitizeMemoryText(renderEvidenceContext(
|
|
18
|
+
recallEvidence(evidence, query, { topK }),
|
|
19
|
+
{ maxBytes },
|
|
20
|
+
));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
24
|
+
try {
|
|
25
|
+
const input = readHookInput();
|
|
26
|
+
const runtime = resolveHookOperatingProfile({ input });
|
|
27
|
+
if (runtime.bindingError) {
|
|
28
|
+
writeHookOutput({});
|
|
29
|
+
} else {
|
|
30
|
+
const context = buildPromptEvidenceContext(
|
|
31
|
+
runtime.vaultBase,
|
|
32
|
+
input.prompt || input.user_prompt || '',
|
|
33
|
+
);
|
|
34
|
+
writeHookOutput(context ? {
|
|
35
|
+
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context },
|
|
36
|
+
} : {});
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
writeHookOutput({});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../packages/vault/src/evidence-recall.mjs';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../packages/vault/src/memory-scope.mjs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.75.0",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"node": ">=18"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
44
|
+
"check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-scope.mjs && node --check packages/vault/src/evidence-recall.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
45
45
|
"test": "node --test --test-concurrency=2",
|
|
46
46
|
"test:core": "node scripts/run-scope.mjs core",
|
|
47
47
|
"release": "node scripts/release.mjs",
|
|
@@ -49,7 +49,7 @@ Usage:
|
|
|
49
49
|
cannot replace itself. · --vault P · --profile <name> · --yes.
|
|
50
50
|
|
|
51
51
|
wendkeep doctor [--vault P] Health check. --scope core|runtime · --strict for CI/release.
|
|
52
|
-
wendkeep observer <sub> Local multi-project Observer: serve | register | publish | status.
|
|
52
|
+
wendkeep observer <sub> Local multi-project Observer: serve | register | publish | reconcile | status.
|
|
53
53
|
wendkeep change <sub> Change lifecycle: new [--simple|--guide] | use | bind <slug> --session <id> | continue | list | show |
|
|
54
54
|
status | done <id> | undone <id> | diff | archive [--force] | abandon | relink | backlink.
|
|
55
55
|
archive exige verdict (rode verify --deep); abandon descarta sem ADR.
|
|
@@ -104,7 +104,7 @@ Usage:
|
|
|
104
104
|
wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
|
|
105
105
|
wendkeep memory curate Guide one semantic conflict at a time in an interactive terminal.
|
|
106
106
|
Every promote/reject requires confirmation; --vault P.
|
|
107
|
-
wendkeep memory <sub> Shared memory v2: status | candidates [--active] | curate | migrate [--apply] | repair |
|
|
107
|
+
wendkeep memory <sub> Shared memory v2: status | candidates [--active] | curate | migrate [--apply] | rescope [--apply] | repair |
|
|
108
108
|
recover-attempt <session> [--apply] |
|
|
109
109
|
reconcile <session> --by-session <session> --reason <text> [--apply] |
|
|
110
110
|
promote <candidate> [--event <event-id>] | reject <candidate>. --vault P.
|
|
@@ -19,6 +19,7 @@ export const SESSION_HOOKS = [
|
|
|
19
19
|
{ event: 'Stop', matcher: null, name: 'session-stop', timeout: 60, codex: true, statusMessage: 'wendkeep: writing session checkpoint' },
|
|
20
20
|
{ event: 'Stop', matcher: null, name: 'observer-publish', timeout: 5, order: 20, codex: true, statusMessage: 'wendkeep: publishing local observer snapshot' },
|
|
21
21
|
{ event: 'UserPromptSubmit', matcher: null, name: 'session-ensure', timeout: 30, codex: true, statusMessage: 'wendkeep: ensuring active session' },
|
|
22
|
+
{ event: 'UserPromptSubmit', matcher: null, name: 'evidence-context', timeout: 10, order: 5, codex: true, statusMessage: 'wendkeep: retrieving relevant evidence' },
|
|
22
23
|
// Capture an interactive decision (AskUserQuestion) — options + the user's choice — into 04-Decisões.
|
|
23
24
|
// codex: AskUserQuestion is a Claude-only tool; there is nothing to match on.
|
|
24
25
|
{ event: 'PostToolUse', matcher: 'AskUserQuestion', name: 'decision-capture', timeout: 15, statusMessage: 'wendkeep: recording decision' },
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, join, relative } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const EVIDENCE_INDEX_FILE = 'EVIDENCE_INDEX.jsonl';
|
|
6
|
+
export const EVIDENCE_INDEX_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
const STOP_WORDS = new Set([
|
|
9
|
+
'a', 'an', 'and', 'as', 'at', 'da', 'das', 'de', 'do', 'dos', 'e', 'em', 'for', 'in',
|
|
10
|
+
'is', 'o', 'os', 'or', 'para', 'por', 'the', 'to', 'um', 'uma', 'with', 'com', 'que',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function hash(value) {
|
|
14
|
+
return createHash('sha256').update(String(value ?? '')).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function cleanText(value) {
|
|
18
|
+
return String(value ?? '').replace(/\r\n/g, '\n').replace(/[\t ]+/g, ' ').trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeRecallText(value) {
|
|
22
|
+
return cleanText(value).normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function recallTerms(value) {
|
|
26
|
+
return normalizeRecallText(value).match(/[\p{L}\p{N}]+(?:[._-][\p{L}\p{N}]+)*/gu)
|
|
27
|
+
?.filter((term) => term.length > 1 && !STOP_WORDS.has(term)) || [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseFrontmatter(content) {
|
|
31
|
+
const match = String(content || '').match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
32
|
+
if (!match) return { data: {}, body: String(content || '') };
|
|
33
|
+
const data = {};
|
|
34
|
+
for (const line of match[1].split('\n')) {
|
|
35
|
+
const item = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
|
|
36
|
+
if (!item) continue;
|
|
37
|
+
data[item[1]] = item[2].replace(/^['"]|['"]$/g, '');
|
|
38
|
+
}
|
|
39
|
+
return { data, body: String(content || '').slice(match[0].length) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function inferredChangeSlug(logicalPath, metadata) {
|
|
43
|
+
const explicit = metadata.change_slug || metadata.change || '';
|
|
44
|
+
if (explicit) return String(explicit);
|
|
45
|
+
const segments = String(logicalPath || '').replaceAll('\\', '/').split('/');
|
|
46
|
+
const at = segments.findIndex((segment) => /^(?:08-Mudan[cç]as|08-Changes)$/i.test(segment));
|
|
47
|
+
return at >= 0 ? String(segments[at + 1] || '').replace(/^\d{4}-\d{2}-\d{2}-/, '') : '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function entityType(logicalPath, heading, block, fallback = 'document') {
|
|
51
|
+
const signal = normalizeRecallText(`${logicalPath} ${heading}`);
|
|
52
|
+
const headingSignal = normalizeRecallText(heading);
|
|
53
|
+
if (/^\s*[-*]\s+\[[ xX]\]/m.test(block) || /\b(tasks?|tarefas?)\b/.test(headingSignal)) return 'task';
|
|
54
|
+
if (/\b(decisions?|decisoes?|adr)\b/.test(signal) || /(^|\/)04-/.test(logicalPath)) return 'decision';
|
|
55
|
+
if (/\b(requirements?|requisitos?|specs?|contratos?)\b/.test(signal) || /(^|\/)07-/.test(logicalPath)) return 'requirement';
|
|
56
|
+
if (/\b(evidence|evidencia|verdict|teste|test)\b/.test(signal)) return 'evidence';
|
|
57
|
+
if (/\b(session|sessao)\b/.test(signal) || /(^|\/)02-/.test(logicalPath)) return 'session';
|
|
58
|
+
return String(fallback || 'document');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function authorityFor(logicalPath, metadata, kind) {
|
|
62
|
+
if (['verified', 'reported', 'candidate'].includes(metadata.authority)) return metadata.authority;
|
|
63
|
+
if (kind === 'decision' || kind === 'requirement' || kind === 'evidence'
|
|
64
|
+
|| /(^|\/)(?:04-|07-)/.test(logicalPath)) return 'verified';
|
|
65
|
+
return kind === 'session' ? 'reported' : 'candidate';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validityFor(metadata, block) {
|
|
69
|
+
const explicit = normalizeRecallText(metadata.validity || metadata.status || '');
|
|
70
|
+
if (/superseded|superado|deprecated|obsoleto|rejected|abandon/.test(explicit)) return 'superseded';
|
|
71
|
+
if (/closed|done|archived|active|ativo|accepted|complete/.test(explicit)) return 'active';
|
|
72
|
+
if (/\b(?:superseded|superado|obsoleto)\b/i.test(block)) return 'superseded';
|
|
73
|
+
return 'active';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function observedAt(metadata) {
|
|
77
|
+
const raw = metadata.observed_at || metadata.updated_at || metadata.ended_at
|
|
78
|
+
|| metadata.date || metadata.created_at || '';
|
|
79
|
+
if (!raw) return new Date(0).toISOString();
|
|
80
|
+
const parsed = Date.parse(raw);
|
|
81
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : new Date(0).toISOString();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function splitLongBlock(block, maxChars = 1200) {
|
|
85
|
+
if (block.length <= maxChars) return [block];
|
|
86
|
+
const out = [];
|
|
87
|
+
let rest = block;
|
|
88
|
+
while (rest.length > maxChars) {
|
|
89
|
+
let cut = rest.lastIndexOf(' ', maxChars);
|
|
90
|
+
if (cut < Math.floor(maxChars * 0.6)) cut = maxChars;
|
|
91
|
+
out.push(rest.slice(0, cut).trim());
|
|
92
|
+
rest = rest.slice(cut).trim();
|
|
93
|
+
}
|
|
94
|
+
if (rest) out.push(rest);
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function indexableBlockParts(block) {
|
|
99
|
+
const maxIndexedChars = 4 * 1024 * 1024;
|
|
100
|
+
if (block.length <= maxIndexedChars) return splitLongBlock(block);
|
|
101
|
+
const samples = 256;
|
|
102
|
+
const sampleChars = Math.floor(maxIndexedChars / samples);
|
|
103
|
+
const stride = block.length / samples;
|
|
104
|
+
return Array.from({ length: samples }, (_, index) => {
|
|
105
|
+
const start = Math.min(block.length - sampleChars, Math.floor(index * stride));
|
|
106
|
+
return cleanText(block.slice(Math.max(0, start), Math.max(0, start) + sampleChars));
|
|
107
|
+
}).filter(Boolean);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function chunkMarkdownDocument({
|
|
111
|
+
projectId = '', logicalPath = '', content = '', metadata = {}, entityType: fallbackType = 'document',
|
|
112
|
+
} = {}) {
|
|
113
|
+
const parsed = parseFrontmatter(content);
|
|
114
|
+
const meta = { ...parsed.data, ...(metadata || {}) };
|
|
115
|
+
const lines = parsed.body.replace(/\r\n/g, '\n').split('\n');
|
|
116
|
+
const title = cleanText(meta.title || lines.find((line) => /^#\s+/.test(line))?.replace(/^#\s+/, '')
|
|
117
|
+
|| basename(logicalPath).replace(/\.md$/i, ''));
|
|
118
|
+
let heading = title;
|
|
119
|
+
let buffer = [];
|
|
120
|
+
const blocks = [];
|
|
121
|
+
let inFence = false;
|
|
122
|
+
|
|
123
|
+
const flush = () => {
|
|
124
|
+
const block = cleanText(buffer.join('\n'));
|
|
125
|
+
if (block) indexableBlockParts(block).forEach((part) => blocks.push({ heading, content: part }));
|
|
126
|
+
buffer = [];
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
if (/^```/.test(line.trim())) inFence = !inFence;
|
|
131
|
+
const headingMatch = !inFence && line.match(/^#{1,6}\s+(.+?)\s*$/);
|
|
132
|
+
if (headingMatch) {
|
|
133
|
+
flush();
|
|
134
|
+
heading = cleanText(headingMatch[1]);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!inFence && !line.trim()) flush();
|
|
138
|
+
else buffer.push(line);
|
|
139
|
+
}
|
|
140
|
+
flush();
|
|
141
|
+
|
|
142
|
+
const common = {
|
|
143
|
+
index_version: EVIDENCE_INDEX_VERSION,
|
|
144
|
+
project_id: String(projectId || ''),
|
|
145
|
+
logical_path: String(logicalPath || '').replaceAll('\\', '/'),
|
|
146
|
+
title,
|
|
147
|
+
change_slug: inferredChangeSlug(logicalPath, meta),
|
|
148
|
+
session_id: String(meta.session_id || ''),
|
|
149
|
+
work_session_id: String(meta.work_session_id || ''),
|
|
150
|
+
observed_at: observedAt(meta),
|
|
151
|
+
};
|
|
152
|
+
return blocks.map((block, ordinal) => {
|
|
153
|
+
const kind = entityType(common.logical_path, block.heading, block.content, meta.entity_type || fallbackType);
|
|
154
|
+
return {
|
|
155
|
+
...common,
|
|
156
|
+
chunk_id: `chunk-${hash(`${projectId}\0${common.logical_path}\0${block.heading}\0${ordinal}\0${block.content}`).slice(0, 24)}`,
|
|
157
|
+
heading: block.heading,
|
|
158
|
+
entity_type: kind,
|
|
159
|
+
authority: authorityFor(common.logical_path, meta, kind),
|
|
160
|
+
validity: validityFor(meta, block.content),
|
|
161
|
+
ordinal,
|
|
162
|
+
content: block.content,
|
|
163
|
+
content_hash: hash(block.content),
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function walkMarkdown(root, dir = root, found = []) {
|
|
169
|
+
let entries = [];
|
|
170
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return found; }
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (entry.name === '.brain' || entry.name === '.obsidian' || entry.name === 'node_modules') continue;
|
|
173
|
+
const path = join(dir, entry.name);
|
|
174
|
+
if (entry.isDirectory()) walkMarkdown(root, path, found);
|
|
175
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) found.push(path);
|
|
176
|
+
}
|
|
177
|
+
return found;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function projectIdForVault(vaultBase) {
|
|
181
|
+
try {
|
|
182
|
+
return String(JSON.parse(readFileSync(join(vaultBase, '.brain', 'PROJECT.json'), 'utf8')).projectId || '');
|
|
183
|
+
} catch {
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function buildEvidenceIndex(vaultBase) {
|
|
189
|
+
const projectId = projectIdForVault(vaultBase);
|
|
190
|
+
const chunks = [];
|
|
191
|
+
for (const path of walkMarkdown(vaultBase)) {
|
|
192
|
+
let content = '';
|
|
193
|
+
try { content = readFileSync(path, 'utf8'); } catch { continue; }
|
|
194
|
+
chunks.push(...chunkMarkdownDocument({
|
|
195
|
+
projectId,
|
|
196
|
+
logicalPath: relative(vaultBase, path).replaceAll('\\', '/'),
|
|
197
|
+
content,
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
chunks.sort((left, right) => left.logical_path.localeCompare(right.logical_path)
|
|
201
|
+
|| left.ordinal - right.ordinal || left.chunk_id.localeCompare(right.chunk_id));
|
|
202
|
+
const output = chunks.map((chunk) => JSON.stringify(chunk)).join('\n') + (chunks.length ? '\n' : '');
|
|
203
|
+
writeFileSync(join(vaultBase, '.brain', EVIDENCE_INDEX_FILE), output, 'utf8');
|
|
204
|
+
return chunks;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function loadEvidenceIndex(vaultBase) {
|
|
208
|
+
const path = join(vaultBase, '.brain', EVIDENCE_INDEX_FILE);
|
|
209
|
+
if (!existsSync(path)) return [];
|
|
210
|
+
try {
|
|
211
|
+
return readFileSync(path, 'utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
212
|
+
} catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function occurrences(terms, text) {
|
|
218
|
+
const tokens = recallTerms(text);
|
|
219
|
+
const counts = new Map();
|
|
220
|
+
for (const token of tokens) counts.set(token, (counts.get(token) || 0) + 1);
|
|
221
|
+
return terms.reduce((sum, term) => sum + (counts.get(term) || 0), 0);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function excerptFor(content, query, terms, max = 360) {
|
|
225
|
+
const raw = cleanText(content);
|
|
226
|
+
const normalized = normalizeRecallText(raw);
|
|
227
|
+
const phrase = normalizeRecallText(query);
|
|
228
|
+
let at = phrase ? normalized.indexOf(phrase) : -1;
|
|
229
|
+
if (at < 0) at = terms.map((term) => normalized.indexOf(term)).filter((index) => index >= 0).sort((a, b) => a - b)[0] ?? 0;
|
|
230
|
+
const start = Math.max(0, at - Math.floor(max * 0.3));
|
|
231
|
+
const end = Math.min(raw.length, start + max);
|
|
232
|
+
return `${start > 0 ? '…' : ''}${raw.slice(start, end).trim()}${end < raw.length ? '…' : ''}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function recencyScore(observed, now) {
|
|
236
|
+
const instant = Date.parse(observed || '');
|
|
237
|
+
if (!Number.isFinite(instant)) return 0;
|
|
238
|
+
const days = Math.max(0, (now - instant) / 86_400_000);
|
|
239
|
+
return Math.max(0, 1.5 * (1 - Math.min(days, 365) / 365));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function recallEvidence(rows, query, { topK = 5, now = Date.now() } = {}) {
|
|
243
|
+
const terms = [...new Set(recallTerms(query))];
|
|
244
|
+
if (!terms.length || !Array.isArray(rows) || !rows.length) return [];
|
|
245
|
+
const docs = rows.map((row) => ({
|
|
246
|
+
row,
|
|
247
|
+
contentTerms: recallTerms(row.content),
|
|
248
|
+
allTerms: new Set(recallTerms(`${row.title} ${row.heading} ${row.logical_path} ${row.content}`)),
|
|
249
|
+
}));
|
|
250
|
+
const df = new Map(terms.map((term) => [term, docs.filter((doc) => doc.allTerms.has(term)).length]));
|
|
251
|
+
const averageLength = docs.reduce((sum, doc) => sum + doc.contentTerms.length, 0) / docs.length || 1;
|
|
252
|
+
const phrase = normalizeRecallText(query);
|
|
253
|
+
const scored = docs.map(({ row, contentTerms, allTerms }) => {
|
|
254
|
+
let score = 0;
|
|
255
|
+
for (const term of terms) {
|
|
256
|
+
const frequency = occurrences([term], row.content);
|
|
257
|
+
const idf = Math.log(1 + ((docs.length - (df.get(term) || 0) + 0.5) / ((df.get(term) || 0) + 0.5)));
|
|
258
|
+
if (frequency) score += idf * ((frequency * 2.2) / (frequency + 1.2 * (0.25 + 0.75 * contentTerms.length / averageLength)));
|
|
259
|
+
if (recallTerms(row.title).includes(term)) score += idf * 3;
|
|
260
|
+
if (recallTerms(row.heading).includes(term)) score += idf * 2.5;
|
|
261
|
+
if (recallTerms(row.logical_path).includes(term)) score += idf * 1.5;
|
|
262
|
+
}
|
|
263
|
+
if (phrase && normalizeRecallText(`${row.title} ${row.heading} ${row.content}`).includes(phrase)) score += 6;
|
|
264
|
+
if (row.authority === 'verified') score += 2;
|
|
265
|
+
else if (row.authority === 'reported') score += 1;
|
|
266
|
+
if (row.validity === 'superseded') score -= 8;
|
|
267
|
+
else if (row.validity === 'active') score += 1;
|
|
268
|
+
score += recencyScore(row.observed_at, now);
|
|
269
|
+
const matchedTerms = terms.filter((term) => allTerms.has(term));
|
|
270
|
+
return {
|
|
271
|
+
...row,
|
|
272
|
+
score: Number(score.toFixed(6)),
|
|
273
|
+
matched_terms: matchedTerms,
|
|
274
|
+
excerpt: excerptFor(row.content, query, matchedTerms),
|
|
275
|
+
};
|
|
276
|
+
}).filter((row) => row.matched_terms.length && row.score > 0)
|
|
277
|
+
.sort((left, right) => right.score - left.score
|
|
278
|
+
|| String(right.observed_at).localeCompare(String(left.observed_at))
|
|
279
|
+
|| left.logical_path.localeCompare(right.logical_path));
|
|
280
|
+
|
|
281
|
+
const selected = [];
|
|
282
|
+
const perSource = new Map();
|
|
283
|
+
for (const row of scored) {
|
|
284
|
+
const count = perSource.get(row.logical_path) || 0;
|
|
285
|
+
if (count >= 1 && scored.some((candidate) => !perSource.has(candidate.logical_path))) continue;
|
|
286
|
+
selected.push(row);
|
|
287
|
+
perSource.set(row.logical_path, count + 1);
|
|
288
|
+
if (selected.length >= topK) break;
|
|
289
|
+
}
|
|
290
|
+
if (selected.length < topK) {
|
|
291
|
+
for (const row of scored) {
|
|
292
|
+
if (selected.some((item) => item.chunk_id === row.chunk_id)) continue;
|
|
293
|
+
selected.push(row);
|
|
294
|
+
if (selected.length >= topK) break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return selected;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function renderEvidenceContext(results, { maxBytes = 3072 } = {}) {
|
|
301
|
+
const lines = ['<wk_evidence_recall>'];
|
|
302
|
+
for (const [index, item] of results.entries()) {
|
|
303
|
+
const entry = [
|
|
304
|
+
`${index + 1}. ${item.title || item.logical_path} — ${item.heading || '(sem heading)'}`,
|
|
305
|
+
` ${item.excerpt}`,
|
|
306
|
+
` source:${item.logical_path} authority:${item.authority} validity:${item.validity} as_of:${item.observed_at}`,
|
|
307
|
+
];
|
|
308
|
+
const candidate = [...lines, ...entry, '</wk_evidence_recall>'].join('\n');
|
|
309
|
+
if (Buffer.byteLength(candidate, 'utf8') > maxBytes) break;
|
|
310
|
+
lines.push(...entry);
|
|
311
|
+
}
|
|
312
|
+
lines.push('</wk_evidence_recall>');
|
|
313
|
+
return lines.length === 2 ? '' : lines.join('\n');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function benchmarkEvidenceRecall(rows, cases, { topK = 5, now = Date.now() } = {}) {
|
|
317
|
+
let reciprocal = 0;
|
|
318
|
+
let recalled = 0;
|
|
319
|
+
let stale = 0;
|
|
320
|
+
let evidenceCorrect = 0;
|
|
321
|
+
let handoffs = 0;
|
|
322
|
+
let handoffsFound = 0;
|
|
323
|
+
for (const item of cases) {
|
|
324
|
+
const results = recallEvidence(rows, item.query, { topK, now });
|
|
325
|
+
const rank = results.findIndex((row) => row.chunk_id === item.expected_chunk_id
|
|
326
|
+
|| row.logical_path === item.expected_path);
|
|
327
|
+
if (rank >= 0) { recalled += 1; reciprocal += 1 / (rank + 1); }
|
|
328
|
+
if (results[0]?.validity === 'superseded') stale += 1;
|
|
329
|
+
if (results.every((row) => row.logical_path && row.heading && row.authority && row.observed_at)) evidenceCorrect += 1;
|
|
330
|
+
if (item.handoff) {
|
|
331
|
+
handoffs += 1;
|
|
332
|
+
if (rank >= 0) handoffsFound += 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const count = Math.max(1, cases.length);
|
|
336
|
+
return {
|
|
337
|
+
recall_at_5: recalled / count,
|
|
338
|
+
mrr: reciprocal / count,
|
|
339
|
+
stale_answer_rate: stale / count,
|
|
340
|
+
evidence_accuracy: evidenceCorrect / count,
|
|
341
|
+
handoff_success: handoffs ? handoffsFound / handoffs : 1,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
@@ -5,5 +5,7 @@ export * from './memory-schema.mjs';
|
|
|
5
5
|
export * from './memory-mode.mjs';
|
|
6
6
|
export * from './memory-handoff.mjs';
|
|
7
7
|
export * from './memory-store.mjs';
|
|
8
|
+
export * from './memory-scope.mjs';
|
|
9
|
+
export * from './evidence-recall.mjs';
|
|
8
10
|
export * from './validate-core.mjs';
|
|
9
11
|
export * from './validate-memory.mjs';
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
2
3
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
4
|
import { basename, join, relative } from 'node:path';
|
|
4
5
|
|
|
5
6
|
import { sanitizeMemoryText } from './memory-schema.mjs';
|
|
7
|
+
import { scopeForMemoryKey } from './memory-scope.mjs';
|
|
6
8
|
|
|
7
9
|
const SHARED_HANDOFF_FIELDS = Object.freeze([
|
|
8
10
|
['objective', 'objective.current'],
|
|
@@ -43,6 +45,10 @@ export function normalizeSharedHandoff(shared) {
|
|
|
43
45
|
const normalized = {};
|
|
44
46
|
const workSessionId = sanitizeMemoryText(shared.work_session_id ?? shared.workSessionId ?? '').trim();
|
|
45
47
|
if (workSessionId) normalized.work_session_id = workSessionId;
|
|
48
|
+
for (const field of ['branch', 'worktree_id', 'repository_id', 'change_slug', 'tasks_hash', 'spec_hash']) {
|
|
49
|
+
const value = sanitizeMemoryText(shared[field] ?? '').trim();
|
|
50
|
+
if (value) normalized[field] = value;
|
|
51
|
+
}
|
|
46
52
|
|
|
47
53
|
for (const [field] of SHARED_HANDOFF_FIELDS) {
|
|
48
54
|
if (!Object.hasOwn(shared, field)) continue;
|
|
@@ -53,7 +59,7 @@ export function normalizeSharedHandoff(shared) {
|
|
|
53
59
|
return Object.keys(normalized).length ? normalized : null;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
function eventId(context, memoryKey, value) {
|
|
62
|
+
function eventId(context, memoryKey, value, scope = null) {
|
|
57
63
|
const digest = createHash('sha256')
|
|
58
64
|
.update(JSON.stringify([
|
|
59
65
|
context.projectId,
|
|
@@ -61,6 +67,7 @@ function eventId(context, memoryKey, value) {
|
|
|
61
67
|
context.activation?.id,
|
|
62
68
|
context.turn?.id,
|
|
63
69
|
memoryKey,
|
|
70
|
+
scope,
|
|
64
71
|
canonicalValue(value),
|
|
65
72
|
]))
|
|
66
73
|
.digest('hex')
|
|
@@ -68,13 +75,15 @@ function eventId(context, memoryKey, value) {
|
|
|
68
75
|
return `mem-${digest}`;
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
function makeEvent(context, { memoryKey, value, authority, evidence }) {
|
|
78
|
+
function makeEvent(context, { memoryKey, value, authority, evidence, scopeContext = {} }) {
|
|
72
79
|
const cleanValue = sanitizeValue(value);
|
|
80
|
+
const scope = scopeForMemoryKey(memoryKey, { ...context, ...scopeContext });
|
|
73
81
|
const event = {
|
|
74
82
|
v: 1,
|
|
75
|
-
event_id: eventId(context, memoryKey, cleanValue),
|
|
83
|
+
event_id: eventId(context, memoryKey, cleanValue, scope),
|
|
76
84
|
project_id: String(context.projectId || ''),
|
|
77
85
|
memory_key: memoryKey,
|
|
86
|
+
scope,
|
|
78
87
|
operation: 'assert',
|
|
79
88
|
value: cleanValue,
|
|
80
89
|
authority,
|
|
@@ -118,6 +127,26 @@ function nextActionFrom(summary) {
|
|
|
118
127
|
return id && text ? { id, summary: text } : null;
|
|
119
128
|
}
|
|
120
129
|
|
|
130
|
+
function gitScope(cwd = process.cwd(), spawn = spawnSync) {
|
|
131
|
+
const run = (args) => {
|
|
132
|
+
const result = spawn('git', args, { cwd, encoding: 'utf8', windowsHide: true });
|
|
133
|
+
return result.status === 0 ? String(result.stdout || '').trim() : '';
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
136
|
+
const branch = run(['branch', '--show-current']) || `detached:${run(['rev-parse', '--short=12', 'HEAD'])}`;
|
|
137
|
+
const gitDir = run(['rev-parse', '--absolute-git-dir']);
|
|
138
|
+
const remote = run(['remote', 'get-url', 'origin']) || run(['rev-parse', '--show-toplevel']);
|
|
139
|
+
if (!branch || !gitDir || !remote) return null;
|
|
140
|
+
return {
|
|
141
|
+
branch,
|
|
142
|
+
worktree_id: createHash('sha256').update(gitDir).digest('hex').slice(0, 16),
|
|
143
|
+
repository_id: createHash('sha256').update(remote).digest('hex').slice(0, 16),
|
|
144
|
+
};
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
121
150
|
export function collectLifecycleEvidence(vaultBase, { changeSlug = '', summary = '', noteRel = '' } = {}) {
|
|
122
151
|
const evidence = {};
|
|
123
152
|
const slug = String(changeSlug || '').trim();
|
|
@@ -159,11 +188,13 @@ export function collectLifecycleEvidence(vaultBase, { changeSlug = '', summary =
|
|
|
159
188
|
if (nextAction) evidence.nextAction = nextAction;
|
|
160
189
|
const commit = String(summary || '').match(/\b[0-9a-f]{40}\b/i)?.[0];
|
|
161
190
|
if (commit) {
|
|
191
|
+
const scope = gitScope();
|
|
162
192
|
evidence.git = {
|
|
163
193
|
commit: commit.toLowerCase(),
|
|
164
194
|
pushed: !/(?:nenhum|sem)\s+push/i.test(String(summary || '')),
|
|
165
195
|
verified: false,
|
|
166
196
|
path: noteRel,
|
|
197
|
+
...(scope || {}),
|
|
167
198
|
};
|
|
168
199
|
}
|
|
169
200
|
return evidence;
|
|
@@ -184,6 +215,14 @@ export function buildSessionMemoryEvents({
|
|
|
184
215
|
const context = {
|
|
185
216
|
projectId, identity, activation, turn, observedAt,
|
|
186
217
|
workSessionId: normalizedShared?.work_session_id || '',
|
|
218
|
+
canonicalSessionId: identity?.canonicalConversationId || '',
|
|
219
|
+
activation_id: activation?.id || '',
|
|
220
|
+
branch: normalizedShared?.branch || '',
|
|
221
|
+
worktreeId: normalizedShared?.worktree_id || '',
|
|
222
|
+
repositoryId: normalizedShared?.repository_id || '',
|
|
223
|
+
changeSlug: normalizedShared?.change_slug || evidence.change?.slug || '',
|
|
224
|
+
tasksHash: normalizedShared?.tasks_hash || '',
|
|
225
|
+
specHash: normalizedShared?.spec_hash || '',
|
|
187
226
|
};
|
|
188
227
|
const events = [];
|
|
189
228
|
|
|
@@ -195,6 +234,7 @@ export function buildSessionMemoryEvents({
|
|
|
195
234
|
value: normalizedShared[field],
|
|
196
235
|
authority: 'reported',
|
|
197
236
|
evidence: [noteRel],
|
|
237
|
+
scopeContext: { changeSlug: evidence.change?.slug || normalizedShared?.change_slug },
|
|
198
238
|
}));
|
|
199
239
|
}
|
|
200
240
|
}
|
|
@@ -214,6 +254,7 @@ export function buildSessionMemoryEvents({
|
|
|
214
254
|
value: { status: evidence.change.status, adr: evidence.change.adr },
|
|
215
255
|
authority: 'verified',
|
|
216
256
|
evidence: [evidence.change.path || evidence.change.adr],
|
|
257
|
+
scopeContext: { changeSlug: evidence.change.slug },
|
|
217
258
|
}));
|
|
218
259
|
}
|
|
219
260
|
|
|
@@ -227,6 +268,11 @@ export function buildSessionMemoryEvents({
|
|
|
227
268
|
},
|
|
228
269
|
authority: 'verified',
|
|
229
270
|
evidence: [evidence.verdict.path],
|
|
271
|
+
scopeContext: {
|
|
272
|
+
changeSlug: evidence.change?.slug || normalizedShared?.change_slug,
|
|
273
|
+
tasksHash: evidence.verdict.tasks_hash || normalizedShared?.tasks_hash,
|
|
274
|
+
specHash: evidence.verdict.spec_hash || normalizedShared?.spec_hash,
|
|
275
|
+
},
|
|
230
276
|
}));
|
|
231
277
|
}
|
|
232
278
|
|
|
@@ -236,6 +282,10 @@ export function buildSessionMemoryEvents({
|
|
|
236
282
|
value: [...new Set(evidence.sensors.map(String))].sort(),
|
|
237
283
|
authority: 'verified',
|
|
238
284
|
evidence: evidence.sensors,
|
|
285
|
+
scopeContext: {
|
|
286
|
+
changeSlug: evidence.change?.slug || normalizedShared?.change_slug,
|
|
287
|
+
tasksHash: evidence.sensors_tasks_hash || normalizedShared?.tasks_hash,
|
|
288
|
+
},
|
|
239
289
|
}));
|
|
240
290
|
}
|
|
241
291
|
|
|
@@ -249,6 +299,11 @@ export function buildSessionMemoryEvents({
|
|
|
249
299
|
},
|
|
250
300
|
authority: evidence.git.verified === false ? 'reported' : 'verified',
|
|
251
301
|
evidence: [evidence.git.path || evidence.git.commit],
|
|
302
|
+
scopeContext: {
|
|
303
|
+
branch: evidence.git.branch || normalizedShared?.branch,
|
|
304
|
+
worktreeId: evidence.git.worktree_id || normalizedShared?.worktree_id,
|
|
305
|
+
repositoryId: evidence.git.repository_id || normalizedShared?.repository_id,
|
|
306
|
+
},
|
|
252
307
|
}));
|
|
253
308
|
}
|
|
254
309
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { MEMORY_SCOPE_TYPES, normalizeMemoryScope } from './memory-scope.mjs';
|
|
2
3
|
|
|
3
4
|
export const SHARED_LIMITS = Object.freeze({ lines: 48, bytes: 6144, lineChars: 320 });
|
|
4
5
|
|
|
@@ -107,6 +108,11 @@ export function validateMemoryEvent(event, { projectId } = {}) {
|
|
|
107
108
|
if (projectId !== undefined && !eventBelongsToVault(event, projectId)) {
|
|
108
109
|
errors.push(`project_id não pertence ao vault esperado (${projectId}).`);
|
|
109
110
|
}
|
|
111
|
+
if (event.scope !== undefined) {
|
|
112
|
+
if (!normalizeMemoryScope(event.scope, { projectId: event.project_id || projectId || '' })) {
|
|
113
|
+
errors.push(`scope deve conter type (${MEMORY_SCOPE_TYPES.join('|')}) e id não vazio compatível com o projeto.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
110
116
|
|
|
111
117
|
if (event.candidate_decision !== undefined) {
|
|
112
118
|
const decision = event.candidate_decision;
|
|
@@ -133,7 +139,7 @@ export function validateMemoryEvent(event, { projectId } = {}) {
|
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
|
|
136
|
-
for (const field of ['value', 'evidence']) sanitizedField(event, field, errors);
|
|
142
|
+
for (const field of ['value', 'evidence', 'scope']) sanitizedField(event, field, errors);
|
|
137
143
|
return { ok: errors.length === 0, errors, warnings };
|
|
138
144
|
}
|
|
139
145
|
|
|
@@ -161,6 +167,7 @@ function hashProjection(events) {
|
|
|
161
167
|
operation: event.operation,
|
|
162
168
|
value: sanitizeMemoryText(event.value),
|
|
163
169
|
authority: event.authority,
|
|
170
|
+
scope: event.scope,
|
|
164
171
|
observed_at: event.observed_at,
|
|
165
172
|
evidence: Array.isArray(event.evidence) ? event.evidence.map(sanitizeMemoryText) : [],
|
|
166
173
|
}));
|
|
@@ -173,7 +180,10 @@ function eventLine(event) {
|
|
|
173
180
|
? event.evidence.map(sanitizeMemoryText).join(', ')
|
|
174
181
|
: 'none';
|
|
175
182
|
const source = sanitizeMemoryText(event.source_turn_id || event.canonical_session_id || event.activation_id || 'unknown');
|
|
176
|
-
const
|
|
183
|
+
const scope = event.scope?.type && event.scope?.id
|
|
184
|
+
? ` · scope:${sanitizeMemoryText(event.scope.type)}:${sanitizeMemoryText(event.scope.id)}`
|
|
185
|
+
: '';
|
|
186
|
+
const line = `- [${sanitizeMemoryText(event.event_id)}] ${value} · authority:${sanitizeMemoryText(event.authority)}${scope} · source:${source} · as_of:${sanitizeMemoryText(event.observed_at)} · evidence:${evidence}`;
|
|
177
187
|
return line.length <= SHARED_LIMITS.lineChars
|
|
178
188
|
? line
|
|
179
189
|
: `${line.slice(0, SHARED_LIMITS.lineChars - 1).trimEnd()}…`;
|