peaks-loop 4.0.34 → 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.
- package/CHANGELOG.md +18 -0
- package/dist/cli/commands/core/memory-command.js +61 -3
- package/dist/cli/commands/dispatch-commands.js +4 -2
- package/dist/cli/commands/memory-commands.d.ts +35 -0
- package/dist/cli/commands/memory-commands.js +119 -10
- 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/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/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 +75 -3
- package/dist/services/memory/project-memory-service/parsers/frontmatter.js +113 -24
- 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/peaks-code/SKILL.md +1 -1
- package/skills/peaks-code/references/runbook.md +6 -0
- package/skills/peaks-code/references/step-11-memory-sediment.md +35 -0
|
@@ -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
|
-
|
|
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
|
-
//
|
|
9
|
-
//
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
};
|
|
68
|
-
const warm = {
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,81 @@
|
|
|
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
|
+
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;
|