claude-memory-layer 1.0.11 → 1.0.13
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/AGENTS.md +60 -0
- package/README.md +166 -2
- package/bootstrap-kb/decisions/decisions.md +244 -0
- package/bootstrap-kb/glossary/glossary.md +46 -0
- package/bootstrap-kb/modules/.claude-plugin.md +22 -0
- package/bootstrap-kb/modules/agents.md.md +15 -0
- package/bootstrap-kb/modules/claude.md.md +15 -0
- package/bootstrap-kb/modules/context.md.md +15 -0
- package/bootstrap-kb/modules/docs.md +18 -0
- package/bootstrap-kb/modules/handoff.md.md +15 -0
- package/bootstrap-kb/modules/package-lock.json.md +15 -0
- package/bootstrap-kb/modules/package.json.md +15 -0
- package/bootstrap-kb/modules/plan.md.md +15 -0
- package/bootstrap-kb/modules/readme.md.md +15 -0
- package/bootstrap-kb/modules/scripts.md +26 -0
- package/bootstrap-kb/modules/spec.md.md +15 -0
- package/bootstrap-kb/modules/specs.md +20 -0
- package/bootstrap-kb/modules/src.md +51 -0
- package/bootstrap-kb/modules/tests.md +42 -0
- package/bootstrap-kb/modules/tsconfig.json.md +15 -0
- package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
- package/bootstrap-kb/overview/overview.md +40 -0
- package/bootstrap-kb/sources/manifest.json +950 -0
- package/bootstrap-kb/sources/manifest.md +227 -0
- package/bootstrap-kb/timeline/timeline.md +57 -0
- package/d.sh +3 -0
- package/deploy.sh +3 -0
- package/dist/cli/index.js +2389 -286
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1017 -132
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +1347 -202
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +1339 -194
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1343 -198
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1351 -206
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1347 -202
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1436 -211
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1445 -220
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1345 -199
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +69 -2
- package/dist/ui/index.html +8 -0
- package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
- package/docs/MEMU_ADOPTION.md +40 -0
- package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
- package/memory/_index.md +405 -0
- package/memory/default/uncategorized/2026-02-25.md +4839 -0
- package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
- package/memory/specs/citations-system/2026-02-25.md +1121 -0
- package/memory/specs/endless-mode/2026-02-25.md +1392 -0
- package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
- package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
- package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
- package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
- package/memory/specs/private-tags/2026-02-25.md +1057 -0
- package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
- package/memory/specs/task-entity-system/2026-02-25.md +924 -0
- package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
- package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
- package/package.json +2 -1
- package/scripts/build.ts +6 -0
- package/scripts/bump-patch-version.sh +18 -0
- package/src/cli/index.ts +281 -2
- package/src/core/consolidated-store.ts +63 -1
- package/src/core/consolidation-worker.ts +115 -6
- package/src/core/event-store.ts +14 -0
- package/src/core/index.ts +1 -0
- package/src/core/ingest-interceptor.ts +80 -0
- package/src/core/markdown-mirror.ts +70 -0
- package/src/core/md-mirror.ts +92 -0
- package/src/core/mongo-sync-config.ts +165 -0
- package/src/core/mongo-sync-worker.ts +381 -0
- package/src/core/retriever.ts +540 -150
- package/src/core/sqlite-event-store.ts +350 -1
- package/src/core/tag-taxonomy.ts +51 -0
- package/src/core/types.ts +28 -0
- package/src/server/api/health.ts +53 -0
- package/src/server/api/index.ts +3 -1
- package/src/server/api/stats.ts +46 -1
- package/src/services/bootstrap-organizer.ts +443 -0
- package/src/services/codex-session-history-importer.ts +474 -0
- package/src/services/memory-service.ts +373 -68
- package/src/services/session-history-importer.ts +53 -25
- package/src/ui/app.js +69 -2
- package/src/ui/index.html +8 -0
- package/tests/bootstrap-organizer.test.ts +111 -0
- package/tests/consolidation-worker.test.ts +75 -0
- package/tests/ingest-interceptor.test.ts +38 -0
- package/tests/markdown-mirror.test.ts +85 -0
- package/tests/md-mirror.test.ts +50 -0
- package/tests/retriever-fallback-chain.test.ts +223 -0
- package/tests/retriever-strategy-scope.test.ts +97 -0
- package/tests/retriever.memu-adoption.test.ts +122 -0
- package/tests/sqlite-event-store-replication.test.ts +92 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
export interface BootstrapKnowledgeOptions {
|
|
6
|
+
repoPath: string;
|
|
7
|
+
outDir: string;
|
|
8
|
+
since?: string;
|
|
9
|
+
maxCommits?: number;
|
|
10
|
+
incremental?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface CommitInfo {
|
|
14
|
+
hash: string;
|
|
15
|
+
date: string;
|
|
16
|
+
author: string;
|
|
17
|
+
subject: string;
|
|
18
|
+
files: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ModuleSummary {
|
|
22
|
+
name: string;
|
|
23
|
+
root: string;
|
|
24
|
+
fileCount: number;
|
|
25
|
+
languages: string[];
|
|
26
|
+
entryCandidates: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const EXCLUDED_DIRS = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.next', '.turbo', 'memory']);
|
|
30
|
+
const CODE_EXTENSIONS = new Set([
|
|
31
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.kt', '.swift', '.rb', '.php', '.cs',
|
|
32
|
+
'.scala', '.sh', '.zsh', '.yaml', '.yml', '.json', '.sql', '.md'
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function safeRel(base: string, target: string): string {
|
|
36
|
+
return path.relative(base, target).replaceAll('\\', '/');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mkdirp(dir: string): void {
|
|
40
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function walkCodeFiles(root: string): string[] {
|
|
44
|
+
const out: string[] = [];
|
|
45
|
+
|
|
46
|
+
function walk(dir: string): void {
|
|
47
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
48
|
+
|
|
49
|
+
for (const e of entries) {
|
|
50
|
+
const full = path.join(dir, e.name);
|
|
51
|
+
if (e.isDirectory()) {
|
|
52
|
+
if (!EXCLUDED_DIRS.has(e.name)) walk(full);
|
|
53
|
+
} else if (e.isFile()) {
|
|
54
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
55
|
+
if (CODE_EXTENSIONS.has(ext)) out.push(full);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
walk(root);
|
|
61
|
+
return out.sort();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function detectLanguage(file: string): string {
|
|
65
|
+
const ext = path.extname(file).toLowerCase();
|
|
66
|
+
const map: Record<string, string> = {
|
|
67
|
+
'.ts': 'TypeScript', '.tsx': 'TypeScript', '.js': 'JavaScript', '.jsx': 'JavaScript', '.mjs': 'JavaScript', '.cjs': 'JavaScript',
|
|
68
|
+
'.py': 'Python', '.go': 'Go', '.rs': 'Rust', '.java': 'Java', '.kt': 'Kotlin', '.swift': 'Swift', '.rb': 'Ruby', '.php': 'PHP',
|
|
69
|
+
'.cs': 'C#', '.scala': 'Scala', '.sh': 'Shell', '.zsh': 'Shell', '.yaml': 'YAML', '.yml': 'YAML', '.json': 'JSON', '.sql': 'SQL', '.md': 'Markdown'
|
|
70
|
+
};
|
|
71
|
+
return map[ext] || 'Other';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function summarizeModules(repoPath: string, files: string[]): ModuleSummary[] {
|
|
75
|
+
const modules = new Map<string, { files: string[]; langs: Map<string, number> }>();
|
|
76
|
+
|
|
77
|
+
for (const abs of files) {
|
|
78
|
+
const rel = safeRel(repoPath, abs);
|
|
79
|
+
const seg = rel.split('/').filter(Boolean);
|
|
80
|
+
const top = seg[0] || 'root';
|
|
81
|
+
|
|
82
|
+
if (!modules.has(top)) modules.set(top, { files: [], langs: new Map() });
|
|
83
|
+
|
|
84
|
+
const bucket = modules.get(top)!;
|
|
85
|
+
bucket.files.push(rel);
|
|
86
|
+
|
|
87
|
+
const lang = detectLanguage(abs);
|
|
88
|
+
bucket.langs.set(lang, (bucket.langs.get(lang) || 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [...modules.entries()]
|
|
92
|
+
.map(([name, data]) => ({
|
|
93
|
+
name,
|
|
94
|
+
root: name,
|
|
95
|
+
fileCount: data.files.length,
|
|
96
|
+
languages: [...data.langs.entries()].sort((a, b) => b[1] - a[1]).map(([l]) => l).slice(0, 5),
|
|
97
|
+
entryCandidates: data.files.filter((f) => /(index|main|app|server|cli)\./i.test(path.basename(f))).slice(0, 10)
|
|
98
|
+
}))
|
|
99
|
+
.sort((a, b) => b.fileCount - a.fileCount || a.name.localeCompare(b.name));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function runGit(repoPath: string, command: string): string {
|
|
103
|
+
return execSync(`git -C ${JSON.stringify(repoPath)} ${command}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getGitCommits(repoPath: string, since = '180 days ago', maxCommits = 1000): CommitInfo[] {
|
|
107
|
+
try {
|
|
108
|
+
const raw = runGit(
|
|
109
|
+
repoPath,
|
|
110
|
+
`log --since=${JSON.stringify(since)} -n ${Math.max(1, maxCommits)} --date=short --pretty=format:%H%x09%ad%x09%an%x09%s --name-only --reverse`
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const lines = raw.split(/\r?\n/);
|
|
114
|
+
const commits: CommitInfo[] = [];
|
|
115
|
+
let current: CommitInfo | null = null;
|
|
116
|
+
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
if (!line.trim()) {
|
|
119
|
+
if (current) {
|
|
120
|
+
commits.push(current);
|
|
121
|
+
current = null;
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (line.includes('\t') && line.split('\t').length >= 4) {
|
|
127
|
+
if (current) commits.push(current);
|
|
128
|
+
const [hash, date, author, ...subjectRest] = line.split('\t');
|
|
129
|
+
current = { hash, date, author, subject: subjectRest.join('\t').trim(), files: [] };
|
|
130
|
+
} else if (current) {
|
|
131
|
+
current.files.push(line.trim());
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (current) commits.push(current);
|
|
136
|
+
return commits;
|
|
137
|
+
} catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function extractDecisions(commits: CommitInfo[]): CommitInfo[] {
|
|
143
|
+
const decisionPattern = /(refactor|migrate|deprecat|remove|replace|introduce|adopt|switch|upgrade|breaking|architecture|feat|fix)/i;
|
|
144
|
+
return commits.filter((c) => decisionPattern.test(c.subject));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildTimeline(commits: CommitInfo[]): Map<string, CommitInfo[]> {
|
|
148
|
+
const timeline = new Map<string, CommitInfo[]>();
|
|
149
|
+
for (const c of commits) {
|
|
150
|
+
const key = (c.date || '').slice(0, 7) || 'unknown';
|
|
151
|
+
if (!timeline.has(key)) timeline.set(key, []);
|
|
152
|
+
timeline.get(key)!.push(c);
|
|
153
|
+
}
|
|
154
|
+
return new Map([...timeline.entries()].sort((a, b) => a[0].localeCompare(b[0])));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildGlossary(files: string[]): string[] {
|
|
158
|
+
const stop = new Set(['src', 'test', 'dist', 'lib', 'core', 'index', 'main', 'app', 'server', 'client', 'utils']);
|
|
159
|
+
const freq = new Map<string, number>();
|
|
160
|
+
|
|
161
|
+
for (const f of files) {
|
|
162
|
+
const base = path.basename(f, path.extname(f));
|
|
163
|
+
const tokens = base
|
|
164
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
165
|
+
.flatMap((t) => t.split(/(?=[A-Z])/))
|
|
166
|
+
.map((t) => t.toLowerCase())
|
|
167
|
+
.filter((t) => t.length >= 3 && !stop.has(t));
|
|
168
|
+
|
|
169
|
+
for (const t of tokens) freq.set(t, (freq.get(t) || 0) + 1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return [...freq.entries()]
|
|
173
|
+
.filter(([, count]) => count >= 2)
|
|
174
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
175
|
+
.slice(0, 80)
|
|
176
|
+
.map(([term]) => term);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function writeFile(filePath: string, content: string): void {
|
|
180
|
+
mkdirp(path.dirname(filePath));
|
|
181
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function confidenceByEvidence(sourceCount: number): 'high' | 'mid' | 'low' {
|
|
185
|
+
if (sourceCount >= 3) return 'high';
|
|
186
|
+
if (sourceCount >= 1) return 'mid';
|
|
187
|
+
return 'low';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function sourceLine(source: string): string {
|
|
191
|
+
return `- source: ${source}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface ExistingManifest {
|
|
195
|
+
generatedAt?: string;
|
|
196
|
+
lastCommitDate?: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function loadExistingManifest(outDir: string): ExistingManifest | null {
|
|
200
|
+
try {
|
|
201
|
+
const p = path.join(outDir, 'sources', 'manifest.json');
|
|
202
|
+
if (!fs.existsSync(p)) return null;
|
|
203
|
+
const data = JSON.parse(fs.readFileSync(p, 'utf8')) as ExistingManifest;
|
|
204
|
+
return data;
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function listMarkdownOutputs(outDir: string): string[] {
|
|
211
|
+
const out: string[] = [];
|
|
212
|
+
const stack = [outDir];
|
|
213
|
+
while (stack.length) {
|
|
214
|
+
const dir = stack.pop()!;
|
|
215
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
216
|
+
const full = path.join(dir, entry.name);
|
|
217
|
+
if (entry.isDirectory()) stack.push(full);
|
|
218
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) out.push(full);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return out.sort((a, b) => a.localeCompare(b));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function bootstrapKnowledgeBase(options: BootstrapKnowledgeOptions): Promise<{
|
|
225
|
+
outDir: string;
|
|
226
|
+
fileCount: number;
|
|
227
|
+
moduleCount: number;
|
|
228
|
+
commitCount: number;
|
|
229
|
+
generatedFiles: string[];
|
|
230
|
+
}> {
|
|
231
|
+
const repoPath = path.resolve(options.repoPath);
|
|
232
|
+
const outDir = path.resolve(options.outDir);
|
|
233
|
+
const maxCommits = options.maxCommits ?? 1000;
|
|
234
|
+
|
|
235
|
+
const existingManifest = options.incremental ? loadExistingManifest(outDir) : null;
|
|
236
|
+
const incrementalSince = existingManifest?.lastCommitDate || existingManifest?.generatedAt;
|
|
237
|
+
const since = options.since || incrementalSince || '180 days ago';
|
|
238
|
+
|
|
239
|
+
const codeFiles = walkCodeFiles(repoPath);
|
|
240
|
+
const modules = summarizeModules(repoPath, codeFiles);
|
|
241
|
+
const commits = getGitCommits(repoPath, since, maxCommits);
|
|
242
|
+
const decisions = extractDecisions(commits);
|
|
243
|
+
const timeline = buildTimeline(commits);
|
|
244
|
+
const glossary = buildGlossary(codeFiles);
|
|
245
|
+
|
|
246
|
+
const generatedFiles: string[] = [];
|
|
247
|
+
|
|
248
|
+
const sections = {
|
|
249
|
+
overview: path.join(outDir, 'overview'),
|
|
250
|
+
modules: path.join(outDir, 'modules'),
|
|
251
|
+
decisions: path.join(outDir, 'decisions'),
|
|
252
|
+
timeline: path.join(outDir, 'timeline'),
|
|
253
|
+
glossary: path.join(outDir, 'glossary'),
|
|
254
|
+
sources: path.join(outDir, 'sources')
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
for (const sectionDir of Object.values(sections)) {
|
|
258
|
+
mkdirp(sectionDir);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const overviewPath = path.join(sections.overview, 'overview.md');
|
|
262
|
+
const overview = [
|
|
263
|
+
'# Codebase Overview',
|
|
264
|
+
'',
|
|
265
|
+
`- generatedAt: ${new Date().toISOString()}`,
|
|
266
|
+
'- deterministicPipeline: true',
|
|
267
|
+
`- repo: ${repoPath}`,
|
|
268
|
+
`- filesAnalyzed: ${codeFiles.length}`,
|
|
269
|
+
`- commitsAnalyzed: ${commits.length}`,
|
|
270
|
+
`- confidence: ${confidenceByEvidence(modules.length > 0 ? 3 : 0)}`,
|
|
271
|
+
'',
|
|
272
|
+
'## Directory / Module Map',
|
|
273
|
+
...modules.slice(0, 50).map((m) => `- ${m.name}: ${m.fileCount} files (${m.languages.join(', ') || 'n/a'})`),
|
|
274
|
+
'',
|
|
275
|
+
'## Fact',
|
|
276
|
+
'- Generated from deterministic file scan and git history parsing.',
|
|
277
|
+
'',
|
|
278
|
+
'## Inference',
|
|
279
|
+
'- Module responsibilities should be reviewed by maintainers for nuanced boundaries.',
|
|
280
|
+
'',
|
|
281
|
+
'## Sources',
|
|
282
|
+
sourceLine(`repo-scan:${repoPath}`),
|
|
283
|
+
sourceLine(`git-log:since=${since};max=${maxCommits}`),
|
|
284
|
+
''
|
|
285
|
+
].join('\n');
|
|
286
|
+
writeFile(overviewPath, overview);
|
|
287
|
+
generatedFiles.push(overviewPath);
|
|
288
|
+
|
|
289
|
+
const touchedRoots = new Set(
|
|
290
|
+
commits
|
|
291
|
+
.flatMap((c) => c.files)
|
|
292
|
+
.map((f) => f.split('/').filter(Boolean)[0])
|
|
293
|
+
.filter(Boolean)
|
|
294
|
+
);
|
|
295
|
+
const moduleTargets = options.incremental && touchedRoots.size > 0
|
|
296
|
+
? modules.filter((m) => touchedRoots.has(m.root)).slice(0, 200)
|
|
297
|
+
: modules.slice(0, 200);
|
|
298
|
+
|
|
299
|
+
for (const m of moduleTargets) {
|
|
300
|
+
const relatedCommits = commits.filter((c) => c.files.some((f) => f.startsWith(`${m.root}/`))).slice(0, 15);
|
|
301
|
+
const content = [
|
|
302
|
+
`# Module: ${m.name}`,
|
|
303
|
+
'',
|
|
304
|
+
`- responsibility: inferred from top-level path \`${m.root}/\``,
|
|
305
|
+
`- files: ${m.fileCount}`,
|
|
306
|
+
`- languages: ${m.languages.join(', ') || 'n/a'}`,
|
|
307
|
+
`- confidence: ${confidenceByEvidence(relatedCommits.length)}`,
|
|
308
|
+
'',
|
|
309
|
+
'## Entry Candidates',
|
|
310
|
+
...(m.entryCandidates.length > 0 ? m.entryCandidates.map((f) => `- ${f}`) : ['- none detected']),
|
|
311
|
+
'',
|
|
312
|
+
'## Related Commits (recent sample)',
|
|
313
|
+
...(relatedCommits.length > 0
|
|
314
|
+
? relatedCommits.map((c) => `- ${c.date} ${c.hash.slice(0, 8)} ${c.subject}`)
|
|
315
|
+
: ['- none in selected range']),
|
|
316
|
+
'',
|
|
317
|
+
'## Sources',
|
|
318
|
+
sourceLine(`repo-path:${m.root}/**`),
|
|
319
|
+
...relatedCommits.map((c) => sourceLine(`commit:${c.hash}`)),
|
|
320
|
+
''
|
|
321
|
+
].join('\n');
|
|
322
|
+
|
|
323
|
+
const modulePath = path.join(sections.modules, `${m.name.replace(/[^a-z0-9._-]+/gi, '-').toLowerCase()}.md`);
|
|
324
|
+
writeFile(modulePath, content);
|
|
325
|
+
generatedFiles.push(modulePath);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const decisionsPath = path.join(sections.decisions, 'decisions.md');
|
|
329
|
+
const decisionsMd = [
|
|
330
|
+
'# Decisions (extracted)',
|
|
331
|
+
'',
|
|
332
|
+
`- confidence: ${confidenceByEvidence(decisions.length)}`,
|
|
333
|
+
'',
|
|
334
|
+
...(decisions.length > 0
|
|
335
|
+
? decisions.slice(0, 500).map((d) => [
|
|
336
|
+
`## ${d.date} | ${d.subject}`,
|
|
337
|
+
'- status: active (inferred)',
|
|
338
|
+
sourceLine(`commit:${d.hash}`),
|
|
339
|
+
`- author: ${d.author}`,
|
|
340
|
+
`- changedFiles: ${d.files.length}`,
|
|
341
|
+
`- confidence: ${confidenceByEvidence(d.files.length > 0 ? 2 : 1)}`,
|
|
342
|
+
''
|
|
343
|
+
].join('\n'))
|
|
344
|
+
: ['- No decision-like commits found in selected range.', '']),
|
|
345
|
+
'## Sources',
|
|
346
|
+
sourceLine(`git-log:since=${since};max=${maxCommits}`),
|
|
347
|
+
''
|
|
348
|
+
].join('\n');
|
|
349
|
+
writeFile(decisionsPath, decisionsMd);
|
|
350
|
+
generatedFiles.push(decisionsPath);
|
|
351
|
+
|
|
352
|
+
const timelinePath = path.join(sections.timeline, 'timeline.md');
|
|
353
|
+
const timelineMd = [
|
|
354
|
+
'# Timeline',
|
|
355
|
+
'',
|
|
356
|
+
`- confidence: ${confidenceByEvidence(commits.length > 0 ? 2 : 0)}`,
|
|
357
|
+
'',
|
|
358
|
+
...[...timeline.entries()].flatMap(([month, list]) => [
|
|
359
|
+
`## ${month}`,
|
|
360
|
+
...list.slice(0, 40).map((c) => `- ${c.date} ${c.hash.slice(0, 8)} ${c.subject}`),
|
|
361
|
+
''
|
|
362
|
+
]),
|
|
363
|
+
'## Sources',
|
|
364
|
+
sourceLine(`git-log:since=${since};max=${maxCommits}`),
|
|
365
|
+
''
|
|
366
|
+
].join('\n');
|
|
367
|
+
writeFile(timelinePath, timelineMd);
|
|
368
|
+
generatedFiles.push(timelinePath);
|
|
369
|
+
|
|
370
|
+
const glossaryPath = path.join(sections.glossary, 'glossary.md');
|
|
371
|
+
const glossaryMd = [
|
|
372
|
+
'# Glossary (auto-extracted)',
|
|
373
|
+
'',
|
|
374
|
+
`- confidence: ${confidenceByEvidence(glossary.length > 0 ? 1 : 0)}`,
|
|
375
|
+
'',
|
|
376
|
+
...glossary.map((t) => `- ${t}`),
|
|
377
|
+
'',
|
|
378
|
+
'## Sources',
|
|
379
|
+
sourceLine(`repo-scan:${repoPath}`),
|
|
380
|
+
''
|
|
381
|
+
].join('\n');
|
|
382
|
+
writeFile(glossaryPath, glossaryMd);
|
|
383
|
+
generatedFiles.push(glossaryPath);
|
|
384
|
+
|
|
385
|
+
const outputs = generatedFiles.map((f) => safeRel(outDir, f)).sort((a, b) => a.localeCompare(b));
|
|
386
|
+
const allOutputs = listMarkdownOutputs(outDir).map((f) => safeRel(outDir, f));
|
|
387
|
+
|
|
388
|
+
const sourceItems = [
|
|
389
|
+
...codeFiles.slice(0, 200).map((f) => ({ type: 'file', ref: safeRel(repoPath, f) })),
|
|
390
|
+
...commits.slice(0, 400).map((c) => ({ type: 'commit', ref: c.hash, date: c.date, subject: c.subject }))
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
const latestCommitDate = commits.length > 0 ? commits[commits.length - 1].date : undefined;
|
|
394
|
+
const manifest = {
|
|
395
|
+
generatedAt: new Date().toISOString(),
|
|
396
|
+
deterministicPipeline: true,
|
|
397
|
+
mode: options.incremental ? 'incremental' : 'full',
|
|
398
|
+
repoPath,
|
|
399
|
+
options: { since, maxCommits, incremental: Boolean(options.incremental) },
|
|
400
|
+
stats: {
|
|
401
|
+
filesAnalyzed: codeFiles.length,
|
|
402
|
+
modules: modules.length,
|
|
403
|
+
modulesGenerated: moduleTargets.length,
|
|
404
|
+
commits: commits.length,
|
|
405
|
+
decisions: decisions.length,
|
|
406
|
+
glossaryTerms: glossary.length
|
|
407
|
+
},
|
|
408
|
+
lastCommitDate: latestCommitDate,
|
|
409
|
+
outputs,
|
|
410
|
+
allOutputs,
|
|
411
|
+
sources: sourceItems
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const manifestJsonPath = path.join(sections.sources, 'manifest.json');
|
|
415
|
+
writeFile(manifestJsonPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
416
|
+
generatedFiles.push(manifestJsonPath);
|
|
417
|
+
|
|
418
|
+
const manifestMdPath = path.join(sections.sources, 'manifest.md');
|
|
419
|
+
const manifestMd = [
|
|
420
|
+
'# Sources Manifest',
|
|
421
|
+
'',
|
|
422
|
+
'- deterministicPipeline: true',
|
|
423
|
+
`- mode: ${options.incremental ? 'incremental' : 'full'}`,
|
|
424
|
+
`- sourceCount: ${sourceItems.length}`,
|
|
425
|
+
'',
|
|
426
|
+
'## Outputs',
|
|
427
|
+
...outputs.map((o) => `- ${o}`),
|
|
428
|
+
'',
|
|
429
|
+
'## Sources (sample)',
|
|
430
|
+
...sourceItems.slice(0, 300).map((s) => `- ${s.type}:${s.ref}`),
|
|
431
|
+
''
|
|
432
|
+
].join('\n');
|
|
433
|
+
writeFile(manifestMdPath, manifestMd);
|
|
434
|
+
generatedFiles.push(manifestMdPath);
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
outDir,
|
|
438
|
+
fileCount: codeFiles.length,
|
|
439
|
+
moduleCount: modules.length,
|
|
440
|
+
commitCount: commits.length,
|
|
441
|
+
generatedFiles: generatedFiles.sort((a, b) => a.localeCompare(b))
|
|
442
|
+
};
|
|
443
|
+
}
|