magector 1.4.1 → 1.4.3

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/README.md CHANGED
@@ -64,6 +64,8 @@ Without Magector, asking Claude Code or Cursor *"how are checkout totals calcula
64
64
  - **Adobe Commerce compatible** -- works with both Magento Open Source and Adobe Commerce (B2B, Staging, and all Commerce-specific modules)
65
65
  - **AST-powered** -- tree-sitter parsing for PHP and JavaScript extracts classes, methods, namespaces, and inheritance
66
66
  - **Cross-tool discovery** -- tool descriptions include keywords and "See also" references so AI clients find the right tool on the first try
67
+ - **SONA feedback learning** -- self-adjusting search that learns from MCP tool call patterns (e.g., search → find_plugin refines future rankings for similar queries)
68
+ - **SONA v2 with MicroLoRA + EWC++** -- rank-2 low-rank adapter (1536 params, ~6KB) adjusts query embeddings based on learned patterns; Elastic Weight Consolidation prevents catastrophic forgetting during online learning
67
69
  - **Diff analysis** -- risk scoring and change classification for git commits and staged changes
68
70
  - **Complexity analysis** -- cyclomatic complexity, function count, and hotspot detection across modules
69
71
  - **Fast** -- 10-45ms queries via persistent serve process, batched ONNX embedding with adaptive thread scaling
@@ -117,7 +119,8 @@ flowchart TD
117
119
  E1 --> E2[ONNX Embedding]
118
120
  E2 --> H[HNSW Search]
119
121
  H --> R[Hybrid Reranking]
120
- R --> J[Structured JSON]
122
+ R --> SA[SONA Adjustment + MicroLoRA]
123
+ SA --> J[Structured JSON]
121
124
  ```
122
125
 
123
126
  ### Components
@@ -130,6 +133,7 @@ flowchart TD
130
133
  | JS parsing | `tree-sitter-javascript` | AMD/ES6 module detection |
131
134
  | Pattern detection | Custom Rust | 20+ Magento-specific patterns |
132
135
  | CLI | `clap` | Command-line interface (index, search, serve, validate) |
136
+ | SONA | Custom Rust | Feedback learning with MicroLoRA + EWC++ |
133
137
  | MCP server | `@modelcontextprotocol/sdk` | AI tool integration with structured JSON output |
134
138
 
135
139
  ---
@@ -252,6 +256,17 @@ When `--magento-root` is provided, a background file watcher polls for changed f
252
256
  {"command":"watcher_status"}
253
257
  // Response:
254
258
  {"ok":true,"data":{"running":true,"tracked_files":18234,"last_scan_changes":3,"interval_secs":60}}
259
+
260
+ // SONA feedback:
261
+ {"command":"feedback","signals":[{"type":"refinement_to_plugin","query":"checkout totals","timestamp":1700000000000}]}
262
+ // Response:
263
+ {"ok":true,"data":{"learned":1}}
264
+
265
+ // SONA status:
266
+ {"command":"sona_status"}
267
+ // Response:
268
+ {"ok":true,"data":{"learned_patterns":5,"total_observations":12}}
269
+
255
270
  ```
256
271
 
257
272
  ### Node.js CLI
@@ -475,29 +490,9 @@ pie title Test Pass Rate (101 queries)
475
490
  | **Index size** | 35,795 vectors |
476
491
  | **Query time** | 10-45ms |
477
492
 
478
- #### Per-Tool Performance
479
-
480
- | Tool | Pass | Precision | MRR | NDCG |
481
- |------|------|-----------|-----|------|
482
- | find_class | 100% | 100% | 100% | 100% |
483
- | find_method | 100% | 98% | 92% | 97% |
484
- | find_controller | 100% | 100% | 100% | 100% |
485
- | find_observer | 100% | 100% | 100% | 100% |
486
- | find_plugin | 100% | 100% | 100% | 100% |
487
- | find_preference | 100% | 100% | 100% | 100% |
488
- | find_api | 100% | 100% | 100% | 100% |
489
- | find_cron | 100% | 100% | 100% | 100% |
490
- | find_db_schema | 100% | 100% | 100% | 100% |
491
- | find_graphql | 100% | 100% | 100% | 100% |
492
- | find_block | 100% | 100% | 100% | 100% |
493
- | find_config | 100% | 100% | 100% | 100% |
494
- | find_template | 100% | 100% | 100% | 100% |
495
- | search | 100% | 100% | 100% | 100% |
496
- | module_structure | 100% | 100% | 100% | 100% |
497
-
498
493
  ### Integration Tests
499
494
 
500
- 62 integration tests covering MCP protocol compliance, tool schemas, tool calls, analysis tools, and stdout JSON integrity.
495
+ 64 integration tests covering MCP protocol compliance, tool schemas, tool calls, analysis tools, and stdout JSON integrity.
501
496
 
502
497
  ### Running Tests
503
498
 
@@ -506,9 +501,13 @@ pie title Test Pass Rate (101 queries)
506
501
  npm run test:accuracy
507
502
  npm run test:accuracy:verbose
508
503
 
509
- # Integration tests (62 tests)
504
+ # Integration tests (64 tests)
510
505
  npm test
511
506
 
507
+ # SONA/MicroLoRA benefit evaluation (180 queries, baseline vs post-training)
508
+ npm run test:sona-eval
509
+ npm run test:sona-eval:verbose
510
+
512
511
  # Rust validation (557 test cases)
513
512
  cd rust-core && cargo run --release -- validate -m ./magento2 --skip-index
514
513
  ```
@@ -536,10 +535,13 @@ magector/
536
535
  │ ├── test-data-generator.js
537
536
  │ └── accuracy-calculator.js
538
537
  ├── tests/ # Automated tests
539
- │ ├── mcp-server.test.js # Integration tests (62 tests)
538
+ │ ├── mcp-server.test.js # Integration tests (64 tests)
540
539
  │ ├── mcp-accuracy.test.js # E2E accuracy tests (101 queries)
540
+ │ ├── mcp-sona.test.js # SONA feedback integration tests (8 tests)
541
+ │ ├── mcp-sona-eval.test.js # SONA/MicroLoRA benefit evaluation (180 queries)
541
542
  │ └── results/ # Test result artifacts
542
- └── accuracy-report.json
543
+ ├── accuracy-report.json
544
+ │ └── sona-eval-report.json
543
545
  ├── platforms/ # Platform-specific binary packages
544
546
  │ ├── darwin-arm64/ # macOS ARM (Apple Silicon)
545
547
  │ ├── linux-x64/ # Linux x64
@@ -556,6 +558,7 @@ magector/
556
558
  │ │ ├── watcher.rs # File watcher for incremental re-indexing
557
559
  │ │ ├── ast.rs # Tree-sitter AST (PHP + JS)
558
560
  │ │ ├── magento.rs # Magento pattern detection (Rust)
561
+ │ │ ├── sona.rs # SONA feedback learning + MicroLoRA + EWC++
559
562
  │ │ └── validation.rs # 557 test cases, validation framework
560
563
  │ └── models/ # ONNX model files (auto-downloaded)
561
564
  │ ├── all-MiniLM-L6-v2.onnx
@@ -593,7 +596,8 @@ Magector scans every `.php`, `.js`, `.xml`, `.phtml`, and `.graphqls` file in a
593
596
  2. The enriched query is embedded into the same 384-dimensional vector space
594
597
  3. HNSW finds the nearest neighbors by cosine similarity
595
598
  4. **Hybrid reranking** boosts results with keyword matches in path and search text
596
- 5. Results are returned as structured JSON with file path, class name, methods, role badges, and content snippet
599
+ 5. **SONA adjustment** -- MicroLoRA adapts the query embedding based on learned patterns; EWC++ prevents forgetting earlier learning
600
+ 6. Results are returned as structured JSON with file path, class name, methods, role badges, and content snippet
597
601
 
598
602
  ### 3. Persistent Serve Mode
599
603
 
@@ -668,6 +672,48 @@ sequenceDiagram
668
672
  AI-->>Dev: TotalsCollector.php
669
673
  ```
670
674
 
675
+ ### 6. SONA Feedback Learning
676
+
677
+ The MCP server tracks sequences of tool calls and sends feedback signals to the Rust process. Over time, this adjusts search result rankings based on observed usage patterns.
678
+
679
+ **How it works:** The Node.js `SessionTracker` watches for follow-up tool calls after `magento_search`. If a user searches and then immediately calls `magento_find_plugin`, SONA learns that similar queries should boost plugin results. The learned weights are persisted to a `.sona` file alongside the index.
680
+
681
+ | MCP Call Sequence | Signal | Effect on Future Searches |
682
+ |---|---|---|
683
+ | `magento_search` → `magento_find_plugin` (within 30s) | `refinement_to_plugin` | Boosts plugin results |
684
+ | `magento_search` → `magento_find_class` (within 30s) | `refinement_to_class` | Boosts class matches |
685
+ | `magento_search` → `magento_find_config` (within 30s) | `refinement_to_config` | Boosts config/XML results |
686
+ | `magento_search` → `magento_find_observer` (within 30s) | `refinement_to_observer` | Boosts observer results |
687
+ | `magento_search` → `magento_find_controller` (within 30s) | `refinement_to_controller` | Boosts controller results |
688
+ | `magento_search` → `magento_find_block` (within 30s) | `refinement_to_block` | Boosts block results |
689
+ | `magento_search` → `magento_trace_flow` (within 30s) | `trace_after_search` | Boosts controller results |
690
+ | `magento_search(Q1)` → `magento_search(Q2)` (within 60s) | `query_refinement` | Tracked for analysis |
691
+
692
+ **Characteristics:**
693
+ - Score adjustments are capped at ±0.15 to avoid overwhelming semantic similarity
694
+ - Learning rate decays with repeated observations (diminishing returns)
695
+ - Learned weights are keyed by normalized, order-independent query term hashes
696
+ - Always active -- no feature flags or build-time opt-in required
697
+ - Persisted via bincode to `<db_path>.sona`
698
+
699
+ **SONA v2: MicroLoRA + EWC++**
700
+
701
+ SONA v2 adds embedding-level adaptation via a MicroLoRA adapter and Elastic Weight Consolidation:
702
+
703
+ | Component | Parameters | Purpose |
704
+ |-----------|-----------|---------|
705
+ | MicroLoRA | 1536 (rank-2, 2×384×2) | Adjusts query embeddings before HNSW search |
706
+ | EWC++ | Fisher matrix (384 values) | Prevents catastrophic forgetting during online learning |
707
+
708
+ - `adjust_query_embedding()` applies the LoRA transform + L2 normalization before vector search; cosine similarity guard (≥0.90) skips destructive adjustments
709
+ - `learn_with_embeddings()` updates LoRA weights from feedback signals with EWC regularization (λ=2000) and decaying learning rate
710
+ - 3-tier scoring with negative learning: positive signals boost the followed feature type, mild negative learning (0.1×) demotes unrelated types
711
+ - V1→V2 persistence format is backward-compatible (auto-upgrades on load)
712
+
713
+ ```bash
714
+ cd rust-core && cargo build --release
715
+ ```
716
+
671
717
  ---
672
718
 
673
719
  ## Magento Patterns Detected
@@ -784,7 +830,7 @@ cargo run --release -- validate
784
830
  ### Testing
785
831
 
786
832
  ```bash
787
- # Integration tests (62 tests, requires indexed codebase)
833
+ # Integration tests (64 tests, requires indexed codebase)
788
834
  npm test
789
835
 
790
836
  # E2E accuracy tests (101 queries)
@@ -794,9 +840,15 @@ npm run test:accuracy:verbose
794
840
  # Run without index (unit + schema tests only)
795
841
  npm run test:no-index
796
842
 
797
- # Rust unit tests
843
+ # Rust unit tests (33 tests including SONA)
798
844
  cd rust-core && cargo test
799
845
 
846
+ # SONA integration tests (8 tests)
847
+ node tests/mcp-sona.test.js
848
+
849
+ # SONA/MicroLoRA benefit evaluation (180 queries)
850
+ npm run test:sona-eval
851
+
800
852
  # Rust validation (557 test cases)
801
853
  cd rust-core && cargo run --release -- validate -m ./magento2 --skip-index
802
854
  ```
@@ -842,7 +894,7 @@ npm run test:accuracy
842
894
  - **Library:** `hnsw_rs`
843
895
  - **Parameters:** M=32, max_layers=16, ef_construction=200
844
896
  - **Distance metric:** Cosine similarity
845
- - **Hybrid search:** Semantic nearest-neighbor + keyword reranking in path and search text
897
+ - **Hybrid search:** Semantic nearest-neighbor + keyword reranking in path and search text + SONA/MicroLoRA feedback adjustments
846
898
  - **Incremental updates:** Tombstone soft-delete + periodic HNSW rebuild (compact)
847
899
  - **Persistence:** Bincode V2 binary serialization (backward-compatible with V1)
848
900
 
@@ -894,6 +946,7 @@ struct IndexMetadata {
894
946
  - **Adaptive HNSW capacity** -- pre-sized to actual vector count
895
947
  - **Parallel HNSW insert** -- batch insert uses hnsw_rs parallel insertion on load and index
896
948
  - **Tuned ef_search** -- optimized search parameters for 36K vector index (ef_search=50 for search, 64 for hybrid)
949
+ - **SONA feedback learning** -- learns from MCP tool call patterns to adjust search rankings; MicroLoRA adapts query embeddings, EWC++ prevents forgetting
897
950
 
898
951
  ---
899
952
 
@@ -912,13 +965,15 @@ gantt
912
965
  E2E tests :done, 2025-03, 15d
913
966
  Adobe Commerce :done, 2025-03, 15d
914
967
  section Next
915
- Method chunking :active, 2025-04, 30d
916
- Intent detection :2025-05, 30d
917
- Type filtering :2025-06, 30d
968
+ SONA feedback :done, 2025-04, 30d
918
969
  Incremental index :done, 2025-04, 30d
970
+ SONA v2 MicroLoRA :done, 2025-05, 15d
971
+ Method chunking :active, 2025-06, 30d
972
+ Intent detection :2025-07, 30d
973
+ Type filtering :2025-08, 30d
919
974
  section Future
920
- VSCode extension :2025-08, 60d
921
- Web UI :2025-10, 60d
975
+ VSCode extension :2025-09, 60d
976
+ Web UI :2025-11, 60d
922
977
  ```
923
978
 
924
979
  - [x] Hybrid search (semantic + keyword re-ranking)
@@ -927,6 +982,8 @@ gantt
927
982
  - [x] Cross-tool discovery hints for AI clients
928
983
  - [x] E2E accuracy test suite (101 queries)
929
984
  - [x] Adobe Commerce support (B2B, Staging, and all Commerce-specific modules)
985
+ - [x] SONA feedback learning (search rankings adapt to MCP tool call patterns)
986
+ - [x] SONA v2 with MicroLoRA + EWC++ (embedding-level adaptation, prevents catastrophic forgetting)
930
987
  - [ ] Method-level chunking (per-method vectors for direct method search)
931
988
  - [ ] Query intent classification (auto-detect "give me XML" vs "give me PHP")
932
989
  - [ ] Filtered search by file type at the vector level
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "Semantic code search for Magento 2 — index, search, MCP server",
5
5
  "type": "module",
6
6
  "main": "src/mcp-server.js",
@@ -23,7 +23,9 @@
23
23
  "test": "node tests/mcp-server.test.js",
24
24
  "test:no-index": "node tests/mcp-server.test.js --no-index",
25
25
  "test:accuracy": "node tests/mcp-accuracy.test.js",
26
- "test:accuracy:verbose": "node tests/mcp-accuracy.test.js --verbose"
26
+ "test:accuracy:verbose": "node tests/mcp-accuracy.test.js --verbose",
27
+ "test:sona-eval": "node tests/mcp-sona-eval.test.js",
28
+ "test:sona-eval:verbose": "node tests/mcp-sona-eval.test.js --verbose"
27
29
  },
28
30
  "dependencies": {
29
31
  "@modelcontextprotocol/sdk": "^1.0.0",
@@ -33,10 +35,10 @@
33
35
  "ruvector": "^0.1.96"
34
36
  },
35
37
  "optionalDependencies": {
36
- "@magector/cli-darwin-arm64": "1.4.1",
37
- "@magector/cli-linux-x64": "1.4.1",
38
- "@magector/cli-linux-arm64": "1.4.1",
39
- "@magector/cli-win32-x64": "1.4.1"
38
+ "@magector/cli-darwin-arm64": "1.4.3",
39
+ "@magector/cli-linux-x64": "1.4.3",
40
+ "@magector/cli-linux-arm64": "1.4.3",
41
+ "@magector/cli-win32-x64": "1.4.3"
40
42
  },
41
43
  "keywords": [
42
44
  "magento",
package/src/init.js CHANGED
@@ -159,19 +159,24 @@ function writeRules(projectPath, ides) {
159
159
  }
160
160
 
161
161
  /**
162
- * Add magector.db to .gitignore if not already present.
162
+ * Add magector.db and magector.log to .gitignore if not already present.
163
163
  */
164
164
  function updateGitignore(projectPath) {
165
165
  const giPath = path.join(projectPath, '.gitignore');
166
+ let updated = false;
166
167
  if (existsSync(giPath)) {
167
168
  const content = readFileSync(giPath, 'utf-8');
168
169
  if (!content.includes('magector.db')) {
169
170
  appendFileSync(giPath, '\n# Magector index\nmagector.db\n');
170
- return true;
171
+ updated = true;
171
172
  }
172
- return false;
173
+ if (!content.includes('magector.log')) {
174
+ appendFileSync(giPath, 'magector.log\n');
175
+ updated = true;
176
+ }
177
+ return updated;
173
178
  }
174
- writeFileSync(giPath, '# Magector index\nmagector.db\n');
179
+ writeFileSync(giPath, '# Magector index\nmagector.db\nmagector.log\n');
175
180
  return true;
176
181
  }
177
182
 
@@ -259,7 +264,7 @@ export async function init(projectPath) {
259
264
  // 8. Update .gitignore
260
265
  const giUpdated = updateGitignore(projectPath);
261
266
  if (giUpdated) {
262
- console.log('\nUpdated .gitignore with magector.db');
267
+ console.log('\nUpdated .gitignore with magector.db and magector.log');
263
268
  }
264
269
 
265
270
  // 9. Get stats and print summary
package/src/mcp-server.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  } from '@modelcontextprotocol/sdk/types.js';
17
17
  import { execFileSync, spawn } from 'child_process';
18
18
  import { createInterface } from 'readline';
19
- import { existsSync } from 'fs';
19
+ import { existsSync, statSync, unlinkSync, appendFileSync, writeFileSync } from 'fs';
20
20
  import { stat } from 'fs/promises';
21
21
  import { glob } from 'glob';
22
22
  import path from 'path';
@@ -35,10 +35,28 @@ import { resolveModels } from './model.js';
35
35
  const config = {
36
36
  dbPath: process.env.MAGECTOR_DB || './magector.db',
37
37
  magentoRoot: process.env.MAGENTO_ROOT || process.cwd(),
38
+ watchInterval: parseInt(process.env.MAGECTOR_WATCH_INTERVAL, 10) || 300,
38
39
  get rustBinary() { return resolveBinary(); },
39
40
  get modelCache() { return resolveModels() || process.env.MAGECTOR_MODELS || './models'; }
40
41
  };
41
42
 
43
+ // ─── Logging ─────────────────────────────────────────────────────
44
+ // All activity is logged to magector.log in the project root (MAGENTO_ROOT).
45
+
46
+ const LOG_PATH = path.join(config.magentoRoot, 'magector.log');
47
+
48
+ function logToFile(level, message) {
49
+ const ts = new Date().toISOString();
50
+ try {
51
+ appendFileSync(LOG_PATH, `[${ts}] [${level}] ${message}\n`);
52
+ } catch {
53
+ // Logging should never crash the server
54
+ }
55
+ }
56
+
57
+ // Initialize log file on startup
58
+ try { writeFileSync(LOG_PATH, `[${new Date().toISOString()}] [INFO] Magector MCP server starting\n`); } catch {}
59
+
42
60
  // ─── Rust Core Integration ──────────────────────────────────────
43
61
 
44
62
  // Env vars to suppress ONNX Runtime native logs that would pollute stdout/JSON-RPC
@@ -48,6 +66,128 @@ const rustEnv = {
48
66
  RUST_LOG: 'error',
49
67
  };
50
68
 
69
+ /**
70
+ * Extract JSON from stdout that may contain tracing/log lines.
71
+ * The npm-distributed binary can emit ANSI-colored tracing lines to stdout
72
+ * even with RUST_LOG=error. This strips non-JSON lines before parsing.
73
+ */
74
+ function extractJson(stdout) {
75
+ const lines = stdout.split('\n');
76
+ // Try each line from the end (JSON output is typically last)
77
+ for (let i = lines.length - 1; i >= 0; i--) {
78
+ const line = lines[i].trim();
79
+ if (!line) continue;
80
+ try {
81
+ return JSON.parse(line);
82
+ } catch {
83
+ // not JSON, skip
84
+ }
85
+ }
86
+ // Fallback: try parsing the entire output (handles multi-line JSON)
87
+ // Strip lines that look like tracing (start with ANSI escape or timestamp bracket)
88
+ const cleaned = lines
89
+ .filter(l => !l.match(/^\s*(\x1b\[|\[[\d\-T:.Z]+)/) && l.trim())
90
+ .join('\n')
91
+ .trim();
92
+ if (cleaned) {
93
+ return JSON.parse(cleaned);
94
+ }
95
+ throw new SyntaxError('No valid JSON found in command output');
96
+ }
97
+
98
+ // ─── Database Format Check & Background Re-index ────────────────
99
+
100
+ let reindexInProgress = false;
101
+ let reindexProcess = null;
102
+
103
+ /**
104
+ * Check if the database file is compatible with the current binary.
105
+ * Returns true if OK, false if format mismatch (file has data but binary reads 0 vectors).
106
+ */
107
+ function checkDbFormat() {
108
+ if (!existsSync(config.dbPath)) return true;
109
+
110
+ try {
111
+ // Check if file is non-trivial (has actual index data)
112
+ const fstat = statSync(config.dbPath);
113
+ if (fstat.size < 100) return true; // Tiny file = likely empty/new
114
+
115
+ const result = execFileSync(config.rustBinary, [
116
+ 'stats', '-d', config.dbPath
117
+ ], { encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
118
+
119
+ const vectors = parseInt(result.match(/Total vectors:\s*(\d+)/)?.[1] || '0');
120
+ // File has real data but binary sees 0 vectors → format incompatible
121
+ return vectors > 0;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Start a background re-index process. Logs to magector.log in project root.
129
+ * MCP tools return an informative error while this is running.
130
+ */
131
+ function startBackgroundReindex() {
132
+ if (reindexInProgress) return;
133
+ if (!config.magentoRoot || !existsSync(config.magentoRoot)) {
134
+ const msg = 'Cannot auto-reindex: MAGENTO_ROOT not set or not found';
135
+ console.error(msg);
136
+ logToFile('WARN', msg);
137
+ return;
138
+ }
139
+
140
+ reindexInProgress = true;
141
+
142
+ // Remove incompatible DB before re-indexing
143
+ try { if (existsSync(config.dbPath)) unlinkSync(config.dbPath); } catch {}
144
+
145
+ logToFile('WARN', `Database format incompatible. Starting background re-index.`);
146
+ console.error(`Database format incompatible. Starting background re-index (log: ${LOG_PATH})`);
147
+
148
+ reindexProcess = spawn(config.rustBinary, [
149
+ 'index',
150
+ '-m', config.magentoRoot,
151
+ '-d', config.dbPath,
152
+ '-c', config.modelCache
153
+ ], {
154
+ stdio: ['pipe', 'pipe', 'pipe'],
155
+ env: rustEnv,
156
+ });
157
+
158
+ // Pipe reindex stdout/stderr to log file (strip ANSI codes)
159
+ reindexProcess.stdout.on('data', (d) => {
160
+ const text = d.toString().replace(/\x1b\[[0-9;]*m/g, '').trim();
161
+ if (text) logToFile('INDEX', text);
162
+ });
163
+ reindexProcess.stderr.on('data', (d) => {
164
+ const text = d.toString().replace(/\x1b\[[0-9;]*m/g, '').trim();
165
+ if (text) logToFile('INDEX', text);
166
+ });
167
+
168
+ reindexProcess.on('exit', (code) => {
169
+ reindexInProgress = false;
170
+ reindexProcess = null;
171
+ if (code === 0) {
172
+ logToFile('INFO', 'Background re-index completed. Restarting serve process.');
173
+ console.error('Background re-index completed. Restarting serve process.');
174
+ if (serveProcess) serveProcess.kill();
175
+ searchCache.clear();
176
+ startServeProcess();
177
+ } else {
178
+ logToFile('ERR', `Background re-index failed (exit code ${code})`);
179
+ console.error(`Background re-index failed (exit code ${code}). Check ${LOG_PATH}`);
180
+ }
181
+ });
182
+
183
+ reindexProcess.on('error', (err) => {
184
+ reindexInProgress = false;
185
+ reindexProcess = null;
186
+ logToFile('ERR', `Background re-index error: ${err.message}`);
187
+ console.error(`Background re-index error: ${err.message}`);
188
+ });
189
+ }
190
+
51
191
  /**
52
192
  * Query cache: avoid re-embedding identical queries.
53
193
  * Keyed by "query|limit", capped at 200 entries (LRU eviction).
@@ -63,6 +203,70 @@ function cacheSet(key, value) {
63
203
  searchCache.set(key, value);
64
204
  }
65
205
 
206
+ // ─── SONA: MCP Feedback Signal Tracker ────────────────────────
207
+ class SessionTracker {
208
+ constructor() {
209
+ this.lastSearch = null; // {query, resultPaths, timestamp}
210
+ this.feedbackQueue = [];
211
+ }
212
+
213
+ recordToolCall(toolName, args, results) {
214
+ const now = Date.now();
215
+
216
+ if (toolName === 'magento_search') {
217
+ // If previous search was < 60s ago and query differs → query_refinement
218
+ if (this.lastSearch && (now - this.lastSearch.timestamp) < 60000
219
+ && args.query !== this.lastSearch.query) {
220
+ this.feedbackQueue.push({
221
+ type: 'query_refinement',
222
+ originalQuery: this.lastSearch.query,
223
+ refinedQuery: args.query,
224
+ originalResultPaths: this.lastSearch.resultPaths,
225
+ timestamp: now
226
+ });
227
+ }
228
+ this.lastSearch = {
229
+ query: args.query,
230
+ resultPaths: (results || []).map(r => r.path || r.metadata?.path).filter(Boolean),
231
+ timestamp: now
232
+ };
233
+ return;
234
+ }
235
+
236
+ // Follow-up tool after search (within 30s)
237
+ if (this.lastSearch && (now - this.lastSearch.timestamp) < 30000) {
238
+ const refinementMap = {
239
+ 'magento_find_plugin': 'refinement_to_plugin',
240
+ 'magento_find_class': 'refinement_to_class',
241
+ 'magento_find_config': 'refinement_to_config',
242
+ 'magento_find_observer': 'refinement_to_observer',
243
+ 'magento_find_controller': 'refinement_to_controller',
244
+ 'magento_find_block': 'refinement_to_block',
245
+ 'magento_trace_flow': 'trace_after_search',
246
+ };
247
+ const signalType = refinementMap[toolName];
248
+ if (signalType) {
249
+ this.feedbackQueue.push({
250
+ type: signalType,
251
+ query: this.lastSearch.query,
252
+ searchResultPaths: this.lastSearch.resultPaths,
253
+ followedTool: toolName,
254
+ followedArgs: args,
255
+ timestamp: now
256
+ });
257
+ }
258
+ }
259
+ }
260
+
261
+ flush() {
262
+ const signals = this.feedbackQueue;
263
+ this.feedbackQueue = [];
264
+ return signals;
265
+ }
266
+ }
267
+
268
+ const sessionTracker = new SessionTracker();
269
+
66
270
  // ─── Persistent Rust Serve Process ──────────────────────────────
67
271
  // Keeps ONNX model + HNSW index loaded; eliminates ~2.6s cold start per query.
68
272
  // Falls back to execFileSync if serve mode unavailable.
@@ -85,14 +289,19 @@ function startServeProcess() {
85
289
  ];
86
290
  // Enable file watcher if magento root is configured
87
291
  if (config.magentoRoot && existsSync(config.magentoRoot)) {
88
- args.push('-m', config.magentoRoot);
292
+ args.push('-m', config.magentoRoot, '--watch-interval', String(config.watchInterval));
89
293
  }
90
294
  const proc = spawn(config.rustBinary, args,
91
295
  { stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
92
296
 
93
297
  proc.on('error', () => { serveProcess = null; serveReady = false; if (serveReadyResolve) { serveReadyResolve(false); serveReadyResolve = null; } });
94
298
  proc.on('exit', () => { serveProcess = null; serveReady = false; if (serveReadyResolve) { serveReadyResolve(false); serveReadyResolve = null; } });
95
- proc.stderr.on('data', () => {}); // drain stderr
299
+ proc.stderr.on('data', (d) => {
300
+ // Log serve process stderr (watcher events, tracing, errors) to magector.log
301
+ // Strip ANSI escape codes for clean log output
302
+ const text = d.toString().replace(/\x1b\[[0-9;]*m/g, '').trim();
303
+ if (text) logToFile('SERVE', text);
304
+ });
96
305
 
97
306
  serveReadline = createInterface({ input: proc.stdout });
98
307
  serveReadline.on('line', (line) => {
@@ -177,7 +386,7 @@ function rustSearchSync(query, limit = 10) {
177
386
  '-l', String(limit),
178
387
  '-f', 'json'
179
388
  ], { encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'], env: rustEnv });
180
- const parsed = JSON.parse(result);
389
+ const parsed = extractJson(result);
181
390
  cacheSet(cacheKey, parsed);
182
391
  return parsed;
183
392
  }
@@ -656,7 +865,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
656
865
  type: 'number',
657
866
  description: 'Maximum results to return (default: 10, max: 100)',
658
867
  default: 10
659
- }
868
+ },
660
869
  },
661
870
  required: ['query']
662
871
  }
@@ -979,18 +1188,40 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
979
1188
  },
980
1189
  required: ['entryPoint']
981
1190
  }
982
- }
1191
+ },
983
1192
  ]
984
1193
  }));
985
1194
 
986
1195
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
987
1196
  const { name, arguments: args } = request.params;
1197
+ const reqStart = Date.now();
1198
+ logToFile('REQ', `${name}(${JSON.stringify(args || {})})`);
1199
+
1200
+ // Block search tools while re-indexing is in progress
1201
+ if (reindexInProgress && name !== 'magento_stats' && name !== 'magento_analyze_diff' && name !== 'magento_complexity') {
1202
+ logToFile('REQ', `${name} → blocked (re-indexing in progress)`);
1203
+ return {
1204
+ content: [{
1205
+ type: 'text',
1206
+ text: 'Re-indexing in progress. The database format was incompatible and is being rebuilt automatically. Check magector.log for progress. Search tools will be available once re-indexing completes.'
1207
+ }],
1208
+ isError: true,
1209
+ };
1210
+ }
1211
+
1212
+ // SONA: record non-search tool calls before processing (for follow-up detection)
1213
+ if (name !== 'magento_search') {
1214
+ sessionTracker.recordToolCall(name, args || {});
1215
+ }
988
1216
 
989
1217
  try {
990
1218
  switch (name) {
991
1219
  case 'magento_search': {
992
1220
  const raw = await rustSearchAsync(args.query, args.limit || 10);
993
- const results = raw.map(normalizeResult);
1221
+ const arr = Array.isArray(raw) ? raw : [];
1222
+ const results = arr.map(normalizeResult);
1223
+ // SONA: record search with results for follow-up tracking
1224
+ sessionTracker.recordToolCall(name, args || {}, arr);
994
1225
  return {
995
1226
  content: [{
996
1227
  type: 'text',
@@ -1455,6 +1686,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1455
1686
  };
1456
1687
  }
1457
1688
 
1689
+
1690
+
1458
1691
  default:
1459
1692
  return {
1460
1693
  content: [{
@@ -1465,6 +1698,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1465
1698
  };
1466
1699
  }
1467
1700
  } catch (error) {
1701
+ const elapsed = Date.now() - reqStart;
1702
+ logToFile('ERR', `${name} → error: ${error.message} (${elapsed}ms)`);
1468
1703
  return {
1469
1704
  content: [{
1470
1705
  type: 'text',
@@ -1472,6 +1707,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1472
1707
  }],
1473
1708
  isError: true
1474
1709
  };
1710
+ } finally {
1711
+ const elapsed = Date.now() - reqStart;
1712
+ logToFile('RES', `${name} completed (${elapsed}ms)`);
1713
+ // SONA: flush accumulated feedback signals to Rust core
1714
+ const signals = sessionTracker.flush();
1715
+ if (signals.length > 0 && serveProcess && serveReady) {
1716
+ serveQuery('feedback', { signals }).catch(() => {});
1717
+ }
1475
1718
  }
1476
1719
  });
1477
1720
 
@@ -1504,27 +1747,37 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1504
1747
  });
1505
1748
 
1506
1749
  async function main() {
1750
+ // Check database format compatibility before starting serve process
1751
+ if (existsSync(config.dbPath) && !checkDbFormat()) {
1752
+ startBackgroundReindex();
1753
+ }
1754
+
1507
1755
  // Try to start persistent Rust serve process for fast queries
1508
- try {
1509
- startServeProcess();
1510
- // Wait for the serve process to load ONNX model + HNSW index (up to 15s)
1511
- if (serveReadyPromise) {
1512
- const ready = await Promise.race([
1513
- serveReadyPromise,
1514
- new Promise(r => setTimeout(() => r(false), 15000))
1515
- ]);
1516
- if (ready) {
1517
- console.error('Serve process ready (persistent mode)');
1518
- } else {
1519
- console.error('Serve process not ready in time, will use fallback');
1756
+ if (!reindexInProgress) {
1757
+ try {
1758
+ startServeProcess();
1759
+ // Wait for the serve process to load ONNX model + HNSW index (up to 15s)
1760
+ if (serveReadyPromise) {
1761
+ const ready = await Promise.race([
1762
+ serveReadyPromise,
1763
+ new Promise(r => setTimeout(() => r(false), 15000))
1764
+ ]);
1765
+ if (ready) {
1766
+ logToFile('INFO', 'Serve process ready (persistent mode)');
1767
+ console.error('Serve process ready (persistent mode)');
1768
+ } else {
1769
+ logToFile('WARN', 'Serve process not ready in time, will use fallback');
1770
+ console.error('Serve process not ready in time, will use fallback');
1771
+ }
1520
1772
  }
1773
+ } catch {
1774
+ // Non-fatal: falls back to execFileSync per query
1521
1775
  }
1522
- } catch {
1523
- // Non-fatal: falls back to execFileSync per query
1524
1776
  }
1525
1777
 
1526
1778
  const transport = new StdioServerTransport();
1527
1779
  await server.connect(transport);
1780
+ logToFile('INFO', 'Magector MCP server started (Rust core backend)');
1528
1781
  console.error('Magector MCP server started (Rust core backend)');
1529
1782
  }
1530
1783