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.
Files changed (72) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
  3. package/dist/cli/commands/code-runtime-commands.js +57 -2
  4. package/dist/cli/commands/core/doctor-command.d.ts +8 -0
  5. package/dist/cli/commands/core/doctor-command.js +44 -2
  6. package/dist/cli/commands/core/memory-command.js +65 -3
  7. package/dist/cli/commands/dispatch-commands.js +19 -5
  8. package/dist/cli/commands/dispatch-from-dag.js +17 -0
  9. package/dist/cli/commands/memory-commands.d.ts +59 -0
  10. package/dist/cli/commands/memory-commands.js +195 -19
  11. package/dist/cli/commands/request-commands.d.ts +8 -0
  12. package/dist/cli/commands/request-commands.js +23 -2
  13. package/dist/cli/commands/sub-agent-commands.js +2 -0
  14. package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
  15. package/dist/cli/commands/wave-plan-commands.js +93 -0
  16. package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
  17. package/dist/services/context/build-dispatch-system-prompt.js +132 -17
  18. package/dist/services/context/context-audit.d.ts +100 -0
  19. package/dist/services/context/context-audit.js +322 -0
  20. package/dist/services/context/context-schema.d.ts +1 -1
  21. package/dist/services/context/memory-index-reader.d.ts +26 -0
  22. package/dist/services/context/memory-index-reader.js +62 -30
  23. package/dist/services/context/memory-preflight-config.d.ts +33 -0
  24. package/dist/services/context/memory-preflight-config.js +32 -2
  25. package/dist/services/context/memory-preflight-service.d.ts +20 -1
  26. package/dist/services/context/memory-preflight-service.js +198 -31
  27. package/dist/services/context/summary-view.d.ts +54 -0
  28. package/dist/services/context/summary-view.js +114 -0
  29. package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
  30. package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
  31. package/dist/services/dispatch/session-capsule.d.ts +23 -0
  32. package/dist/services/dispatch/session-capsule.js +56 -0
  33. package/dist/services/dispatch/slice-dag.d.ts +9 -0
  34. package/dist/services/dispatch/slice-dag.js +9 -1
  35. package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
  36. package/dist/services/dispatch/test-tool-detection.js +14 -13
  37. package/dist/services/doctor/doctor-service/checks/l3-memory-health.d.ts +19 -2
  38. package/dist/services/doctor/doctor-service/checks/l3-memory-health.js +143 -19
  39. package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
  40. package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
  41. package/dist/services/ide/ide-types.d.ts +15 -0
  42. package/dist/services/job/job-types.d.ts +3 -3
  43. package/dist/services/memory/memory-ingest-service.d.ts +79 -0
  44. package/dist/services/memory/memory-ingest-service.js +225 -0
  45. package/dist/services/memory/memory-rotate-service.d.ts +88 -0
  46. package/dist/services/memory/memory-rotate-service.js +373 -0
  47. package/dist/services/memory/project-memory-service/index/ranking.d.ts +9 -1
  48. package/dist/services/memory/project-memory-service/index/ranking.js +25 -13
  49. package/dist/services/memory/project-memory-service/index/reindex.d.ts +75 -0
  50. package/dist/services/memory/project-memory-service/index/reindex.js +207 -0
  51. package/dist/services/memory/project-memory-service/index/search.js +14 -24
  52. package/dist/services/memory/project-memory-service/index.d.ts +7 -3
  53. package/dist/services/memory/project-memory-service/index.js +6 -2
  54. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +80 -3
  55. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +167 -28
  56. package/dist/services/memory/project-memory-service/types.d.ts +31 -1
  57. package/dist/services/memory/project-memory-service/types.js +76 -1
  58. package/dist/services/preferences/preferences-types.d.ts +14 -0
  59. package/dist/services/preferences/preferences-types.js +8 -0
  60. package/dist/services/share/run-state-contract.d.ts +1 -1
  61. package/package.json +5 -5
  62. package/skills/bee/peaks-qa/SKILL.md +2 -0
  63. package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
  64. package/skills/bee/peaks-rd/SKILL.md +2 -0
  65. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
  66. package/skills/bee/peaks-txt/SKILL.md +2 -0
  67. package/skills/bee/peaks-ui/SKILL.md +2 -0
  68. package/skills/peaks-code/SKILL.md +9 -1
  69. package/skills/peaks-code/references/context-governance.md +29 -0
  70. package/skills/peaks-code/references/runbook.md +6 -0
  71. package/skills/peaks-code/references/step-11-memory-sediment.md +35 -0
  72. 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
- return {
56
- project: [],
57
- rule: [],
58
- decision: [],
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 hot/warm layout the reader
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
- feedback: [],
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
- export declare const VALID_MEMORY_KINDS: Set<ProjectMemoryKind>;
3
- /** Exported for guard tests + tooling that needs to enumerate the valid
4
- * set without duplicating the literal. Single source of truth. */
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 8-kind `VALID_MEMORY_KINDS` allow-list and 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
- export const VALID_MEMORY_KINDS = new Set([
19
- 'project',
20
- 'rule',
21
- 'decision',
22
- 'reference',
23
- 'feedback',
24
- 'convention',
25
- 'module',
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
- export function parseStoredMemoryFile(content, filePath) {
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
- if (!normalized.startsWith('---\n'))
75
- return null;
76
- const endIndex = normalized.indexOf('\n---\n', 4);
77
- if (endIndex < 0)
78
- return null;
79
- const frontmatter = normalized.slice(4, endIndex);
80
- const body = normalized.slice(endIndex + '\n---\n'.length).trim();
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
- kind = line.slice('type:'.length).trim();
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
- if (!name || !kind || !VALID_MEMORY_KINDS.has(kind) || body.length === 0)
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: kind,
240
+ kind,
102
241
  sourceArtifact: sourceArtifact && sourceArtifact !== 'undefined' ? sourceArtifact : null,
103
242
  body,
104
243
  filePath