memorix 1.2.2 → 1.2.4
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 +27 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/TEAM.md +86 -86
- package/dist/cli/index.js +5199 -4726
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +428 -49
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.js +97 -18
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +27 -0
- package/dist/sdk.js +428 -49
- package/dist/sdk.js.map +1 -1
- package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +86 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +13 -1
- package/docs/API_REFERENCE.md +13 -3
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/docs/dev-log/progress.txt +60 -9
- package/package.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/src/audit/index.ts +156 -156
- package/src/cli/capability-map.ts +1 -1
- package/src/cli/command-guide.ts +4 -1
- package/src/cli/commands/agent-integrations.ts +5 -1
- package/src/cli/commands/audit-list.ts +89 -89
- package/src/cli/commands/background.ts +659 -659
- package/src/cli/commands/codegraph.ts +1 -1
- package/src/cli/commands/context.ts +9 -1
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +54 -54
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest-commit.ts +153 -153
- package/src/cli/commands/ingest-image.ts +66 -66
- package/src/cli/commands/ingest-log.ts +180 -180
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +82 -82
- package/src/cli/commands/message.ts +104 -104
- package/src/cli/commands/poll.ts +58 -58
- package/src/cli/commands/purge-all-memory.ts +85 -85
- package/src/cli/commands/purge-project-memory.ts +83 -83
- package/src/cli/commands/reasoning.ts +118 -118
- package/src/cli/commands/resume.ts +31 -0
- package/src/cli/commands/serve-shared.ts +118 -118
- package/src/cli/commands/session.ts +15 -7
- package/src/cli/commands/skills.ts +114 -114
- package/src/cli/commands/task.ts +167 -167
- package/src/cli/commands/transfer.ts +47 -47
- package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/Sidebar.tsx +179 -179
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/tui/markdown-render.tsx +371 -371
- package/src/cli/tui/session-service.ts +3 -2
- package/src/cli/tui/use-mouse.ts +157 -157
- package/src/cli/tui/useNavigation.ts +56 -56
- package/src/cli/update-checker.ts +211 -211
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/codegraph/auto-context.ts +54 -1
- package/src/codegraph/task-lens.ts +29 -0
- package/src/compact/token-budget.ts +89 -74
- package/src/config/toml-loader.ts +9 -5
- package/src/dashboard/project-classification.ts +64 -64
- package/src/embedding/fastembed-provider.ts +142 -142
- package/src/embedding/transformers-provider.ts +111 -111
- package/src/git/extractor.ts +209 -209
- package/src/git/hooks-path.ts +85 -85
- package/src/hooks/handler.ts +127 -66
- package/src/hooks/installers/index.ts +5 -4
- package/src/hooks/official-skills.ts +6 -4
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/rules/memorix-agent-rules.md +9 -7
- package/src/hooks/significance-filter.ts +250 -250
- package/src/knowledge/context-assembly.ts +4 -1
- package/src/knowledge/workset.ts +89 -1
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/provider.ts +885 -885
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/disclosure-policy.ts +135 -135
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/formation/extract.ts +361 -361
- package/src/memory/formation/index.ts +417 -417
- package/src/memory/formation/resolve.ts +344 -344
- package/src/memory/formation/types.ts +315 -315
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/refs.ts +94 -94
- package/src/memory/secret-filter.ts +79 -79
- package/src/memory/session.ts +158 -9
- package/src/multimodal/image-loader.ts +143 -143
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/spawn-helper.ts +286 -286
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/context-compact.ts +188 -188
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +343 -343
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/orchestrate/worktree.ts +232 -232
- package/src/project/aliases.ts +374 -374
- package/src/project/detector.ts +268 -268
- package/src/rules/adapters/claude-code.ts +99 -99
- package/src/rules/adapters/codex.ts +97 -97
- package/src/rules/adapters/copilot.ts +124 -124
- package/src/rules/adapters/cursor.ts +114 -114
- package/src/rules/adapters/kiro.ts +126 -126
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/adapters/windsurf.ts +83 -83
- package/src/rules/syncer.ts +235 -235
- package/src/sdk.ts +299 -299
- package/src/search/intent-detector.ts +289 -289
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/server.ts +144 -10
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/bun-sqlite-compat.ts +118 -15
- package/src/store/chat-store.ts +119 -119
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/store/sqlite-db.ts +3 -3
- package/src/team/event-bus.ts +76 -76
- package/src/team/file-locks.ts +173 -173
- package/src/team/handoff.ts +161 -161
- package/src/team/messages.ts +203 -203
- package/src/team/poll.ts +132 -132
- package/src/team/tasks.ts +211 -211
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
- package/src/workspace/workflow-sync.ts +131 -131
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transformers.js Provider
|
|
3
|
-
*
|
|
4
|
-
* Pure JavaScript embedding using @huggingface/transformers (HuggingFace).
|
|
5
|
-
* Model: Xenova/all-MiniLM-L6-v2 (384 dimensions, ~22MB quantized)
|
|
6
|
-
*
|
|
7
|
-
* Key advantages over fastembed:
|
|
8
|
-
* - No native ONNX binding required (pure JS / WASM)
|
|
9
|
-
* - Works out-of-the-box on Windows, macOS, Linux
|
|
10
|
-
* - Supports quantized models (q8, q4) for smaller footprint
|
|
11
|
-
*
|
|
12
|
-
* This is an optional dependency — if @huggingface/transformers is not
|
|
13
|
-
* installed, the provider module gracefully falls back to the next option.
|
|
14
|
-
*
|
|
15
|
-
* Inspired by Mem0's multi-provider embedding architecture.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { EmbeddingProvider } from './provider.js';
|
|
19
|
-
|
|
20
|
-
// In-memory LRU cache
|
|
21
|
-
const cache = new Map<string, number[]>();
|
|
22
|
-
const MAX_CACHE_SIZE = 5000;
|
|
23
|
-
|
|
24
|
-
export class TransformersProvider implements EmbeddingProvider {
|
|
25
|
-
readonly name = 'transformers-minilm';
|
|
26
|
-
readonly dimensions = 384;
|
|
27
|
-
|
|
28
|
-
private extractor: any; // Pipeline instance
|
|
29
|
-
|
|
30
|
-
private constructor(extractor: any) {
|
|
31
|
-
this.extractor = extractor;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Initialize the Transformers.js provider.
|
|
36
|
-
* Downloads model on first use (~22MB quantized), cached locally after.
|
|
37
|
-
*/
|
|
38
|
-
static async create(): Promise<TransformersProvider> {
|
|
39
|
-
// Dynamic import — throws if @huggingface/transformers is not installed
|
|
40
|
-
const { pipeline } = await import('@huggingface/transformers');
|
|
41
|
-
const extractor = await pipeline(
|
|
42
|
-
'feature-extraction',
|
|
43
|
-
'Xenova/all-MiniLM-L6-v2',
|
|
44
|
-
{ dtype: 'q8' }, // Quantized for small footprint
|
|
45
|
-
);
|
|
46
|
-
return new TransformersProvider(extractor);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async embed(text: string): Promise<number[]> {
|
|
50
|
-
// Check cache first
|
|
51
|
-
const cached = cache.get(text);
|
|
52
|
-
if (cached) return cached;
|
|
53
|
-
|
|
54
|
-
const output = await this.extractor(text, {
|
|
55
|
-
pooling: 'mean',
|
|
56
|
-
normalize: true,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// output.tolist() returns [[...384 floats]]
|
|
60
|
-
const result: number[] = Array.from(output.tolist()[0]);
|
|
61
|
-
if (result.length !== this.dimensions) {
|
|
62
|
-
throw new Error(`Expected ${this.dimensions}d embedding, got ${result.length}d`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
this.cacheSet(text, result);
|
|
66
|
-
return result;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async embedBatch(texts: string[]): Promise<number[][]> {
|
|
70
|
-
const results: number[][] = new Array(texts.length);
|
|
71
|
-
const uncachedIndices: number[] = [];
|
|
72
|
-
const uncachedTexts: string[] = [];
|
|
73
|
-
|
|
74
|
-
// Check cache for each text
|
|
75
|
-
for (let i = 0; i < texts.length; i++) {
|
|
76
|
-
const cached = cache.get(texts[i]);
|
|
77
|
-
if (cached) {
|
|
78
|
-
results[i] = cached;
|
|
79
|
-
} else {
|
|
80
|
-
uncachedIndices.push(i);
|
|
81
|
-
uncachedTexts.push(texts[i]);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Batch embed uncached texts
|
|
86
|
-
if (uncachedTexts.length > 0) {
|
|
87
|
-
const output = await this.extractor(uncachedTexts, {
|
|
88
|
-
pooling: 'mean',
|
|
89
|
-
normalize: true,
|
|
90
|
-
});
|
|
91
|
-
const allVecs: number[][] = output.tolist();
|
|
92
|
-
|
|
93
|
-
for (let i = 0; i < allVecs.length; i++) {
|
|
94
|
-
const vec = Array.from(allVecs[i]) as number[];
|
|
95
|
-
const originalIdx = uncachedIndices[i];
|
|
96
|
-
results[originalIdx] = vec;
|
|
97
|
-
this.cacheSet(uncachedTexts[i], vec);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return results;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private cacheSet(key: string, value: number[]): void {
|
|
105
|
-
if (cache.size >= MAX_CACHE_SIZE) {
|
|
106
|
-
const firstKey = cache.keys().next().value;
|
|
107
|
-
if (firstKey !== undefined) cache.delete(firstKey);
|
|
108
|
-
}
|
|
109
|
-
cache.set(key, value);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Transformers.js Provider
|
|
3
|
+
*
|
|
4
|
+
* Pure JavaScript embedding using @huggingface/transformers (HuggingFace).
|
|
5
|
+
* Model: Xenova/all-MiniLM-L6-v2 (384 dimensions, ~22MB quantized)
|
|
6
|
+
*
|
|
7
|
+
* Key advantages over fastembed:
|
|
8
|
+
* - No native ONNX binding required (pure JS / WASM)
|
|
9
|
+
* - Works out-of-the-box on Windows, macOS, Linux
|
|
10
|
+
* - Supports quantized models (q8, q4) for smaller footprint
|
|
11
|
+
*
|
|
12
|
+
* This is an optional dependency — if @huggingface/transformers is not
|
|
13
|
+
* installed, the provider module gracefully falls back to the next option.
|
|
14
|
+
*
|
|
15
|
+
* Inspired by Mem0's multi-provider embedding architecture.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { EmbeddingProvider } from './provider.js';
|
|
19
|
+
|
|
20
|
+
// In-memory LRU cache
|
|
21
|
+
const cache = new Map<string, number[]>();
|
|
22
|
+
const MAX_CACHE_SIZE = 5000;
|
|
23
|
+
|
|
24
|
+
export class TransformersProvider implements EmbeddingProvider {
|
|
25
|
+
readonly name = 'transformers-minilm';
|
|
26
|
+
readonly dimensions = 384;
|
|
27
|
+
|
|
28
|
+
private extractor: any; // Pipeline instance
|
|
29
|
+
|
|
30
|
+
private constructor(extractor: any) {
|
|
31
|
+
this.extractor = extractor;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Initialize the Transformers.js provider.
|
|
36
|
+
* Downloads model on first use (~22MB quantized), cached locally after.
|
|
37
|
+
*/
|
|
38
|
+
static async create(): Promise<TransformersProvider> {
|
|
39
|
+
// Dynamic import — throws if @huggingface/transformers is not installed
|
|
40
|
+
const { pipeline } = await import('@huggingface/transformers');
|
|
41
|
+
const extractor = await pipeline(
|
|
42
|
+
'feature-extraction',
|
|
43
|
+
'Xenova/all-MiniLM-L6-v2',
|
|
44
|
+
{ dtype: 'q8' }, // Quantized for small footprint
|
|
45
|
+
);
|
|
46
|
+
return new TransformersProvider(extractor);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async embed(text: string): Promise<number[]> {
|
|
50
|
+
// Check cache first
|
|
51
|
+
const cached = cache.get(text);
|
|
52
|
+
if (cached) return cached;
|
|
53
|
+
|
|
54
|
+
const output = await this.extractor(text, {
|
|
55
|
+
pooling: 'mean',
|
|
56
|
+
normalize: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// output.tolist() returns [[...384 floats]]
|
|
60
|
+
const result: number[] = Array.from(output.tolist()[0]);
|
|
61
|
+
if (result.length !== this.dimensions) {
|
|
62
|
+
throw new Error(`Expected ${this.dimensions}d embedding, got ${result.length}d`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.cacheSet(text, result);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async embedBatch(texts: string[]): Promise<number[][]> {
|
|
70
|
+
const results: number[][] = new Array(texts.length);
|
|
71
|
+
const uncachedIndices: number[] = [];
|
|
72
|
+
const uncachedTexts: string[] = [];
|
|
73
|
+
|
|
74
|
+
// Check cache for each text
|
|
75
|
+
for (let i = 0; i < texts.length; i++) {
|
|
76
|
+
const cached = cache.get(texts[i]);
|
|
77
|
+
if (cached) {
|
|
78
|
+
results[i] = cached;
|
|
79
|
+
} else {
|
|
80
|
+
uncachedIndices.push(i);
|
|
81
|
+
uncachedTexts.push(texts[i]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Batch embed uncached texts
|
|
86
|
+
if (uncachedTexts.length > 0) {
|
|
87
|
+
const output = await this.extractor(uncachedTexts, {
|
|
88
|
+
pooling: 'mean',
|
|
89
|
+
normalize: true,
|
|
90
|
+
});
|
|
91
|
+
const allVecs: number[][] = output.tolist();
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < allVecs.length; i++) {
|
|
94
|
+
const vec = Array.from(allVecs[i]) as number[];
|
|
95
|
+
const originalIdx = uncachedIndices[i];
|
|
96
|
+
results[originalIdx] = vec;
|
|
97
|
+
this.cacheSet(uncachedTexts[i], vec);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private cacheSet(key: string, value: number[]): void {
|
|
105
|
+
if (cache.size >= MAX_CACHE_SIZE) {
|
|
106
|
+
const firstKey = cache.keys().next().value;
|
|
107
|
+
if (firstKey !== undefined) cache.delete(firstKey);
|
|
108
|
+
}
|
|
109
|
+
cache.set(key, value);
|
|
110
|
+
}
|
|
111
|
+
}
|
package/src/git/extractor.ts
CHANGED
|
@@ -1,209 +1,209 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git Knowledge Extractor
|
|
3
|
-
*
|
|
4
|
-
* Extracts engineering knowledge from git commits, diffs, and logs.
|
|
5
|
-
* This is the Git→Memory direction — no other memory project does this.
|
|
6
|
-
*
|
|
7
|
-
* Git records code changes. Memorix records engineering knowledge.
|
|
8
|
-
* This module bridges the gap.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { execSync } from 'node:child_process';
|
|
12
|
-
|
|
13
|
-
export interface CommitInfo {
|
|
14
|
-
hash: string;
|
|
15
|
-
shortHash: string;
|
|
16
|
-
author: string;
|
|
17
|
-
date: string;
|
|
18
|
-
subject: string;
|
|
19
|
-
body: string;
|
|
20
|
-
filesChanged: string[];
|
|
21
|
-
insertions: number;
|
|
22
|
-
deletions: number;
|
|
23
|
-
diffSummary: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface IngestResult {
|
|
27
|
-
commit: CommitInfo;
|
|
28
|
-
entityName: string;
|
|
29
|
-
type: string;
|
|
30
|
-
title: string;
|
|
31
|
-
narrative: string;
|
|
32
|
-
facts: string[];
|
|
33
|
-
concepts: string[];
|
|
34
|
-
filesModified: string[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Get commit info by hash (or HEAD).
|
|
39
|
-
*/
|
|
40
|
-
export function getCommitInfo(cwd: string, ref = 'HEAD'): CommitInfo {
|
|
41
|
-
const FORMAT = '%H%n%h%n%aN%n%aI%n%s%n%b';
|
|
42
|
-
const raw = execSync(
|
|
43
|
-
`git log -1 --format="${FORMAT}" ${ref}`,
|
|
44
|
-
{ cwd, encoding: 'utf-8', timeout: 10000 },
|
|
45
|
-
).trim();
|
|
46
|
-
|
|
47
|
-
const lines = raw.split('\n');
|
|
48
|
-
const hash = lines[0];
|
|
49
|
-
const shortHash = lines[1];
|
|
50
|
-
const author = lines[2];
|
|
51
|
-
const date = lines[3];
|
|
52
|
-
const subject = lines[4];
|
|
53
|
-
const body = lines.slice(5).join('\n').trim();
|
|
54
|
-
|
|
55
|
-
// Get diff stat
|
|
56
|
-
const stat = execSync(
|
|
57
|
-
`git diff-tree --no-commit-id --numstat ${hash}`,
|
|
58
|
-
{ cwd, encoding: 'utf-8', timeout: 10000 },
|
|
59
|
-
).trim();
|
|
60
|
-
|
|
61
|
-
let insertions = 0;
|
|
62
|
-
let deletions = 0;
|
|
63
|
-
const filesChanged: string[] = [];
|
|
64
|
-
|
|
65
|
-
for (const line of stat.split('\n').filter(Boolean)) {
|
|
66
|
-
const parts = line.split('\t');
|
|
67
|
-
if (parts.length >= 3) {
|
|
68
|
-
const ins = parseInt(parts[0], 10) || 0;
|
|
69
|
-
const del = parseInt(parts[1], 10) || 0;
|
|
70
|
-
insertions += ins;
|
|
71
|
-
deletions += del;
|
|
72
|
-
filesChanged.push(parts[2]);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Get short diff summary (first 500 chars of diff)
|
|
77
|
-
let diffSummary = '';
|
|
78
|
-
try {
|
|
79
|
-
const diff = execSync(
|
|
80
|
-
`git diff-tree -p --no-commit-id ${hash}`,
|
|
81
|
-
{ cwd, encoding: 'utf-8', timeout: 10000, maxBuffer: 1024 * 100 },
|
|
82
|
-
);
|
|
83
|
-
diffSummary = diff.substring(0, 500);
|
|
84
|
-
} catch { /* diff may be too large */ }
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
hash, shortHash, author, date, subject, body,
|
|
88
|
-
filesChanged, insertions, deletions, diffSummary,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Get recent N commits.
|
|
94
|
-
*/
|
|
95
|
-
export function getRecentCommits(cwd: string, count = 10): CommitInfo[] {
|
|
96
|
-
const hashes = execSync(
|
|
97
|
-
`git log --format="%H" -${count}`,
|
|
98
|
-
{ cwd, encoding: 'utf-8', timeout: 10000 },
|
|
99
|
-
).trim().split('\n').filter(Boolean);
|
|
100
|
-
|
|
101
|
-
return hashes.map(hash => getCommitInfo(cwd, hash));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Infer observation type from commit message.
|
|
106
|
-
*/
|
|
107
|
-
function inferType(subject: string, body: string): string {
|
|
108
|
-
// Priority 1: conventional commit prefix (e.g., "fix:", "feat:", "test:")
|
|
109
|
-
const prefix = subject.match(/^(\w+)[\s(:]/)?.[1]?.toLowerCase() ?? '';
|
|
110
|
-
const prefixMap: Record<string, string> = {
|
|
111
|
-
fix: 'problem-solution', bugfix: 'problem-solution', hotfix: 'problem-solution',
|
|
112
|
-
feat: 'what-changed', feature: 'what-changed',
|
|
113
|
-
refactor: 'what-changed', chore: 'what-changed',
|
|
114
|
-
docs: 'how-it-works', doc: 'how-it-works',
|
|
115
|
-
test: 'discovery', spec: 'discovery',
|
|
116
|
-
ci: 'what-changed', cd: 'what-changed',
|
|
117
|
-
security: 'gotcha',
|
|
118
|
-
revert: 'problem-solution',
|
|
119
|
-
deprecate: 'decision',
|
|
120
|
-
perf: 'trade-off',
|
|
121
|
-
};
|
|
122
|
-
if (prefixMap[prefix]) return prefixMap[prefix];
|
|
123
|
-
|
|
124
|
-
// Priority 2: keyword scan in full text
|
|
125
|
-
const text = `${subject} ${body}`.toLowerCase();
|
|
126
|
-
if (/\b(fix|bug|crash|error|issue|hotfix|patch)\b/.test(text)) return 'problem-solution';
|
|
127
|
-
if (/\b(feat|feature|add|implement|new)\b/.test(text)) return 'what-changed';
|
|
128
|
-
if (/\b(refactor|clean|restructure|reorganize)\b/.test(text)) return 'what-changed';
|
|
129
|
-
if (/\b(chore|bump|version|release|publish)\b/.test(text)) return 'what-changed';
|
|
130
|
-
if (/\b(doc|readme|changelog)\b/.test(text)) return 'how-it-works';
|
|
131
|
-
if (/\b(test|spec|e2e|coverage)\b/.test(text)) return 'discovery';
|
|
132
|
-
if (/\b(ci|cd|workflow|deploy|docker)\b/.test(text)) return 'what-changed';
|
|
133
|
-
if (/\b(security|vulnerability|cve)\b/.test(text)) return 'gotcha';
|
|
134
|
-
if (/\b(revert|rollback)\b/.test(text)) return 'problem-solution';
|
|
135
|
-
if (/\b(deprecat|remove|drop)\b/.test(text)) return 'decision';
|
|
136
|
-
if (/\b(perf|optim|speed|cache)\b/.test(text)) return 'trade-off';
|
|
137
|
-
|
|
138
|
-
return 'what-changed';
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Extract concepts from commit.
|
|
143
|
-
*/
|
|
144
|
-
function extractConcepts(commit: CommitInfo): string[] {
|
|
145
|
-
const concepts: string[] = [];
|
|
146
|
-
const text = `${commit.subject} ${commit.body}`;
|
|
147
|
-
|
|
148
|
-
// Extract technology/library mentions
|
|
149
|
-
const techPatterns = /\b(react|vue|angular|express|fastify|redis|postgres|mysql|docker|nginx|webpack|vite|tsup|vitest|jest)\b/gi;
|
|
150
|
-
for (const match of text.matchAll(techPatterns)) {
|
|
151
|
-
concepts.push(match[0].toLowerCase());
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Extract conventional commit scope
|
|
155
|
-
const scopeMatch = commit.subject.match(/^\w+\(([^)]+)\)/);
|
|
156
|
-
if (scopeMatch) {
|
|
157
|
-
concepts.push(scopeMatch[1]);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return [...new Set(concepts)];
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Convert a commit into a memory-ready ingest result.
|
|
165
|
-
*/
|
|
166
|
-
export function ingestCommit(commit: CommitInfo): IngestResult {
|
|
167
|
-
const type = inferType(commit.subject, commit.body);
|
|
168
|
-
|
|
169
|
-
// Entity name from conventional commit scope or first file directory
|
|
170
|
-
let entityName = 'project';
|
|
171
|
-
const scopeMatch = commit.subject.match(/^\w+\(([^)]+)\)/);
|
|
172
|
-
if (scopeMatch) {
|
|
173
|
-
entityName = scopeMatch[1];
|
|
174
|
-
} else if (commit.filesChanged.length > 0) {
|
|
175
|
-
const firstDir = commit.filesChanged[0].split('/')[0];
|
|
176
|
-
if (firstDir && firstDir !== '.') entityName = firstDir;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Build narrative from commit message + stats
|
|
180
|
-
const parts: string[] = [];
|
|
181
|
-
parts.push(commit.subject);
|
|
182
|
-
if (commit.body) parts.push(commit.body);
|
|
183
|
-
parts.push(`\n[${commit.shortHash}] ${commit.author} @ ${commit.date}`);
|
|
184
|
-
parts.push(`Files: ${commit.filesChanged.length} changed (+${commit.insertions}/-${commit.deletions})`);
|
|
185
|
-
|
|
186
|
-
const narrative = parts.join('\n');
|
|
187
|
-
|
|
188
|
-
// Extract facts
|
|
189
|
-
const facts: string[] = [];
|
|
190
|
-
facts.push(`Commit: ${commit.shortHash}`);
|
|
191
|
-
facts.push(`Author: ${commit.author}`);
|
|
192
|
-
if (commit.filesChanged.length <= 5) {
|
|
193
|
-
facts.push(`Files: ${commit.filesChanged.join(', ')}`);
|
|
194
|
-
} else {
|
|
195
|
-
facts.push(`Files: ${commit.filesChanged.length} changed`);
|
|
196
|
-
}
|
|
197
|
-
facts.push(`Changes: +${commit.insertions}/-${commit.deletions}`);
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
commit,
|
|
201
|
-
entityName,
|
|
202
|
-
type,
|
|
203
|
-
title: commit.subject.length > 60 ? commit.subject.substring(0, 60) + '...' : commit.subject,
|
|
204
|
-
narrative,
|
|
205
|
-
facts,
|
|
206
|
-
concepts: extractConcepts(commit),
|
|
207
|
-
filesModified: commit.filesChanged,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Git Knowledge Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts engineering knowledge from git commits, diffs, and logs.
|
|
5
|
+
* This is the Git→Memory direction — no other memory project does this.
|
|
6
|
+
*
|
|
7
|
+
* Git records code changes. Memorix records engineering knowledge.
|
|
8
|
+
* This module bridges the gap.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync } from 'node:child_process';
|
|
12
|
+
|
|
13
|
+
export interface CommitInfo {
|
|
14
|
+
hash: string;
|
|
15
|
+
shortHash: string;
|
|
16
|
+
author: string;
|
|
17
|
+
date: string;
|
|
18
|
+
subject: string;
|
|
19
|
+
body: string;
|
|
20
|
+
filesChanged: string[];
|
|
21
|
+
insertions: number;
|
|
22
|
+
deletions: number;
|
|
23
|
+
diffSummary: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface IngestResult {
|
|
27
|
+
commit: CommitInfo;
|
|
28
|
+
entityName: string;
|
|
29
|
+
type: string;
|
|
30
|
+
title: string;
|
|
31
|
+
narrative: string;
|
|
32
|
+
facts: string[];
|
|
33
|
+
concepts: string[];
|
|
34
|
+
filesModified: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get commit info by hash (or HEAD).
|
|
39
|
+
*/
|
|
40
|
+
export function getCommitInfo(cwd: string, ref = 'HEAD'): CommitInfo {
|
|
41
|
+
const FORMAT = '%H%n%h%n%aN%n%aI%n%s%n%b';
|
|
42
|
+
const raw = execSync(
|
|
43
|
+
`git log -1 --format="${FORMAT}" ${ref}`,
|
|
44
|
+
{ cwd, encoding: 'utf-8', timeout: 10000 },
|
|
45
|
+
).trim();
|
|
46
|
+
|
|
47
|
+
const lines = raw.split('\n');
|
|
48
|
+
const hash = lines[0];
|
|
49
|
+
const shortHash = lines[1];
|
|
50
|
+
const author = lines[2];
|
|
51
|
+
const date = lines[3];
|
|
52
|
+
const subject = lines[4];
|
|
53
|
+
const body = lines.slice(5).join('\n').trim();
|
|
54
|
+
|
|
55
|
+
// Get diff stat
|
|
56
|
+
const stat = execSync(
|
|
57
|
+
`git diff-tree --no-commit-id --numstat ${hash}`,
|
|
58
|
+
{ cwd, encoding: 'utf-8', timeout: 10000 },
|
|
59
|
+
).trim();
|
|
60
|
+
|
|
61
|
+
let insertions = 0;
|
|
62
|
+
let deletions = 0;
|
|
63
|
+
const filesChanged: string[] = [];
|
|
64
|
+
|
|
65
|
+
for (const line of stat.split('\n').filter(Boolean)) {
|
|
66
|
+
const parts = line.split('\t');
|
|
67
|
+
if (parts.length >= 3) {
|
|
68
|
+
const ins = parseInt(parts[0], 10) || 0;
|
|
69
|
+
const del = parseInt(parts[1], 10) || 0;
|
|
70
|
+
insertions += ins;
|
|
71
|
+
deletions += del;
|
|
72
|
+
filesChanged.push(parts[2]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Get short diff summary (first 500 chars of diff)
|
|
77
|
+
let diffSummary = '';
|
|
78
|
+
try {
|
|
79
|
+
const diff = execSync(
|
|
80
|
+
`git diff-tree -p --no-commit-id ${hash}`,
|
|
81
|
+
{ cwd, encoding: 'utf-8', timeout: 10000, maxBuffer: 1024 * 100 },
|
|
82
|
+
);
|
|
83
|
+
diffSummary = diff.substring(0, 500);
|
|
84
|
+
} catch { /* diff may be too large */ }
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
hash, shortHash, author, date, subject, body,
|
|
88
|
+
filesChanged, insertions, deletions, diffSummary,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get recent N commits.
|
|
94
|
+
*/
|
|
95
|
+
export function getRecentCommits(cwd: string, count = 10): CommitInfo[] {
|
|
96
|
+
const hashes = execSync(
|
|
97
|
+
`git log --format="%H" -${count}`,
|
|
98
|
+
{ cwd, encoding: 'utf-8', timeout: 10000 },
|
|
99
|
+
).trim().split('\n').filter(Boolean);
|
|
100
|
+
|
|
101
|
+
return hashes.map(hash => getCommitInfo(cwd, hash));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Infer observation type from commit message.
|
|
106
|
+
*/
|
|
107
|
+
function inferType(subject: string, body: string): string {
|
|
108
|
+
// Priority 1: conventional commit prefix (e.g., "fix:", "feat:", "test:")
|
|
109
|
+
const prefix = subject.match(/^(\w+)[\s(:]/)?.[1]?.toLowerCase() ?? '';
|
|
110
|
+
const prefixMap: Record<string, string> = {
|
|
111
|
+
fix: 'problem-solution', bugfix: 'problem-solution', hotfix: 'problem-solution',
|
|
112
|
+
feat: 'what-changed', feature: 'what-changed',
|
|
113
|
+
refactor: 'what-changed', chore: 'what-changed',
|
|
114
|
+
docs: 'how-it-works', doc: 'how-it-works',
|
|
115
|
+
test: 'discovery', spec: 'discovery',
|
|
116
|
+
ci: 'what-changed', cd: 'what-changed',
|
|
117
|
+
security: 'gotcha',
|
|
118
|
+
revert: 'problem-solution',
|
|
119
|
+
deprecate: 'decision',
|
|
120
|
+
perf: 'trade-off',
|
|
121
|
+
};
|
|
122
|
+
if (prefixMap[prefix]) return prefixMap[prefix];
|
|
123
|
+
|
|
124
|
+
// Priority 2: keyword scan in full text
|
|
125
|
+
const text = `${subject} ${body}`.toLowerCase();
|
|
126
|
+
if (/\b(fix|bug|crash|error|issue|hotfix|patch)\b/.test(text)) return 'problem-solution';
|
|
127
|
+
if (/\b(feat|feature|add|implement|new)\b/.test(text)) return 'what-changed';
|
|
128
|
+
if (/\b(refactor|clean|restructure|reorganize)\b/.test(text)) return 'what-changed';
|
|
129
|
+
if (/\b(chore|bump|version|release|publish)\b/.test(text)) return 'what-changed';
|
|
130
|
+
if (/\b(doc|readme|changelog)\b/.test(text)) return 'how-it-works';
|
|
131
|
+
if (/\b(test|spec|e2e|coverage)\b/.test(text)) return 'discovery';
|
|
132
|
+
if (/\b(ci|cd|workflow|deploy|docker)\b/.test(text)) return 'what-changed';
|
|
133
|
+
if (/\b(security|vulnerability|cve)\b/.test(text)) return 'gotcha';
|
|
134
|
+
if (/\b(revert|rollback)\b/.test(text)) return 'problem-solution';
|
|
135
|
+
if (/\b(deprecat|remove|drop)\b/.test(text)) return 'decision';
|
|
136
|
+
if (/\b(perf|optim|speed|cache)\b/.test(text)) return 'trade-off';
|
|
137
|
+
|
|
138
|
+
return 'what-changed';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Extract concepts from commit.
|
|
143
|
+
*/
|
|
144
|
+
function extractConcepts(commit: CommitInfo): string[] {
|
|
145
|
+
const concepts: string[] = [];
|
|
146
|
+
const text = `${commit.subject} ${commit.body}`;
|
|
147
|
+
|
|
148
|
+
// Extract technology/library mentions
|
|
149
|
+
const techPatterns = /\b(react|vue|angular|express|fastify|redis|postgres|mysql|docker|nginx|webpack|vite|tsup|vitest|jest)\b/gi;
|
|
150
|
+
for (const match of text.matchAll(techPatterns)) {
|
|
151
|
+
concepts.push(match[0].toLowerCase());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Extract conventional commit scope
|
|
155
|
+
const scopeMatch = commit.subject.match(/^\w+\(([^)]+)\)/);
|
|
156
|
+
if (scopeMatch) {
|
|
157
|
+
concepts.push(scopeMatch[1]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return [...new Set(concepts)];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Convert a commit into a memory-ready ingest result.
|
|
165
|
+
*/
|
|
166
|
+
export function ingestCommit(commit: CommitInfo): IngestResult {
|
|
167
|
+
const type = inferType(commit.subject, commit.body);
|
|
168
|
+
|
|
169
|
+
// Entity name from conventional commit scope or first file directory
|
|
170
|
+
let entityName = 'project';
|
|
171
|
+
const scopeMatch = commit.subject.match(/^\w+\(([^)]+)\)/);
|
|
172
|
+
if (scopeMatch) {
|
|
173
|
+
entityName = scopeMatch[1];
|
|
174
|
+
} else if (commit.filesChanged.length > 0) {
|
|
175
|
+
const firstDir = commit.filesChanged[0].split('/')[0];
|
|
176
|
+
if (firstDir && firstDir !== '.') entityName = firstDir;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Build narrative from commit message + stats
|
|
180
|
+
const parts: string[] = [];
|
|
181
|
+
parts.push(commit.subject);
|
|
182
|
+
if (commit.body) parts.push(commit.body);
|
|
183
|
+
parts.push(`\n[${commit.shortHash}] ${commit.author} @ ${commit.date}`);
|
|
184
|
+
parts.push(`Files: ${commit.filesChanged.length} changed (+${commit.insertions}/-${commit.deletions})`);
|
|
185
|
+
|
|
186
|
+
const narrative = parts.join('\n');
|
|
187
|
+
|
|
188
|
+
// Extract facts
|
|
189
|
+
const facts: string[] = [];
|
|
190
|
+
facts.push(`Commit: ${commit.shortHash}`);
|
|
191
|
+
facts.push(`Author: ${commit.author}`);
|
|
192
|
+
if (commit.filesChanged.length <= 5) {
|
|
193
|
+
facts.push(`Files: ${commit.filesChanged.join(', ')}`);
|
|
194
|
+
} else {
|
|
195
|
+
facts.push(`Files: ${commit.filesChanged.length} changed`);
|
|
196
|
+
}
|
|
197
|
+
facts.push(`Changes: +${commit.insertions}/-${commit.deletions}`);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
commit,
|
|
201
|
+
entityName,
|
|
202
|
+
type,
|
|
203
|
+
title: commit.subject.length > 60 ? commit.subject.substring(0, 60) + '...' : commit.subject,
|
|
204
|
+
narrative,
|
|
205
|
+
facts,
|
|
206
|
+
concepts: extractConcepts(commit),
|
|
207
|
+
filesModified: commit.filesChanged,
|
|
208
|
+
};
|
|
209
|
+
}
|