memorix 1.2.0 → 1.2.2

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 (212) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +18 -4
  3. package/README.zh-CN.md +18 -4
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +15919 -14055
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1997 -1021
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +8481 -8005
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +30 -1
  13. package/dist/sdk.d.ts +7 -2
  14. package/dist/sdk.js +2022 -1024
  15. package/dist/sdk.js.map +1 -1
  16. package/dist/types.d.ts +49 -1
  17. package/dist/types.js.map +1 -1
  18. package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
  19. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
  20. package/docs/API_REFERENCE.md +27 -5
  21. package/docs/DESIGN_DECISIONS.md +357 -357
  22. package/docs/DEVELOPMENT.md +4 -0
  23. package/docs/README.md +1 -1
  24. package/docs/SETUP.md +7 -1
  25. package/docs/dev-log/progress.txt +91 -11
  26. package/docs/knowledge/workflows/memorix-release.md +57 -0
  27. package/package.json +1 -1
  28. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  29. package/src/audit/index.ts +156 -156
  30. package/src/cli/command-guide.ts +192 -0
  31. package/src/cli/commands/audit-list.ts +89 -89
  32. package/src/cli/commands/audit.ts +9 -4
  33. package/src/cli/commands/background.ts +659 -659
  34. package/src/cli/commands/cleanup.ts +5 -1
  35. package/src/cli/commands/codegraph.ts +17 -8
  36. package/src/cli/commands/context.ts +3 -2
  37. package/src/cli/commands/doctor.ts +4 -2
  38. package/src/cli/commands/explain.ts +9 -3
  39. package/src/cli/commands/formation.ts +48 -48
  40. package/src/cli/commands/git-hook-install.ts +111 -111
  41. package/src/cli/commands/handoff.ts +75 -61
  42. package/src/cli/commands/hooks-status.ts +63 -63
  43. package/src/cli/commands/identity.ts +116 -0
  44. package/src/cli/commands/ingest-commit.ts +153 -153
  45. package/src/cli/commands/ingest-image.ts +71 -69
  46. package/src/cli/commands/ingest-log.ts +180 -180
  47. package/src/cli/commands/ingest.ts +44 -44
  48. package/src/cli/commands/integrate-shared.ts +15 -15
  49. package/src/cli/commands/knowledge.ts +40 -0
  50. package/src/cli/commands/lock.ts +93 -92
  51. package/src/cli/commands/memory.ts +58 -21
  52. package/src/cli/commands/message.ts +123 -118
  53. package/src/cli/commands/operator-shared.ts +98 -3
  54. package/src/cli/commands/poll.ts +74 -64
  55. package/src/cli/commands/purge-all-memory.ts +85 -85
  56. package/src/cli/commands/purge-project-memory.ts +83 -83
  57. package/src/cli/commands/reasoning.ts +135 -121
  58. package/src/cli/commands/retention.ts +9 -4
  59. package/src/cli/commands/serve-http.ts +22 -43
  60. package/src/cli/commands/serve-shared.ts +118 -118
  61. package/src/cli/commands/session.ts +29 -3
  62. package/src/cli/commands/setup.ts +9 -3
  63. package/src/cli/commands/skills.ts +124 -119
  64. package/src/cli/commands/status.ts +4 -3
  65. package/src/cli/commands/task.ts +193 -184
  66. package/src/cli/commands/team.ts +14 -10
  67. package/src/cli/commands/transfer.ts +108 -55
  68. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  69. package/src/cli/identity.ts +89 -0
  70. package/src/cli/index.ts +96 -19
  71. package/src/cli/invocation.ts +115 -0
  72. package/src/cli/tui/ChatView.tsx +234 -234
  73. package/src/cli/tui/CommandBar.tsx +312 -312
  74. package/src/cli/tui/ContextRail.tsx +118 -118
  75. package/src/cli/tui/HeaderBar.tsx +72 -72
  76. package/src/cli/tui/LogoBanner.tsx +51 -51
  77. package/src/cli/tui/Sidebar.tsx +179 -179
  78. package/src/cli/tui/chat-service.ts +41 -18
  79. package/src/cli/tui/data.ts +23 -44
  80. package/src/cli/tui/index.ts +41 -41
  81. package/src/cli/tui/markdown-render.tsx +371 -371
  82. package/src/cli/tui/operator-context.ts +60 -0
  83. package/src/cli/tui/use-mouse.ts +157 -157
  84. package/src/cli/tui/useNavigation.ts +56 -56
  85. package/src/cli/tui/views/MemoryView.tsx +10 -8
  86. package/src/cli/update-checker.ts +211 -211
  87. package/src/cli/version.ts +7 -7
  88. package/src/cli/workbench.ts +1 -1
  89. package/src/codegraph/auto-context.ts +34 -17
  90. package/src/codegraph/context-pack.ts +1 -0
  91. package/src/codegraph/current-facts.ts +19 -1
  92. package/src/codegraph/project-context.ts +2 -0
  93. package/src/codegraph/task-lens.ts +49 -5
  94. package/src/compact/engine.ts +26 -10
  95. package/src/compact/index-format.ts +25 -2
  96. package/src/compact/token-budget.ts +74 -74
  97. package/src/dashboard/project-classification.ts +64 -64
  98. package/src/dashboard/server.ts +58 -52
  99. package/src/embedding/fastembed-provider.ts +142 -142
  100. package/src/embedding/transformers-provider.ts +111 -111
  101. package/src/git/extractor.ts +209 -209
  102. package/src/git/hooks-path.ts +85 -85
  103. package/src/hooks/admission.ts +117 -0
  104. package/src/hooks/handler.ts +98 -91
  105. package/src/hooks/pattern-detector.ts +173 -173
  106. package/src/hooks/significance-filter.ts +250 -250
  107. package/src/knowledge/claims.ts +51 -1
  108. package/src/knowledge/context-assembly.ts +97 -0
  109. package/src/knowledge/types.ts +1 -0
  110. package/src/knowledge/workflows.ts +34 -3
  111. package/src/knowledge/workset.ts +179 -10
  112. package/src/llm/memory-manager.ts +328 -328
  113. package/src/llm/provider.ts +885 -885
  114. package/src/llm/quality.ts +248 -248
  115. package/src/memory/admission.ts +57 -0
  116. package/src/memory/attribution-guard.ts +249 -249
  117. package/src/memory/auto-relations.ts +21 -0
  118. package/src/memory/consolidation.ts +13 -2
  119. package/src/memory/disclosure-policy.ts +140 -135
  120. package/src/memory/entity-extractor.ts +197 -197
  121. package/src/memory/export-import.ts +11 -3
  122. package/src/memory/formation/evaluate.ts +217 -217
  123. package/src/memory/formation/extract.ts +361 -361
  124. package/src/memory/formation/index.ts +417 -417
  125. package/src/memory/formation/resolve.ts +344 -344
  126. package/src/memory/formation/types.ts +315 -315
  127. package/src/memory/freshness.ts +122 -122
  128. package/src/memory/graph-context.ts +8 -2
  129. package/src/memory/graph-scope.ts +46 -0
  130. package/src/memory/graph.ts +197 -197
  131. package/src/memory/observations.ts +162 -4
  132. package/src/memory/quality-audit.ts +2 -0
  133. package/src/memory/refs.ts +94 -94
  134. package/src/memory/retention.ts +22 -2
  135. package/src/memory/secret-filter.ts +79 -79
  136. package/src/memory/session.ts +5 -2
  137. package/src/memory/visibility.ts +80 -0
  138. package/src/multimodal/image-loader.ts +143 -143
  139. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  140. package/src/orchestrate/adapters/claude.ts +111 -111
  141. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  142. package/src/orchestrate/adapters/codex.ts +41 -41
  143. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  144. package/src/orchestrate/adapters/gemini.ts +42 -42
  145. package/src/orchestrate/adapters/index.ts +73 -73
  146. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  147. package/src/orchestrate/adapters/opencode.ts +47 -47
  148. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  149. package/src/orchestrate/adapters/types.ts +77 -77
  150. package/src/orchestrate/capability-router.ts +284 -284
  151. package/src/orchestrate/context-compact.ts +188 -188
  152. package/src/orchestrate/cost-tracker.ts +219 -219
  153. package/src/orchestrate/error-recovery.ts +191 -191
  154. package/src/orchestrate/evidence.ts +140 -140
  155. package/src/orchestrate/ledger.ts +110 -110
  156. package/src/orchestrate/memorix-bridge.ts +378 -340
  157. package/src/orchestrate/output-budget.ts +80 -80
  158. package/src/orchestrate/permission.ts +152 -152
  159. package/src/orchestrate/pipeline-trace.ts +131 -131
  160. package/src/orchestrate/prompt-builder.ts +155 -155
  161. package/src/orchestrate/ring-buffer.ts +37 -37
  162. package/src/orchestrate/task-graph.ts +389 -389
  163. package/src/orchestrate/verify-gate.ts +33 -10
  164. package/src/orchestrate/worktree.ts +232 -232
  165. package/src/project/aliases.ts +374 -374
  166. package/src/project/detector.ts +268 -268
  167. package/src/rules/adapters/claude-code.ts +99 -99
  168. package/src/rules/adapters/codex.ts +97 -97
  169. package/src/rules/adapters/copilot.ts +124 -124
  170. package/src/rules/adapters/cursor.ts +114 -114
  171. package/src/rules/adapters/kiro.ts +126 -126
  172. package/src/rules/adapters/trae.ts +56 -56
  173. package/src/rules/adapters/windsurf.ts +83 -83
  174. package/src/rules/syncer.ts +235 -235
  175. package/src/runtime/control-plane-maintenance.ts +1 -0
  176. package/src/runtime/isolated-maintenance.ts +1 -0
  177. package/src/runtime/lifecycle.ts +18 -0
  178. package/src/runtime/maintenance-jobs.ts +1 -0
  179. package/src/runtime/maintenance-runner.ts +2 -0
  180. package/src/runtime/project-maintenance.ts +89 -0
  181. package/src/sdk.ts +334 -304
  182. package/src/search/intent-detector.ts +289 -289
  183. package/src/search/query-expansion.ts +52 -52
  184. package/src/server/formation-timeout.ts +27 -27
  185. package/src/server.ts +334 -93
  186. package/src/skills/mini-skills.ts +386 -386
  187. package/src/store/chat-store.ts +119 -119
  188. package/src/store/graph-store.ts +249 -249
  189. package/src/store/mini-skill-store.ts +349 -349
  190. package/src/store/orama-store.ts +61 -6
  191. package/src/store/persistence-json.ts +212 -212
  192. package/src/store/persistence.ts +291 -291
  193. package/src/store/project-affinity.ts +195 -195
  194. package/src/store/sqlite-db.ts +23 -1
  195. package/src/store/sqlite-store.ts +12 -2
  196. package/src/team/event-bus.ts +76 -76
  197. package/src/team/file-locks.ts +173 -173
  198. package/src/team/handoff.ts +168 -161
  199. package/src/team/messages.ts +203 -203
  200. package/src/team/poll.ts +132 -132
  201. package/src/team/tasks.ts +211 -211
  202. package/src/types.ts +51 -0
  203. package/src/wiki/generator.ts +2 -0
  204. package/src/workspace/mcp-adapters/codex.ts +191 -191
  205. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  206. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  207. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  208. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  209. package/src/workspace/mcp-adapters/trae.ts +134 -134
  210. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  211. package/src/workspace/sanitizer.ts +60 -60
  212. 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
+ }