peaks-loop 4.0.34 → 4.0.36
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 +34 -0
- package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
- package/dist/cli/commands/code-runtime-commands.js +57 -2
- package/dist/cli/commands/core/doctor-command.d.ts +8 -0
- package/dist/cli/commands/core/doctor-command.js +44 -2
- package/dist/cli/commands/core/memory-command.js +65 -3
- package/dist/cli/commands/dispatch-commands.js +19 -5
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/memory-commands.d.ts +59 -0
- package/dist/cli/commands/memory-commands.js +195 -19
- package/dist/cli/commands/request-commands.d.ts +8 -0
- package/dist/cli/commands/request-commands.js +23 -2
- package/dist/cli/commands/sub-agent-commands.js +2 -0
- package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
- package/dist/cli/commands/wave-plan-commands.js +93 -0
- package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
- package/dist/services/context/build-dispatch-system-prompt.js +132 -17
- package/dist/services/context/context-audit.d.ts +100 -0
- package/dist/services/context/context-audit.js +322 -0
- package/dist/services/context/context-schema.d.ts +1 -1
- package/dist/services/context/memory-index-reader.d.ts +26 -0
- package/dist/services/context/memory-index-reader.js +62 -30
- package/dist/services/context/memory-preflight-config.d.ts +33 -0
- package/dist/services/context/memory-preflight-config.js +32 -2
- package/dist/services/context/memory-preflight-service.d.ts +20 -1
- package/dist/services/context/memory-preflight-service.js +198 -31
- package/dist/services/context/summary-view.d.ts +54 -0
- package/dist/services/context/summary-view.js +114 -0
- package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
- package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
- package/dist/services/dispatch/session-capsule.d.ts +23 -0
- package/dist/services/dispatch/session-capsule.js +56 -0
- package/dist/services/dispatch/slice-dag.d.ts +9 -0
- package/dist/services/dispatch/slice-dag.js +9 -1
- package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
- package/dist/services/dispatch/test-tool-detection.js +14 -13
- package/dist/services/doctor/doctor-service/checks/l3-memory-health.d.ts +19 -2
- package/dist/services/doctor/doctor-service/checks/l3-memory-health.js +143 -19
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/job/job-types.d.ts +3 -3
- package/dist/services/memory/memory-ingest-service.d.ts +79 -0
- package/dist/services/memory/memory-ingest-service.js +225 -0
- package/dist/services/memory/memory-rotate-service.d.ts +88 -0
- package/dist/services/memory/memory-rotate-service.js +373 -0
- package/dist/services/memory/project-memory-service/index/ranking.d.ts +9 -1
- package/dist/services/memory/project-memory-service/index/ranking.js +25 -13
- package/dist/services/memory/project-memory-service/index/reindex.d.ts +75 -0
- package/dist/services/memory/project-memory-service/index/reindex.js +207 -0
- package/dist/services/memory/project-memory-service/index/search.js +14 -24
- package/dist/services/memory/project-memory-service/index.d.ts +7 -3
- package/dist/services/memory/project-memory-service/index.js +6 -2
- package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +80 -3
- package/dist/services/memory/project-memory-service/parsers/frontmatter.js +167 -28
- package/dist/services/memory/project-memory-service/types.d.ts +31 -1
- package/dist/services/memory/project-memory-service/types.js +76 -1
- package/dist/services/preferences/preferences-types.d.ts +14 -0
- package/dist/services/preferences/preferences-types.js +8 -0
- package/dist/services/share/run-state-contract.d.ts +1 -1
- package/package.json +5 -5
- package/skills/bee/peaks-qa/SKILL.md +2 -0
- package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
- package/skills/bee/peaks-rd/SKILL.md +2 -0
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
- package/skills/bee/peaks-txt/SKILL.md +2 -0
- package/skills/bee/peaks-ui/SKILL.md +2 -0
- package/skills/peaks-code/SKILL.md +9 -1
- package/skills/peaks-code/references/context-governance.md +29 -0
- package/skills/peaks-code/references/runbook.md +6 -0
- package/skills/peaks-code/references/step-11-memory-sediment.md +35 -0
- package/skills/peaks-doctor/SKILL.md +2 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// `peaks memory reindex` — full rebuild of `.peaks/memory/index.json` from
|
|
3
|
+
// the markdown files on disk, plus regeneration of the human/LLM-facing
|
|
4
|
+
// `MEMORY.md` index and a drift report.
|
|
5
|
+
//
|
|
6
|
+
// Why this exists (slice 2026-09-09-memory-system-overhaul, B):
|
|
7
|
+
// - `peaks memory extract` only ever wrote memories FROM artifacts; it
|
|
8
|
+
// never re-scanned files already on disk, and there was no rebuild
|
|
9
|
+
// command. 78 top-level files were absent from index.json.
|
|
10
|
+
// - The index's `kind` came only from the nested `metadata.type` field;
|
|
11
|
+
// files using a top-level `kind:` were silently dropped.
|
|
12
|
+
// - `index.json` (machine) and `MEMORY.md` (human/LLM) were two
|
|
13
|
+
// unsynchronised indexes with nothing keeping them in sync.
|
|
14
|
+
//
|
|
15
|
+
// Contract:
|
|
16
|
+
// - read-only on the memory markdown files (never rewrites a memory body)
|
|
17
|
+
// - deterministic ordering: kinds in `KIND_ORDER`, entries by name
|
|
18
|
+
// - nothing is silently dropped: every file that could not be classified
|
|
19
|
+
// is reported with its path + raw value
|
|
20
|
+
// - `MEMORY.md` is regenerated with a "do not edit" banner (the previous
|
|
21
|
+
// hand-maintained index had drifted to 116 lines against 231 index
|
|
22
|
+
// entries; a derived view is the only way to keep them in sync)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { basename, join, relative } from 'node:path';
|
|
26
|
+
import { MEMORY_KIND_TIER } from '../types.js';
|
|
27
|
+
import { parseMemoryFrontmatter, parseStoredMemoryFile } from '../parsers/frontmatter.js';
|
|
28
|
+
import { assertSafeProjectMemoryDir, normalizeRoot } from '../store/paths.js';
|
|
29
|
+
import { buildMemoryIndex, generateMemoryIndexFile, readExistingIndex } from './ranking.js';
|
|
30
|
+
import { listMarkdownFiles, readProjectMemories } from './search.js';
|
|
31
|
+
/** Marker line that owns `MEMORY.md`. Anything above it is machine output. */
|
|
32
|
+
export const MEMORY_MD_BANNER = '<!-- generated by `peaks memory reindex` — do not edit -->';
|
|
33
|
+
/** `MEMORY.md` is the generated index itself — never a memory source. */
|
|
34
|
+
export const MEMORY_MD_FILENAME = 'MEMORY.md';
|
|
35
|
+
/**
|
|
36
|
+
* Deterministic section order for the generated `MEMORY.md`: hot kinds
|
|
37
|
+
* first (mirrors `HOT_KINDS` in `ranking.ts`), warm kinds last. Derived
|
|
38
|
+
* from the insertion order of `MEMORY_KIND_TIER` so the vocabulary has
|
|
39
|
+
* exactly one definition.
|
|
40
|
+
*/
|
|
41
|
+
export const KIND_ORDER = Object.keys(MEMORY_KIND_TIER);
|
|
42
|
+
function emptyKindCounts() {
|
|
43
|
+
const counts = {};
|
|
44
|
+
for (const kind of KIND_ORDER)
|
|
45
|
+
counts[kind] = 0;
|
|
46
|
+
return counts;
|
|
47
|
+
}
|
|
48
|
+
function readPreviousIndexEntries(indexPath) {
|
|
49
|
+
// `readExistingIndex` is fail-soft (null on missing / unparsable / wrong
|
|
50
|
+
// version), which is exactly what a drift report wants: a corrupt index
|
|
51
|
+
// simply yields no orphan-index findings instead of throwing.
|
|
52
|
+
const previous = readExistingIndex(indexPath);
|
|
53
|
+
if (previous === null)
|
|
54
|
+
return [];
|
|
55
|
+
return [
|
|
56
|
+
...Object.values(previous.hot ?? {}).flat(),
|
|
57
|
+
...Object.values(previous.warm ?? {}).flat(),
|
|
58
|
+
...(previous.cold ?? [])
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
function collectUnclassified(diskFiles) {
|
|
62
|
+
const unclassified = [];
|
|
63
|
+
for (const filePath of diskFiles) {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = parseMemoryFrontmatter(readFileSync(filePath, 'utf8'));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Unreadable file: still report it rather than dropping it silently.
|
|
70
|
+
unclassified.push({ name: basename(filePath, '.md'), filePath, rawKind: null, reason: 'file could not be read' });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (parsed.kind.kind !== null)
|
|
74
|
+
continue;
|
|
75
|
+
const rawKind = parsed.kind.rawKind;
|
|
76
|
+
unclassified.push({
|
|
77
|
+
name: parsed.name ?? basename(filePath, '.md'),
|
|
78
|
+
filePath,
|
|
79
|
+
rawKind,
|
|
80
|
+
reason: rawKind === null
|
|
81
|
+
? 'no metadata.type / kind / type field in frontmatter'
|
|
82
|
+
: `unrecognized kind value: ${rawKind}`
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return unclassified.sort((left, right) => left.filePath.localeCompare(right.filePath));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Detect files that resolve to the same memory name. Only files the read
|
|
89
|
+
* path actually indexes participate (a file rejected for a missing kind is
|
|
90
|
+
* not an index entry and cannot collide with one).
|
|
91
|
+
*
|
|
92
|
+
* Deterministic: names sorted, each `filePaths` sorted. Never resolves the
|
|
93
|
+
* collision itself — the caller reports it; both files keep their entries.
|
|
94
|
+
*/
|
|
95
|
+
function collectNameConflicts(diskFiles) {
|
|
96
|
+
const byName = new Map();
|
|
97
|
+
for (const filePath of diskFiles) {
|
|
98
|
+
let parsed;
|
|
99
|
+
try {
|
|
100
|
+
parsed = parseStoredMemoryFile(readFileSync(filePath, 'utf8'), filePath);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
continue; // unreadable files are reported by collectUnclassified
|
|
104
|
+
}
|
|
105
|
+
if (parsed === null)
|
|
106
|
+
continue;
|
|
107
|
+
const bucket = byName.get(parsed.name);
|
|
108
|
+
if (bucket === undefined)
|
|
109
|
+
byName.set(parsed.name, [filePath]);
|
|
110
|
+
else if (!bucket.includes(filePath))
|
|
111
|
+
bucket.push(filePath);
|
|
112
|
+
}
|
|
113
|
+
return [...byName.entries()]
|
|
114
|
+
.filter(([, filePaths]) => filePaths.length > 1)
|
|
115
|
+
.map(([name, filePaths]) => ({ name, filePaths: [...filePaths].sort((left, right) => left.localeCompare(right)) }))
|
|
116
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Render the human/LLM-facing `MEMORY.md` from a built index. Deterministic:
|
|
120
|
+
* kinds in `KIND_ORDER`, entries sorted by name, no timestamps.
|
|
121
|
+
*/
|
|
122
|
+
export function renderMemoryMarkdown(index, memoryDir) {
|
|
123
|
+
const lines = [
|
|
124
|
+
MEMORY_MD_BANNER,
|
|
125
|
+
'',
|
|
126
|
+
'# Peaks Memory Index',
|
|
127
|
+
'',
|
|
128
|
+
'> Auto-generated by `peaks memory reindex`. Do not edit by hand — the next',
|
|
129
|
+
'> reindex overwrites this file. Source of truth: `.peaks/memory/*.md`',
|
|
130
|
+
'> (bodies) and `.peaks/memory/index.json` (machine index).',
|
|
131
|
+
''
|
|
132
|
+
];
|
|
133
|
+
for (const kind of KIND_ORDER) {
|
|
134
|
+
const entries = [...(index.hot[kind] ?? []), ...(index.warm[kind] ?? [])]
|
|
135
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
136
|
+
if (entries.length === 0)
|
|
137
|
+
continue;
|
|
138
|
+
lines.push(`## ${kind} (${entries.length})`, '');
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
const link = relative(memoryDir, entry.sourcePath).replaceAll('\\', '/');
|
|
141
|
+
lines.push(`- [${entry.name}](${link}) — ${entry.description}`);
|
|
142
|
+
}
|
|
143
|
+
lines.push('');
|
|
144
|
+
}
|
|
145
|
+
return lines.join('\n').replace(/\n+$/, '\n');
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Rebuild `.peaks/memory/index.json` from disk and (on `--apply`) regenerate
|
|
149
|
+
* `MEMORY.md`. Always returns the drift report, even on a dry run.
|
|
150
|
+
*/
|
|
151
|
+
export function executeMemoryReindex(options) {
|
|
152
|
+
const projectRoot = normalizeRoot(options.projectRoot);
|
|
153
|
+
const apply = options.apply ?? false;
|
|
154
|
+
const memoryDir = assertSafeProjectMemoryDir(projectRoot);
|
|
155
|
+
const indexPath = join(memoryDir, 'index.json');
|
|
156
|
+
const memoryMdPath = join(memoryDir, MEMORY_MD_FILENAME);
|
|
157
|
+
const diskFiles = listMarkdownFiles(memoryDir).filter((filePath) => basename(filePath) !== MEMORY_MD_FILENAME);
|
|
158
|
+
const orphanIndex = readPreviousIndexEntries(indexPath)
|
|
159
|
+
.filter((entry) => typeof entry.sourcePath !== 'string' || entry.sourcePath.length === 0 || !existsSync(entry.sourcePath))
|
|
160
|
+
.map((entry) => ({ name: entry.name, kind: entry.kind, sourcePath: entry.sourcePath }))
|
|
161
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
162
|
+
const unclassified = collectUnclassified(diskFiles);
|
|
163
|
+
const nameConflicts = collectNameConflicts(diskFiles);
|
|
164
|
+
// Build once; reuse for the write, the counts, and MEMORY.md so all three
|
|
165
|
+
// views are guaranteed identical.
|
|
166
|
+
const index = buildMemoryIndex(projectRoot);
|
|
167
|
+
const writtenFiles = [];
|
|
168
|
+
if (apply) {
|
|
169
|
+
generateMemoryIndexFile(projectRoot, memoryDir, indexPath, index);
|
|
170
|
+
writtenFiles.push(indexPath);
|
|
171
|
+
}
|
|
172
|
+
// Authoritative post-rebuild view: read the memories back through the same
|
|
173
|
+
// read path every consumer uses, so `orphanDisk` is measured against the
|
|
174
|
+
// real index rather than against this module's own scan.
|
|
175
|
+
const memories = readProjectMemories(projectRoot);
|
|
176
|
+
const indexedFilePaths = new Set(memories.memories.map((memory) => memory.filePath));
|
|
177
|
+
const orphanDisk = diskFiles.filter((filePath) => !indexedFilePaths.has(filePath));
|
|
178
|
+
const indexedByKind = emptyKindCounts();
|
|
179
|
+
for (const memory of memories.memories) {
|
|
180
|
+
indexedByKind[memory.kind] = (indexedByKind[memory.kind] ?? 0) + 1;
|
|
181
|
+
}
|
|
182
|
+
let memoryMdRegenerated = false;
|
|
183
|
+
if (apply) {
|
|
184
|
+
const markdown = renderMemoryMarkdown(index, memoryDir);
|
|
185
|
+
// Plain write (not O_EXCL): MEMORY.md is a derived view and is expected
|
|
186
|
+
// to be overwritten on every reindex.
|
|
187
|
+
writeFileSync(memoryMdPath, markdown, 'utf8');
|
|
188
|
+
writtenFiles.push(memoryMdPath);
|
|
189
|
+
memoryMdRegenerated = true;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
apply,
|
|
193
|
+
projectRoot,
|
|
194
|
+
memoryDir,
|
|
195
|
+
indexPath,
|
|
196
|
+
memoryMdPath,
|
|
197
|
+
scannedFiles: diskFiles.length,
|
|
198
|
+
indexed: memories.memories.length,
|
|
199
|
+
indexedByKind,
|
|
200
|
+
unclassified,
|
|
201
|
+
nameConflicts,
|
|
202
|
+
orphanIndex,
|
|
203
|
+
orphanDisk,
|
|
204
|
+
memoryMd: { path: memoryMdPath, regenerated: memoryMdRegenerated },
|
|
205
|
+
writtenFiles
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
23
23
|
import { basename, join } from 'node:path';
|
|
24
|
+
import { MEMORY_KIND_TIER, PROJECT_MEMORY_KINDS } from '../types.js';
|
|
24
25
|
import { parseStoredMemoryFile } from '../parsers/frontmatter.js';
|
|
25
26
|
import { assertSafeProjectMemoryDir, normalizeRoot } from '../store/paths.js';
|
|
26
27
|
export function listMarkdownFiles(dirPath, options = {}) {
|
|
@@ -52,38 +53,27 @@ export function listMarkdownFiles(dirPath, options = {}) {
|
|
|
52
53
|
return files.sort((left, right) => left.localeCompare(right));
|
|
53
54
|
}
|
|
54
55
|
export function emptyByKind() {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
reference: [],
|
|
60
|
-
feedback: [],
|
|
61
|
-
convention: [],
|
|
62
|
-
module: [],
|
|
63
|
-
lesson: []
|
|
64
|
-
};
|
|
56
|
+
const byKind = {};
|
|
57
|
+
for (const kind of PROJECT_MEMORY_KINDS)
|
|
58
|
+
byKind[kind] = [];
|
|
59
|
+
return byKind;
|
|
65
60
|
}
|
|
66
61
|
export function emptyIndex() {
|
|
67
62
|
// Cast through unknown: we *intend* the two halves to together cover the
|
|
68
63
|
// union `ProjectMemoryKind`, but TS does not know that. The `MemoryIndex`
|
|
69
64
|
// type's `hot` / `warm` fields together cover the union; we split the
|
|
70
|
-
// construction so the JSON output mirrors the
|
|
71
|
-
// expects.
|
|
65
|
+
// construction from the canonical tier map so the JSON output mirrors the
|
|
66
|
+
// hot/warm layout the reader expects.
|
|
67
|
+
const hot = {};
|
|
68
|
+
const warm = {};
|
|
69
|
+
for (const kind of PROJECT_MEMORY_KINDS) {
|
|
70
|
+
(MEMORY_KIND_TIER[kind] === 'hot' ? hot : warm)[kind] = [];
|
|
71
|
+
}
|
|
72
72
|
return {
|
|
73
73
|
version: 1,
|
|
74
74
|
updatedAt: new Date().toISOString(),
|
|
75
|
-
hot:
|
|
76
|
-
|
|
77
|
-
decision: [],
|
|
78
|
-
rule: [],
|
|
79
|
-
convention: [],
|
|
80
|
-
module: [],
|
|
81
|
-
lesson: []
|
|
82
|
-
},
|
|
83
|
-
warm: {
|
|
84
|
-
project: [],
|
|
85
|
-
reference: []
|
|
86
|
-
}
|
|
75
|
+
hot: hot,
|
|
76
|
+
warm: warm
|
|
87
77
|
};
|
|
88
78
|
}
|
|
89
79
|
export function renderEmptyIndex() {
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
export type { BackupPlanOptions, ExtractedProjectMemory, ExtractPlanOptions, ExtractSessionMemoriesOptions, ExtractSessionMemoriesResult, MemoryIndex, MemoryIndexEntry, ProjectMemoryBackupPlan, ProjectMemoryBackupResult, ProjectMemoryBackupSummary, ProjectMemoryCopy, ProjectMemoryExtractPlan, ProjectMemoryExtractResult, ProjectMemoryExtractSummary, ProjectMemoryKind, ProjectMemoryReadResult, ProjectMemoryShowResult, ProjectMemoryWrite, StoredProjectMemory } from './types.js';
|
|
1
|
+
export type { BackupPlanOptions, ExtractedProjectMemory, MemoryKindTier, ExtractPlanOptions, ExtractSessionMemoriesOptions, ExtractSessionMemoriesResult, MemoryIndex, MemoryIndexEntry, ProjectMemoryBackupPlan, ProjectMemoryBackupResult, ProjectMemoryBackupSummary, ProjectMemoryCopy, ProjectMemoryExtractPlan, ProjectMemoryExtractResult, ProjectMemoryExtractSummary, ProjectMemoryKind, ProjectMemoryReadResult, ProjectMemoryShowResult, ProjectMemoryWrite, StoredProjectMemory } from './types.js';
|
|
2
2
|
export { VALID_PROJECT_MEMORY_KINDS } from './parsers/frontmatter.js';
|
|
3
|
+
export { HOT_MEMORY_KINDS, MEMORY_KIND_TIER, PROJECT_MEMORY_KINDS, WARM_MEMORY_KINDS } from './types.js';
|
|
3
4
|
export { summarizeBackupResult, summarizeExtractResult, summarizeMemoryBody, extractStableProjectMemories, END_MARKER, START_MARKER } from './parsers/markdown-pure.js';
|
|
4
|
-
export { parseBlock, parseStoredMemoryFile, renderMemoryFile, slugify } from './parsers/frontmatter.js';
|
|
5
|
+
export { parseBlock, parseMemoryFrontmatter, parseStoredMemoryFile, renderMemoryFile, resolveMemoryKind, resolveMemoryName, slugify } from './parsers/frontmatter.js';
|
|
6
|
+
export type { MemoryKindResolution, MemoryKindSource, MemoryNameResolution, MemoryNameSource, ParsedMemoryFrontmatter } from './parsers/frontmatter.js';
|
|
5
7
|
export { assertInsideProject, assertSafeProjectMemoryDir, assertSafeSessionDir, normalizeRealRoot, normalizeRoot, realPathOrThrow, resolveProjectPath, safeRealpath } from './store/paths.js';
|
|
6
8
|
export { assertSafeMemory, assertSafeMemoryFileContent, hasSensitiveMemoryContent, writeNewFile } from './store/atomic-write.js';
|
|
7
9
|
export { ensureMemoryBootstrap, emptyByKind, emptyIndex, listMarkdownFiles, readProjectMemories, readProjectMemoryBody } from './index/search.js';
|
|
8
|
-
export { generateMemoryIndexFile, readExistingIndex, readMemoryFileMtime, readMemoryIndex, readStoredMemoryNames } from './index/ranking.js';
|
|
10
|
+
export { buildMemoryIndex, generateMemoryIndexFile, readExistingIndex, readMemoryFileMtime, readMemoryIndex, readStoredMemoryNames } from './index/ranking.js';
|
|
11
|
+
export { executeMemoryReindex, renderMemoryMarkdown, KIND_ORDER, MEMORY_MD_BANNER, MEMORY_MD_FILENAME } from './index/reindex.js';
|
|
12
|
+
export type { MemoryReindexOptions, MemoryReindexReport, ReindexNameConflict, ReindexOrphanEntry, ReindexUnclassified } from './index/reindex.js';
|
|
9
13
|
export { createProjectMemoryBackupPlan, createProjectMemoryExtractPlan, executeProjectMemoryBackup, executeProjectMemoryExtract, extractSessionMemories, summarizeProjectMemoryBackupResult, summarizeProjectMemoryExtractResult } from './index/kind-dispatch.js';
|
|
@@ -11,14 +11,18 @@
|
|
|
11
11
|
// `./types.ts` and are re-exported here.
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
export { VALID_PROJECT_MEMORY_KINDS } from './parsers/frontmatter.js';
|
|
14
|
+
// Canonical kind vocabulary + hot/warm tier map (slice E)
|
|
15
|
+
export { HOT_MEMORY_KINDS, MEMORY_KIND_TIER, PROJECT_MEMORY_KINDS, WARM_MEMORY_KINDS } from './types.js';
|
|
14
16
|
// Pure markdown helpers
|
|
15
17
|
export { summarizeBackupResult, summarizeExtractResult, summarizeMemoryBody, extractStableProjectMemories, END_MARKER, START_MARKER } from './parsers/markdown-pure.js';
|
|
16
18
|
// Frontmatter parser + renderer
|
|
17
|
-
export { parseBlock, parseStoredMemoryFile, renderMemoryFile, slugify } from './parsers/frontmatter.js';
|
|
19
|
+
export { parseBlock, parseMemoryFrontmatter, parseStoredMemoryFile, renderMemoryFile, resolveMemoryKind, resolveMemoryName, slugify } from './parsers/frontmatter.js';
|
|
18
20
|
// Store: path safety + sensitive content
|
|
19
21
|
export { assertInsideProject, assertSafeProjectMemoryDir, assertSafeSessionDir, normalizeRealRoot, normalizeRoot, realPathOrThrow, resolveProjectPath, safeRealpath } from './store/paths.js';
|
|
20
22
|
export { assertSafeMemory, assertSafeMemoryFileContent, hasSensitiveMemoryContent, writeNewFile } from './store/atomic-write.js';
|
|
21
23
|
// Index: search / ranking / dispatch
|
|
22
24
|
export { ensureMemoryBootstrap, emptyByKind, emptyIndex, listMarkdownFiles, readProjectMemories, readProjectMemoryBody } from './index/search.js';
|
|
23
|
-
export { generateMemoryIndexFile, readExistingIndex, readMemoryFileMtime, readMemoryIndex, readStoredMemoryNames } from './index/ranking.js';
|
|
25
|
+
export { buildMemoryIndex, generateMemoryIndexFile, readExistingIndex, readMemoryFileMtime, readMemoryIndex, readStoredMemoryNames } from './index/ranking.js';
|
|
26
|
+
// Full index rebuild + drift report (`peaks memory reindex`)
|
|
27
|
+
export { executeMemoryReindex, renderMemoryMarkdown, KIND_ORDER, MEMORY_MD_BANNER, MEMORY_MD_FILENAME } from './index/reindex.js';
|
|
24
28
|
export { createProjectMemoryBackupPlan, createProjectMemoryExtractPlan, executeProjectMemoryBackup, executeProjectMemoryExtract, extractSessionMemories, summarizeProjectMemoryBackupResult, summarizeProjectMemoryExtractResult } from './index/kind-dispatch.js';
|
|
@@ -1,9 +1,86 @@
|
|
|
1
1
|
import type { ExtractedProjectMemory, ProjectMemoryKind, StoredProjectMemory } from '../types.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/** Accepted-kind set, derived from the canonical `PROJECT_MEMORY_KINDS`
|
|
3
|
+
* tuple so the parser cannot drift from the union type / tier map. */
|
|
4
|
+
export declare const VALID_MEMORY_KINDS: ReadonlySet<ProjectMemoryKind>;
|
|
5
|
+
/** Exported for guard tests + tooling that needs to enumerate the accepted
|
|
6
|
+
* set (CLI help text, `--kind` validation) without duplicating the literal. */
|
|
5
7
|
export declare const VALID_PROJECT_MEMORY_KINDS: readonly ProjectMemoryKind[];
|
|
6
8
|
export declare function slugify(title: string): string;
|
|
7
9
|
export declare function parseBlock(block: string, sourceArtifact: string): ExtractedProjectMemory | null;
|
|
8
10
|
export declare function renderMemoryFile(memory: ExtractedProjectMemory): string;
|
|
11
|
+
/**
|
|
12
|
+
* Where a stored memory file's `kind` came from. `'none'` means the file
|
|
13
|
+
* has no resolvable kind (no `metadata.type`, no `kind:`, no `type:`, or
|
|
14
|
+
* the value present is not one of the accepted kinds in
|
|
15
|
+
* `PROJECT_MEMORY_KINDS`).
|
|
16
|
+
*/
|
|
17
|
+
export type MemoryKindSource = 'metadata.type' | 'kind' | 'type' | 'none';
|
|
18
|
+
export interface MemoryKindResolution {
|
|
19
|
+
kind: ProjectMemoryKind | null;
|
|
20
|
+
source: MemoryKindSource;
|
|
21
|
+
/** The first raw value found in the frontmatter, even when it is not a valid kind. */
|
|
22
|
+
rawKind: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface ParsedMemoryFrontmatter {
|
|
25
|
+
hasFrontmatter: boolean;
|
|
26
|
+
name?: string;
|
|
27
|
+
/** Top-level `title:` frontmatter value. Never the nested `metadata.title`
|
|
28
|
+
* (that is a different semantic); used only as a name fallback on the
|
|
29
|
+
* read path — see `resolveMemoryName`. */
|
|
30
|
+
title?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
sourceArtifact?: string;
|
|
33
|
+
kind: MemoryKindResolution;
|
|
34
|
+
/** Raw frontmatter block text (without the `---` fences); '' when absent. */
|
|
35
|
+
frontmatter: string;
|
|
36
|
+
body: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the peaks memory kind from a stored memory file's frontmatter.
|
|
40
|
+
*
|
|
41
|
+
* Resolution order (first *valid* kind wins):
|
|
42
|
+
* 1. nested `metadata.type` — the canonical peaks contract
|
|
43
|
+
* 2. top-level `kind:` — the legacy alias (was silently dropped before)
|
|
44
|
+
* 3. top-level `type:` — tolerated by the pre-existing trim-based reader
|
|
45
|
+
* 4. `none` — reported as unclassified; never invented
|
|
46
|
+
*
|
|
47
|
+
* Slice 2026-09-09-memory-system-overhaul (B): before this helper, files
|
|
48
|
+
* using a top-level `kind:` were silently dropped by the reader (defect
|
|
49
|
+
* #2). Falling through to `kind:` / `type:` is a strict superset of the
|
|
50
|
+
* old behaviour — no previously-indexed file changes kind.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveMemoryKind(content: string): MemoryKindResolution;
|
|
53
|
+
/**
|
|
54
|
+
* Single parse surface for stored memory frontmatter. Both
|
|
55
|
+
* `parseStoredMemoryFile` (read path) and the reindex / ingest / doctor
|
|
56
|
+
* classifiers consume this so there is exactly one kind-resolution rule
|
|
57
|
+
* in the codebase.
|
|
58
|
+
*
|
|
59
|
+
* Tolerates a leading run of HTML-comment lines (e.g. the
|
|
60
|
+
* `<!-- peaks-memory:start -->` sediment marker) before the opening `---`
|
|
61
|
+
* fence. The closing fence is still required and body extraction is
|
|
62
|
+
* unchanged: the body is the text after the closing fence.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseMemoryFrontmatter(content: string): ParsedMemoryFrontmatter;
|
|
65
|
+
/** Which frontmatter field (or the filename) supplied a memory's name. */
|
|
66
|
+
export type MemoryNameSource = 'name' | 'title' | 'stem' | 'none';
|
|
67
|
+
export interface MemoryNameResolution {
|
|
68
|
+
/** The resolved name, or null when every fallback was empty. */
|
|
69
|
+
name: string | null;
|
|
70
|
+
source: MemoryNameSource;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Deterministic name fallback chain for the read path:
|
|
74
|
+
*
|
|
75
|
+
* 1. `name:` — the canonical field written by `renderMemoryFile`
|
|
76
|
+
* 2. `title:` — hand-written / legacy files (the 5 on-disk files
|
|
77
|
+
* this slice fixes carried only `title:` + `kind:`)
|
|
78
|
+
* 3. filename stem — last resort, so a well-formed memory with a valid
|
|
79
|
+
* kind is never dropped just for missing a name
|
|
80
|
+
*
|
|
81
|
+
* Empty values are skipped rather than accepted: a `name:` of `''` still
|
|
82
|
+
* falls through, and a file whose stem is also empty resolves to null so the
|
|
83
|
+
* caller's validation is preserved (never invents a name).
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveMemoryName(parsed: ParsedMemoryFrontmatter, filePath: string): MemoryNameResolution;
|
|
9
86
|
export declare function parseStoredMemoryFile(content: string, filePath: string): StoredProjectMemory | null;
|
|
@@ -10,24 +10,23 @@
|
|
|
10
10
|
//
|
|
11
11
|
// 2. `parseStoredMemoryFile` — read-path side. Files in `.peaks/memory/`
|
|
12
12
|
// are stored as standard YAML frontmatter (name / description /
|
|
13
|
-
// metadata.type / metadata.sourceArtifact) followed by the body.
|
|
13
|
+
// metadata.type / metadata.sourceArtifact) followed by the body. A file
|
|
14
|
+
// may also open with HTML-comment lines (the `<!-- peaks-memory:start -->`
|
|
15
|
+
// sediment marker) before the frontmatter; `parseMemoryFrontmatter`
|
|
16
|
+
// skips that leading comment run before looking for the `---` fence.
|
|
14
17
|
//
|
|
15
|
-
// Both parsers share the
|
|
18
|
+
// Both parsers share the `VALID_MEMORY_KINDS` allow-list (derived from the
|
|
19
|
+
// canonical `PROJECT_MEMORY_KINDS` tuple in `../types.ts`) and the
|
|
16
20
|
// `slugify` helper used to derive filenames from titles.
|
|
17
21
|
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'lesson'
|
|
27
|
-
]);
|
|
28
|
-
/** Exported for guard tests + tooling that needs to enumerate the valid
|
|
29
|
-
* set without duplicating the literal. Single source of truth. */
|
|
30
|
-
export const VALID_PROJECT_MEMORY_KINDS = Array.from(VALID_MEMORY_KINDS);
|
|
22
|
+
import { basename } from 'node:path';
|
|
23
|
+
import { PROJECT_MEMORY_KINDS } from '../types.js';
|
|
24
|
+
/** Accepted-kind set, derived from the canonical `PROJECT_MEMORY_KINDS`
|
|
25
|
+
* tuple so the parser cannot drift from the union type / tier map. */
|
|
26
|
+
export const VALID_MEMORY_KINDS = new Set(PROJECT_MEMORY_KINDS);
|
|
27
|
+
/** Exported for guard tests + tooling that needs to enumerate the accepted
|
|
28
|
+
* set (CLI help text, `--kind` validation) without duplicating the literal. */
|
|
29
|
+
export const VALID_PROJECT_MEMORY_KINDS = PROJECT_MEMORY_KINDS;
|
|
31
30
|
export function slugify(title) {
|
|
32
31
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
33
32
|
return slug.length > 0 ? slug : 'project-memory';
|
|
@@ -69,36 +68,176 @@ export function renderMemoryFile(memory) {
|
|
|
69
68
|
''
|
|
70
69
|
].join('\n');
|
|
71
70
|
}
|
|
72
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the peaks memory kind from a stored memory file's frontmatter.
|
|
73
|
+
*
|
|
74
|
+
* Resolution order (first *valid* kind wins):
|
|
75
|
+
* 1. nested `metadata.type` — the canonical peaks contract
|
|
76
|
+
* 2. top-level `kind:` — the legacy alias (was silently dropped before)
|
|
77
|
+
* 3. top-level `type:` — tolerated by the pre-existing trim-based reader
|
|
78
|
+
* 4. `none` — reported as unclassified; never invented
|
|
79
|
+
*
|
|
80
|
+
* Slice 2026-09-09-memory-system-overhaul (B): before this helper, files
|
|
81
|
+
* using a top-level `kind:` were silently dropped by the reader (defect
|
|
82
|
+
* #2). Falling through to `kind:` / `type:` is a strict superset of the
|
|
83
|
+
* old behaviour — no previously-indexed file changes kind.
|
|
84
|
+
*/
|
|
85
|
+
export function resolveMemoryKind(content) {
|
|
86
|
+
const parsed = parseMemoryFrontmatter(content);
|
|
87
|
+
return parsed.kind;
|
|
88
|
+
}
|
|
89
|
+
/** Start of an HTML comment line, allowing leading horizontal whitespace. */
|
|
90
|
+
const LEADING_COMMENT_OPEN = /^[ \t]*<!--/;
|
|
91
|
+
/**
|
|
92
|
+
* Length of the leading run of blank lines and standalone HTML-comment lines.
|
|
93
|
+
*
|
|
94
|
+
* A stored memory may be written with the documented sediment marker
|
|
95
|
+
* (`<!-- peaks-memory:start -->`) — or any HTML comment — BEFORE its YAML
|
|
96
|
+
* frontmatter. This helper reports how much of the file to skip so the fence
|
|
97
|
+
* can still be found. At least one comment line must be present: a file that
|
|
98
|
+
* merely starts with blank lines is not treated as marker-prefixed, so the
|
|
99
|
+
* pre-existing (fence-at-byte-0) behaviour is preserved exactly.
|
|
100
|
+
*/
|
|
101
|
+
function leadingCommentPrefixLength(normalized) {
|
|
102
|
+
let offset = 0;
|
|
103
|
+
let sawComment = false;
|
|
104
|
+
for (;;) {
|
|
105
|
+
const rest = normalized.slice(offset);
|
|
106
|
+
const blank = /^[ \t]*\n/.exec(rest);
|
|
107
|
+
if (blank !== null) {
|
|
108
|
+
offset += blank[0].length;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const open = LEADING_COMMENT_OPEN.exec(rest);
|
|
112
|
+
if (open === null)
|
|
113
|
+
break;
|
|
114
|
+
const closeIndex = rest.indexOf('-->', open[0].length);
|
|
115
|
+
if (closeIndex < 0)
|
|
116
|
+
break;
|
|
117
|
+
const afterClose = closeIndex + '-->'.length;
|
|
118
|
+
const lineEnd = rest.indexOf('\n', afterClose);
|
|
119
|
+
if (lineEnd < 0)
|
|
120
|
+
break;
|
|
121
|
+
// Only a whole comment line counts; trailing prose after `-->` means the
|
|
122
|
+
// file does not open with a comment block.
|
|
123
|
+
if (rest.slice(afterClose, lineEnd).trim() !== '')
|
|
124
|
+
break;
|
|
125
|
+
offset += lineEnd + 1;
|
|
126
|
+
sawComment = true;
|
|
127
|
+
}
|
|
128
|
+
return sawComment ? offset : 0;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Single parse surface for stored memory frontmatter. Both
|
|
132
|
+
* `parseStoredMemoryFile` (read path) and the reindex / ingest / doctor
|
|
133
|
+
* classifiers consume this so there is exactly one kind-resolution rule
|
|
134
|
+
* in the codebase.
|
|
135
|
+
*
|
|
136
|
+
* Tolerates a leading run of HTML-comment lines (e.g. the
|
|
137
|
+
* `<!-- peaks-memory:start -->` sediment marker) before the opening `---`
|
|
138
|
+
* fence. The closing fence is still required and body extraction is
|
|
139
|
+
* unchanged: the body is the text after the closing fence.
|
|
140
|
+
*/
|
|
141
|
+
export function parseMemoryFrontmatter(content) {
|
|
73
142
|
const normalized = content.replace(/\r\n/g, '\n');
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
143
|
+
const head = normalized.slice(leadingCommentPrefixLength(normalized));
|
|
144
|
+
if (!head.startsWith('---\n')) {
|
|
145
|
+
return { hasFrontmatter: false, kind: { kind: null, source: 'none', rawKind: null }, frontmatter: '', body: normalized.trim() };
|
|
146
|
+
}
|
|
147
|
+
const endIndex = head.indexOf('\n---\n', 4);
|
|
148
|
+
if (endIndex < 0) {
|
|
149
|
+
return { hasFrontmatter: false, kind: { kind: null, source: 'none', rawKind: null }, frontmatter: '', body: normalized.trim() };
|
|
150
|
+
}
|
|
151
|
+
const frontmatter = head.slice(4, endIndex);
|
|
152
|
+
const body = head.slice(endIndex + '\n---\n'.length).trim();
|
|
81
153
|
let name;
|
|
154
|
+
let titleField;
|
|
82
155
|
let description;
|
|
83
|
-
let kind;
|
|
84
156
|
let sourceArtifact;
|
|
157
|
+
let nestedType;
|
|
158
|
+
let topType;
|
|
159
|
+
let kindField;
|
|
160
|
+
let inMetadata = false;
|
|
85
161
|
for (const rawLine of frontmatter.split('\n')) {
|
|
162
|
+
const indented = /^\s/.test(rawLine);
|
|
86
163
|
const line = rawLine.trim();
|
|
164
|
+
if (!indented) {
|
|
165
|
+
inMetadata = line === 'metadata:';
|
|
166
|
+
}
|
|
87
167
|
if (line.startsWith('name:'))
|
|
88
168
|
name = line.slice('name:'.length).trim();
|
|
169
|
+
else if (line.startsWith('title:')) {
|
|
170
|
+
if (!indented)
|
|
171
|
+
titleField = line.slice('title:'.length).trim();
|
|
172
|
+
}
|
|
89
173
|
else if (line.startsWith('description:'))
|
|
90
174
|
description = line.slice('description:'.length).trim();
|
|
91
|
-
else if (line.startsWith('type:'))
|
|
92
|
-
|
|
175
|
+
else if (line.startsWith('type:')) {
|
|
176
|
+
const value = line.slice('type:'.length).trim();
|
|
177
|
+
if (indented || inMetadata)
|
|
178
|
+
nestedType ??= value;
|
|
179
|
+
else
|
|
180
|
+
topType ??= value;
|
|
181
|
+
}
|
|
182
|
+
else if (line.startsWith('kind:'))
|
|
183
|
+
kindField ??= line.slice('kind:'.length).trim();
|
|
93
184
|
else if (line.startsWith('sourceArtifact:'))
|
|
94
185
|
sourceArtifact = line.slice('sourceArtifact:'.length).trim();
|
|
95
186
|
}
|
|
96
|
-
|
|
187
|
+
const candidates = [
|
|
188
|
+
['metadata.type', nestedType],
|
|
189
|
+
['kind', kindField],
|
|
190
|
+
['type', topType]
|
|
191
|
+
];
|
|
192
|
+
let kind = { kind: null, source: 'none', rawKind: null };
|
|
193
|
+
for (const [source, raw] of candidates) {
|
|
194
|
+
if (raw === undefined || raw === '')
|
|
195
|
+
continue;
|
|
196
|
+
if (kind.rawKind === null)
|
|
197
|
+
kind = { kind: null, source: 'none', rawKind: raw };
|
|
198
|
+
if (VALID_MEMORY_KINDS.has(raw)) {
|
|
199
|
+
kind = { kind: raw, source, rawKind: raw };
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { hasFrontmatter: true, frontmatter, ...(name !== undefined ? { name } : {}), ...(titleField !== undefined ? { title: titleField } : {}), ...(description !== undefined ? { description } : {}), ...(sourceArtifact !== undefined ? { sourceArtifact } : {}), kind, body };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Deterministic name fallback chain for the read path:
|
|
207
|
+
*
|
|
208
|
+
* 1. `name:` — the canonical field written by `renderMemoryFile`
|
|
209
|
+
* 2. `title:` — hand-written / legacy files (the 5 on-disk files
|
|
210
|
+
* this slice fixes carried only `title:` + `kind:`)
|
|
211
|
+
* 3. filename stem — last resort, so a well-formed memory with a valid
|
|
212
|
+
* kind is never dropped just for missing a name
|
|
213
|
+
*
|
|
214
|
+
* Empty values are skipped rather than accepted: a `name:` of `''` still
|
|
215
|
+
* falls through, and a file whose stem is also empty resolves to null so the
|
|
216
|
+
* caller's validation is preserved (never invents a name).
|
|
217
|
+
*/
|
|
218
|
+
export function resolveMemoryName(parsed, filePath) {
|
|
219
|
+
if (parsed.name !== undefined && parsed.name.length > 0)
|
|
220
|
+
return { name: parsed.name, source: 'name' };
|
|
221
|
+
if (parsed.title !== undefined && parsed.title.length > 0)
|
|
222
|
+
return { name: parsed.title, source: 'title' };
|
|
223
|
+
const stem = basename(filePath, '.md');
|
|
224
|
+
if (stem.length > 0)
|
|
225
|
+
return { name: stem, source: 'stem' };
|
|
226
|
+
return { name: null, source: 'none' };
|
|
227
|
+
}
|
|
228
|
+
export function parseStoredMemoryFile(content, filePath) {
|
|
229
|
+
const parsed = parseMemoryFrontmatter(content);
|
|
230
|
+
if (!parsed.hasFrontmatter)
|
|
231
|
+
return null;
|
|
232
|
+
const { description, sourceArtifact, body } = parsed;
|
|
233
|
+
const kind = parsed.kind.kind;
|
|
234
|
+
const { name } = resolveMemoryName(parsed, filePath);
|
|
235
|
+
if (name === null || kind === null || body.length === 0)
|
|
97
236
|
return null;
|
|
98
237
|
return {
|
|
99
238
|
name,
|
|
100
239
|
title: description ?? name,
|
|
101
|
-
kind
|
|
240
|
+
kind,
|
|
102
241
|
sourceArtifact: sourceArtifact && sourceArtifact !== 'undefined' ? sourceArtifact : null,
|
|
103
242
|
body,
|
|
104
243
|
filePath
|