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 +91 -34
- package/package.json +8 -6
- package/src/init.js +10 -5
- package/src/mcp-server.js +274 -21
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 -->
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
│
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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-
|
|
921
|
-
Web UI :2025-
|
|
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.
|
|
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.
|
|
37
|
-
"@magector/cli-linux-x64": "1.4.
|
|
38
|
-
"@magector/cli-linux-arm64": "1.4.
|
|
39
|
-
"@magector/cli-win32-x64": "1.4.
|
|
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
|
-
|
|
171
|
+
updated = true;
|
|
171
172
|
}
|
|
172
|
-
|
|
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', () => {
|
|
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 =
|
|
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
|
|
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
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
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
|
|