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.
Files changed (163) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +5199 -4726
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +428 -49
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.js +97 -18
  10. package/dist/maintenance-runner.js.map +1 -1
  11. package/dist/memcode-runtime/CHANGELOG.md +27 -0
  12. package/dist/sdk.js +428 -49
  13. package/dist/sdk.js.map +1 -1
  14. package/docs/1.2.4-PERSISTENT-MEMORY-DELIVERY.md +86 -0
  15. package/docs/AGENT_OPERATOR_PLAYBOOK.md +13 -1
  16. package/docs/API_REFERENCE.md +13 -3
  17. package/docs/DESIGN_DECISIONS.md +357 -357
  18. package/docs/dev-log/progress.txt +60 -9
  19. package/package.json +1 -1
  20. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  21. package/src/audit/index.ts +156 -156
  22. package/src/cli/capability-map.ts +1 -1
  23. package/src/cli/command-guide.ts +4 -1
  24. package/src/cli/commands/agent-integrations.ts +5 -1
  25. package/src/cli/commands/audit-list.ts +89 -89
  26. package/src/cli/commands/background.ts +659 -659
  27. package/src/cli/commands/codegraph.ts +1 -1
  28. package/src/cli/commands/context.ts +9 -1
  29. package/src/cli/commands/formation.ts +48 -48
  30. package/src/cli/commands/git-hook-install.ts +111 -111
  31. package/src/cli/commands/handoff.ts +54 -54
  32. package/src/cli/commands/hooks-status.ts +63 -63
  33. package/src/cli/commands/ingest-commit.ts +153 -153
  34. package/src/cli/commands/ingest-image.ts +66 -66
  35. package/src/cli/commands/ingest-log.ts +180 -180
  36. package/src/cli/commands/ingest.ts +44 -44
  37. package/src/cli/commands/integrate-shared.ts +15 -15
  38. package/src/cli/commands/lock.ts +82 -82
  39. package/src/cli/commands/message.ts +104 -104
  40. package/src/cli/commands/poll.ts +58 -58
  41. package/src/cli/commands/purge-all-memory.ts +85 -85
  42. package/src/cli/commands/purge-project-memory.ts +83 -83
  43. package/src/cli/commands/reasoning.ts +118 -118
  44. package/src/cli/commands/resume.ts +31 -0
  45. package/src/cli/commands/serve-shared.ts +118 -118
  46. package/src/cli/commands/session.ts +15 -7
  47. package/src/cli/commands/skills.ts +114 -114
  48. package/src/cli/commands/task.ts +167 -167
  49. package/src/cli/commands/transfer.ts +47 -47
  50. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  51. package/src/cli/index.ts +3 -1
  52. package/src/cli/tui/ChatView.tsx +234 -234
  53. package/src/cli/tui/CommandBar.tsx +312 -312
  54. package/src/cli/tui/ContextRail.tsx +118 -118
  55. package/src/cli/tui/HeaderBar.tsx +72 -72
  56. package/src/cli/tui/LogoBanner.tsx +51 -51
  57. package/src/cli/tui/Sidebar.tsx +179 -179
  58. package/src/cli/tui/index.ts +41 -41
  59. package/src/cli/tui/markdown-render.tsx +371 -371
  60. package/src/cli/tui/session-service.ts +3 -2
  61. package/src/cli/tui/use-mouse.ts +157 -157
  62. package/src/cli/tui/useNavigation.ts +56 -56
  63. package/src/cli/update-checker.ts +211 -211
  64. package/src/cli/version.ts +7 -7
  65. package/src/cli/workbench.ts +1 -1
  66. package/src/codegraph/auto-context.ts +54 -1
  67. package/src/codegraph/task-lens.ts +29 -0
  68. package/src/compact/token-budget.ts +89 -74
  69. package/src/config/toml-loader.ts +9 -5
  70. package/src/dashboard/project-classification.ts +64 -64
  71. package/src/embedding/fastembed-provider.ts +142 -142
  72. package/src/embedding/transformers-provider.ts +111 -111
  73. package/src/git/extractor.ts +209 -209
  74. package/src/git/hooks-path.ts +85 -85
  75. package/src/hooks/handler.ts +127 -66
  76. package/src/hooks/installers/index.ts +5 -4
  77. package/src/hooks/official-skills.ts +6 -4
  78. package/src/hooks/pattern-detector.ts +173 -173
  79. package/src/hooks/rules/memorix-agent-rules.md +9 -7
  80. package/src/hooks/significance-filter.ts +250 -250
  81. package/src/knowledge/context-assembly.ts +4 -1
  82. package/src/knowledge/workset.ts +89 -1
  83. package/src/llm/memory-manager.ts +328 -328
  84. package/src/llm/provider.ts +885 -885
  85. package/src/llm/quality.ts +248 -248
  86. package/src/memory/attribution-guard.ts +249 -249
  87. package/src/memory/disclosure-policy.ts +135 -135
  88. package/src/memory/entity-extractor.ts +197 -197
  89. package/src/memory/formation/evaluate.ts +217 -217
  90. package/src/memory/formation/extract.ts +361 -361
  91. package/src/memory/formation/index.ts +417 -417
  92. package/src/memory/formation/resolve.ts +344 -344
  93. package/src/memory/formation/types.ts +315 -315
  94. package/src/memory/freshness.ts +122 -122
  95. package/src/memory/graph.ts +197 -197
  96. package/src/memory/refs.ts +94 -94
  97. package/src/memory/secret-filter.ts +79 -79
  98. package/src/memory/session.ts +158 -9
  99. package/src/multimodal/image-loader.ts +143 -143
  100. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  101. package/src/orchestrate/adapters/claude.ts +111 -111
  102. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  103. package/src/orchestrate/adapters/codex.ts +41 -41
  104. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  105. package/src/orchestrate/adapters/gemini.ts +42 -42
  106. package/src/orchestrate/adapters/index.ts +73 -73
  107. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  108. package/src/orchestrate/adapters/opencode.ts +47 -47
  109. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  110. package/src/orchestrate/adapters/types.ts +77 -77
  111. package/src/orchestrate/capability-router.ts +284 -284
  112. package/src/orchestrate/context-compact.ts +188 -188
  113. package/src/orchestrate/cost-tracker.ts +219 -219
  114. package/src/orchestrate/error-recovery.ts +191 -191
  115. package/src/orchestrate/evidence.ts +140 -140
  116. package/src/orchestrate/ledger.ts +110 -110
  117. package/src/orchestrate/memorix-bridge.ts +343 -343
  118. package/src/orchestrate/output-budget.ts +80 -80
  119. package/src/orchestrate/permission.ts +152 -152
  120. package/src/orchestrate/pipeline-trace.ts +131 -131
  121. package/src/orchestrate/prompt-builder.ts +155 -155
  122. package/src/orchestrate/ring-buffer.ts +37 -37
  123. package/src/orchestrate/task-graph.ts +389 -389
  124. package/src/orchestrate/worktree.ts +232 -232
  125. package/src/project/aliases.ts +374 -374
  126. package/src/project/detector.ts +268 -268
  127. package/src/rules/adapters/claude-code.ts +99 -99
  128. package/src/rules/adapters/codex.ts +97 -97
  129. package/src/rules/adapters/copilot.ts +124 -124
  130. package/src/rules/adapters/cursor.ts +114 -114
  131. package/src/rules/adapters/kiro.ts +126 -126
  132. package/src/rules/adapters/trae.ts +56 -56
  133. package/src/rules/adapters/windsurf.ts +83 -83
  134. package/src/rules/syncer.ts +235 -235
  135. package/src/sdk.ts +299 -299
  136. package/src/search/intent-detector.ts +289 -289
  137. package/src/search/query-expansion.ts +52 -52
  138. package/src/server/formation-timeout.ts +27 -27
  139. package/src/server.ts +144 -10
  140. package/src/skills/mini-skills.ts +386 -386
  141. package/src/store/bun-sqlite-compat.ts +118 -15
  142. package/src/store/chat-store.ts +119 -119
  143. package/src/store/graph-store.ts +249 -249
  144. package/src/store/mini-skill-store.ts +349 -349
  145. package/src/store/persistence-json.ts +212 -212
  146. package/src/store/persistence.ts +291 -291
  147. package/src/store/project-affinity.ts +195 -195
  148. package/src/store/sqlite-db.ts +3 -3
  149. package/src/team/event-bus.ts +76 -76
  150. package/src/team/file-locks.ts +173 -173
  151. package/src/team/handoff.ts +161 -161
  152. package/src/team/messages.ts +203 -203
  153. package/src/team/poll.ts +132 -132
  154. package/src/team/tasks.ts +211 -211
  155. package/src/workspace/mcp-adapters/codex.ts +191 -191
  156. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  157. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  158. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  159. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  160. package/src/workspace/mcp-adapters/trae.ts +134 -134
  161. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  162. package/src/workspace/sanitizer.ts +60 -60
  163. 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
+ }
@@ -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
+ }