peaks-loop 4.0.33 → 4.0.35

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 (88) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/cli/commands/code-commands.js +2 -0
  3. package/dist/cli/commands/code-mode-gate-commands.js +3 -3
  4. package/dist/cli/commands/code-mode-status-command.d.ts +17 -0
  5. package/dist/cli/commands/code-mode-status-command.js +51 -0
  6. package/dist/cli/commands/code-run-command.js +8 -1
  7. package/dist/cli/commands/code-runtime-commands.js +13 -5
  8. package/dist/cli/commands/core/memory-command.js +61 -3
  9. package/dist/cli/commands/core/skill-command.js +1 -1
  10. package/dist/cli/commands/dispatch-commands.js +4 -2
  11. package/dist/cli/commands/memory-commands.d.ts +35 -0
  12. package/dist/cli/commands/memory-commands.js +119 -10
  13. package/dist/cli/commands/request-commands.js +3 -7
  14. package/dist/cli/commands/session-24h-mode.js +9 -1
  15. package/dist/services/24h-mode/auto-engage.d.ts +39 -0
  16. package/dist/services/24h-mode/auto-engage.js +46 -0
  17. package/dist/services/24h-mode/decider.d.ts +11 -5
  18. package/dist/services/24h-mode/decider.js +13 -13
  19. package/dist/services/audit/enforcers/active-skill-resolver.d.ts +3 -1
  20. package/dist/services/audit/enforcers/active-skill-resolver.js +3 -5
  21. package/dist/services/code/auto-compact-lifecycle.d.ts +1 -1
  22. package/dist/services/code/auto-compact-lifecycle.js +1 -1
  23. package/dist/services/code/auto-compact-modes.d.ts +1 -1
  24. package/dist/services/code/auto-compact-modes.js +1 -1
  25. package/dist/services/code/auto-compact-orchestrator.js +18 -22
  26. package/dist/services/code/mode-gate.d.ts +6 -2
  27. package/dist/services/code/mode-gate.js +9 -5
  28. package/dist/services/code/orchestrator-can-do.js +2 -2
  29. package/dist/services/config/config-service.d.ts +17 -0
  30. package/dist/services/config/config-service.js +34 -0
  31. package/dist/services/context/auto-compact-dispatcher.d.ts +2 -2
  32. package/dist/services/context/auto-compact-dispatcher.js +4 -4
  33. package/dist/services/context/auto-compact-reader.js +8 -1
  34. package/dist/services/context/auto-compact-types.d.ts +13 -1
  35. package/dist/services/context/build-dispatch-system-prompt.js +2 -2
  36. package/dist/services/context/memory-index-reader.d.ts +26 -0
  37. package/dist/services/context/memory-index-reader.js +62 -30
  38. package/dist/services/context/memory-preflight-config.d.ts +33 -0
  39. package/dist/services/context/memory-preflight-config.js +32 -2
  40. package/dist/services/context/memory-preflight-service.d.ts +20 -1
  41. package/dist/services/context/memory-preflight-service.js +198 -31
  42. package/dist/services/doctor/doctor-service/checks/l3-memory-health.d.ts +19 -2
  43. package/dist/services/doctor/doctor-service/checks/l3-memory-health.js +143 -19
  44. package/dist/services/hooks/auto-compact-hook-install.d.ts +2 -2
  45. package/dist/services/hooks/auto-compact-hook-install.js +2 -2
  46. package/dist/services/ide/adapters/claude-code-adapter.d.ts +51 -0
  47. package/dist/services/ide/adapters/claude-code-adapter.js +84 -14
  48. package/dist/services/ide/ide-types.d.ts +10 -1
  49. package/dist/services/job/job-types.d.ts +3 -3
  50. package/dist/services/memory/memory-ingest-service.d.ts +79 -0
  51. package/dist/services/memory/memory-ingest-service.js +225 -0
  52. package/dist/services/memory/memory-rotate-service.d.ts +88 -0
  53. package/dist/services/memory/memory-rotate-service.js +373 -0
  54. package/dist/services/memory/project-memory-service/index/ranking.d.ts +9 -1
  55. package/dist/services/memory/project-memory-service/index/ranking.js +25 -13
  56. package/dist/services/memory/project-memory-service/index/reindex.d.ts +75 -0
  57. package/dist/services/memory/project-memory-service/index/reindex.js +207 -0
  58. package/dist/services/memory/project-memory-service/index/search.js +14 -24
  59. package/dist/services/memory/project-memory-service/index.d.ts +7 -3
  60. package/dist/services/memory/project-memory-service/index.js +6 -2
  61. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +75 -3
  62. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +113 -24
  63. package/dist/services/memory/project-memory-service/types.d.ts +31 -1
  64. package/dist/services/memory/project-memory-service/types.js +76 -1
  65. package/dist/services/mode/mode-enforcement.d.ts +4 -1
  66. package/dist/services/mode/mode-enforcement.js +26 -28
  67. package/dist/services/mode/mode-status-service.d.ts +81 -0
  68. package/dist/services/mode/mode-status-service.js +108 -0
  69. package/dist/services/preferences/preferences-types.d.ts +14 -0
  70. package/dist/services/preferences/preferences-types.js +8 -0
  71. package/dist/services/share/run-state-contract.d.ts +1 -1
  72. package/dist/services/skills/presence-lease-service.d.ts +21 -0
  73. package/dist/services/skills/presence-lease-service.js +26 -0
  74. package/dist/services/skills/skill-presence-service.d.ts +10 -1
  75. package/dist/services/skills/skill-presence-service.js +32 -4
  76. package/dist/services/skills/skill-statusline-service.js +6 -3
  77. package/package.json +5 -5
  78. package/skills/peaks-code/SKILL.md +10 -10
  79. package/skills/peaks-code/references/mode-selection.md +7 -3
  80. package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
  81. package/skills/peaks-code/references/runbook.md +16 -5
  82. package/skills/peaks-code/references/session-overload-signal-index.md +2 -2
  83. package/skills/peaks-code/references/standards-preflight.md +1 -1
  84. package/skills/peaks-code/references/startup-sequence.md +1 -1
  85. package/skills/peaks-code/references/step-0-8-gate.md +1 -1
  86. package/skills/peaks-code/references/step-11-memory-sediment.md +35 -0
  87. package/skills/peaks-code/references/swarm-dispatch-contract.md +2 -2
  88. package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
@@ -1,6 +1,14 @@
1
1
  import type { MemoryIndex } from '../types.js';
2
2
  export declare function readMemoryFileMtime(filePath: string): string;
3
3
  export declare function readStoredMemoryNames(memoryDir: string): Set<string>;
4
- export declare function generateMemoryIndexFile(projectRoot: string, memoryDir: string, indexPath: string): void;
4
+ /**
5
+ * Build the hot/warm index object from the current `.peaks/memory/` contents
6
+ * without touching disk. Pure read + in-memory assembly; `generateMemoryIndexFile`
7
+ * serializes the result. Exposed so `peaks memory reindex` can reuse the exact
8
+ * same entry construction for its counts + `MEMORY.md` regeneration instead of
9
+ * duplicating it.
10
+ */
11
+ export declare function buildMemoryIndex(projectRoot: string): MemoryIndex;
12
+ export declare function generateMemoryIndexFile(projectRoot: string, memoryDir: string, indexPath: string, prebuilt?: MemoryIndex): void;
5
13
  export declare function readExistingIndex(indexPath: string): MemoryIndex | null;
6
14
  export declare function readMemoryIndex(projectRoot: string): MemoryIndex | null;
@@ -4,9 +4,9 @@
4
4
  // The memory index is the always-available summary of every memory in
5
5
  // `.peaks/memory/`. This module owns its write path:
6
6
  //
7
- // - `HOT_KINDS` — the kinds whose full body is kept in the index
8
- // (`feedback`, `decision`, `rule`, `convention`, `module`, `lesson`).
9
- // Anything not in `HOT_KINDS` lands in `warm` with the same shape.
7
+ // - `HOT_KINDS` — the kinds whose full body is kept in the index,
8
+ // derived from `MEMORY_KIND_TIER` in `../types.ts`. Anything not in
9
+ // `HOT_KINDS` lands in `warm` with the same shape.
10
10
  // - `readMemoryFileMtime` / `readStoredMemoryNames` — small stat /
11
11
  // filename helpers used by the index generator and by the slug-
12
12
  // collision idempotency check.
@@ -22,12 +22,13 @@
22
22
  // ---------------------------------------------------------------------------
23
23
  import { closeSync, constants, existsSync, openSync, readFileSync, statSync, writeFileSync } from 'node:fs';
24
24
  import { basename, join } from 'node:path';
25
+ import { HOT_MEMORY_KINDS, MEMORY_KIND_TIER, PROJECT_MEMORY_KINDS } from '../types.js';
25
26
  import { parseStoredMemoryFile } from '../parsers/frontmatter.js';
26
27
  import { summarizeMemoryBody } from '../parsers/markdown-pure.js';
27
28
  import { assertSafeProjectMemoryDir, normalizeRoot } from '../store/paths.js';
28
29
  import { ensureMemoryBootstrap, listMarkdownFiles, readProjectMemories } from './search.js';
29
- // Hot kinds: full body kept in index for always-available context
30
- const HOT_KINDS = new Set(['feedback', 'decision', 'rule', 'convention', 'module', 'lesson']);
30
+ // Hot kinds: full body kept in index for always-available context.
31
+ const HOT_KINDS = new Set(HOT_MEMORY_KINDS);
31
32
  export function readMemoryFileMtime(filePath) {
32
33
  try {
33
34
  return statSync(filePath).mtime.toISOString().slice(0, 10);
@@ -60,14 +61,22 @@ export function readStoredMemoryNames(memoryDir) {
60
61
  }
61
62
  return names;
62
63
  }
63
- export function generateMemoryIndexFile(projectRoot, memoryDir, indexPath) {
64
+ /**
65
+ * Build the hot/warm index object from the current `.peaks/memory/` contents
66
+ * without touching disk. Pure read + in-memory assembly; `generateMemoryIndexFile`
67
+ * serializes the result. Exposed so `peaks memory reindex` can reuse the exact
68
+ * same entry construction for its counts + `MEMORY.md` regeneration instead of
69
+ * duplicating it.
70
+ */
71
+ export function buildMemoryIndex(projectRoot) {
64
72
  const memories = readProjectMemories(projectRoot);
65
- const hot = {
66
- feedback: [], decision: [], rule: [], convention: [], module: [], lesson: []
67
- };
68
- const warm = {
69
- project: [], reference: []
70
- };
73
+ // Full-shape hot/warm buckets, derived from the canonical tier map so a
74
+ // newly accepted kind cannot be silently absent from the index.
75
+ const hot = {};
76
+ const warm = {};
77
+ for (const kind of PROJECT_MEMORY_KINDS) {
78
+ (MEMORY_KIND_TIER[kind] === 'hot' ? hot : warm)[kind] = [];
79
+ }
71
80
  for (const memory of memories.memories) {
72
81
  const entry = {
73
82
  name: memory.name,
@@ -89,12 +98,15 @@ export function generateMemoryIndexFile(projectRoot, memoryDir, indexPath) {
89
98
  if (arr)
90
99
  arr.sort((a, b) => a.name.localeCompare(b.name));
91
100
  }
92
- const index = {
101
+ return {
93
102
  version: 1,
94
103
  updatedAt: new Date().toISOString(),
95
104
  hot: hot,
96
105
  warm: warm
97
106
  };
107
+ }
108
+ export function generateMemoryIndexFile(projectRoot, memoryDir, indexPath, prebuilt) {
109
+ const index = prebuilt ?? buildMemoryIndex(projectRoot);
98
110
  const fd = openSync(indexPath, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC, 0o644);
99
111
  try {
100
112
  writeFileSync(fd, JSON.stringify(index, null, 2), 'utf8');
@@ -0,0 +1,75 @@
1
+ import type { MemoryIndex, ProjectMemoryKind } from '../types.js';
2
+ /** Marker line that owns `MEMORY.md`. Anything above it is machine output. */
3
+ export declare const MEMORY_MD_BANNER = "<!-- generated by `peaks memory reindex` \u2014 do not edit -->";
4
+ /** `MEMORY.md` is the generated index itself — never a memory source. */
5
+ export declare const MEMORY_MD_FILENAME = "MEMORY.md";
6
+ /**
7
+ * Deterministic section order for the generated `MEMORY.md`: hot kinds
8
+ * first (mirrors `HOT_KINDS` in `ranking.ts`), warm kinds last. Derived
9
+ * from the insertion order of `MEMORY_KIND_TIER` so the vocabulary has
10
+ * exactly one definition.
11
+ */
12
+ export declare const KIND_ORDER: readonly ProjectMemoryKind[];
13
+ export interface ReindexUnclassified {
14
+ name: string;
15
+ filePath: string;
16
+ /** The first raw type/kind value found in the frontmatter (null when absent). */
17
+ rawKind: string | null;
18
+ reason: string;
19
+ }
20
+ export interface ReindexOrphanEntry {
21
+ name: string;
22
+ kind: string;
23
+ /** The `sourcePath` recorded in the previous index that no longer exists on disk. */
24
+ sourcePath: string;
25
+ }
26
+ /**
27
+ * Two or more different files resolve to the same memory name (after the
28
+ * `name:` → `title:` → filename-stem fallback chain). Reported, never
29
+ * resolved by overwriting: both files keep their own index entry, and the
30
+ * collision is surfaced so a human/LLM can rename one of them.
31
+ */
32
+ export interface ReindexNameConflict {
33
+ name: string;
34
+ /** Every file that resolves to this name, sorted. Always length >= 2. */
35
+ filePaths: string[];
36
+ }
37
+ export interface MemoryReindexReport {
38
+ apply: boolean;
39
+ projectRoot: string;
40
+ memoryDir: string;
41
+ indexPath: string;
42
+ memoryMdPath: string;
43
+ /** Markdown files scanned (excludes the generated `MEMORY.md`). */
44
+ scannedFiles: number;
45
+ /** Files represented in the rebuilt index. */
46
+ indexed: number;
47
+ indexedByKind: Record<string, number>;
48
+ /** Files on disk with no resolvable kind — reported, never invented. */
49
+ unclassified: ReindexUnclassified[];
50
+ /** Distinct files that resolve to the same index name — reported, never overwritten. */
51
+ nameConflicts: ReindexNameConflict[];
52
+ /** Previous index entries whose `sourcePath` no longer exists (error-class drift). */
53
+ orphanIndex: ReindexOrphanEntry[];
54
+ /** Files on disk that the rebuilt index does not contain (warn-class drift). */
55
+ orphanDisk: string[];
56
+ memoryMd: {
57
+ path: string;
58
+ regenerated: boolean;
59
+ };
60
+ writtenFiles: string[];
61
+ }
62
+ export interface MemoryReindexOptions {
63
+ projectRoot: string;
64
+ apply?: boolean;
65
+ }
66
+ /**
67
+ * Render the human/LLM-facing `MEMORY.md` from a built index. Deterministic:
68
+ * kinds in `KIND_ORDER`, entries sorted by name, no timestamps.
69
+ */
70
+ export declare function renderMemoryMarkdown(index: MemoryIndex, memoryDir: string): string;
71
+ /**
72
+ * Rebuild `.peaks/memory/index.json` from disk and (on `--apply`) regenerate
73
+ * `MEMORY.md`. Always returns the drift report, even on a dry run.
74
+ */
75
+ export declare function executeMemoryReindex(options: MemoryReindexOptions): MemoryReindexReport;
@@ -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,81 @@
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
+ export declare function parseMemoryFrontmatter(content: string): ParsedMemoryFrontmatter;
60
+ /** Which frontmatter field (or the filename) supplied a memory's name. */
61
+ export type MemoryNameSource = 'name' | 'title' | 'stem' | 'none';
62
+ export interface MemoryNameResolution {
63
+ /** The resolved name, or null when every fallback was empty. */
64
+ name: string | null;
65
+ source: MemoryNameSource;
66
+ }
67
+ /**
68
+ * Deterministic name fallback chain for the read path:
69
+ *
70
+ * 1. `name:` — the canonical field written by `renderMemoryFile`
71
+ * 2. `title:` — hand-written / legacy files (the 5 on-disk files
72
+ * this slice fixes carried only `title:` + `kind:`)
73
+ * 3. filename stem — last resort, so a well-formed memory with a valid
74
+ * kind is never dropped just for missing a name
75
+ *
76
+ * Empty values are skipped rather than accepted: a `name:` of `''` still
77
+ * falls through, and a file whose stem is also empty resolves to null so the
78
+ * caller's validation is preserved (never invents a name).
79
+ */
80
+ export declare function resolveMemoryName(parsed: ParsedMemoryFrontmatter, filePath: string): MemoryNameResolution;
9
81
  export declare function parseStoredMemoryFile(content: string, filePath: string): StoredProjectMemory | null;