filemayor 2.1.0 → 3.6.0

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
@@ -4,7 +4,7 @@
4
4
  # FileMayor
5
5
  ### The intelligent, local-first engine for filesystem order.
6
6
 
7
- `v2.1.0` · Windows · macOS · Linux · Node ≥18 · **Zero Dependencies**
7
+ `v3.0.0` · Windows · macOS · Linux · Node ≥18 · **Zero Dependencies**
8
8
 
9
9
  [Website](https://filemayor.com) · [npm](https://www.npmjs.com/package/filemayor) · [Safety Report](#security)
10
10
 
@@ -0,0 +1,42 @@
1
+ /**
2
+ * ═══════════════════════════════════════════════════════════════════
3
+ * FILEMAYOR AGENTIC CREW — CURATIVE PLANNER
4
+ * The Generative Brain (Gemini). Orchestrates the move plan.
5
+ * ═══════════════════════════════════════════════════════════════════
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const IntentInterpreter = require('../intent-interpreter');
11
+
12
+ class CurativePlanner {
13
+ constructor(apiKey) {
14
+ this.interpreter = new IntentInterpreter(apiKey);
15
+ }
16
+
17
+ /**
18
+ * Create a Curative Plan using AI logic
19
+ * @param {Object} intent - From Strategist
20
+ * @param {Object} sentryData - From MetadataSentry
21
+ * @param {string} rawPrompt - Original user input
22
+ * @returns {Promise<Object>} Curative Plan
23
+ */
24
+ async plan(intent, sentryData, rawPrompt) {
25
+ // v3.2 Intuition: Pass enriched clusters with relational metadata
26
+ const context = {
27
+ intent: intent.intent,
28
+ strategy: intent.strategy,
29
+ isRefine: intent.isRefine,
30
+ cwd: process.cwd()
31
+ };
32
+
33
+ // Standardized Bridge: Telemetry is passed as clusters
34
+ return await this.interpreter.interpret(
35
+ rawPrompt,
36
+ sentryData.clusters,
37
+ context
38
+ );
39
+ }
40
+ }
41
+
42
+ module.exports = CurativePlanner;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * ═══════════════════════════════════════════════════════════════════
3
+ * FILEMAYOR AGENTIC CREW — METADATA SENTRY
4
+ * Responsible for scanning, clustering, and summarizing filesystem state.
5
+ * Reduces token cost by 90% via representative sampling.
6
+ * ═══════════════════════════════════════════════════════════════════
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+ const crypto = require('crypto');
13
+
14
+ class MetadataSentry {
15
+ /**
16
+ * Clustered Analysis: Group files by category and provided samples
17
+ * Now includes v3.2 Intuition metadata (Ancestry & Bundles)
18
+ * @param {Object[]} files - Raw FileInfo objects from scanner
19
+ * @returns {Object} Summarized perspective for the planner
20
+ */
21
+ analyze(files) {
22
+ const clusters = {};
23
+ const bundles = new Map(); // UUID map for Atomic Bundles
24
+ let totalBytes = 0;
25
+
26
+ // Pass 1: Dependency Mapping (Detect Anchors)
27
+ const ANCHORS = ['.git', '.als', '.flp', '.xcodeproj', 'package.json', 'index.js', 'project.json'];
28
+
29
+ for (const f of files) {
30
+ if (!f.path) continue; // Skip files without a path
31
+ const fileName = path.basename(f.path).toLowerCase();
32
+ if (ANCHORS.includes(fileName) || ANCHORS.some(a => fileName.endsWith(a))) {
33
+ const bundlePath = path.dirname(f.path);
34
+ const bundleId = crypto.randomUUID();
35
+ bundles.set(bundlePath, bundleId);
36
+ }
37
+ }
38
+
39
+ // Pass 2: Analysis & Contextual Tagging
40
+ for (const f of files) {
41
+ const cat = f.category || 'other';
42
+ if (!clusters[cat]) {
43
+ clusters[cat] = {
44
+ count: 0,
45
+ totalSize: 0,
46
+ samples: []
47
+ };
48
+ }
49
+
50
+ clusters[cat].count++;
51
+ clusters[cat].totalSize += f.size || 0;
52
+ totalBytes += f.size || 0;
53
+
54
+ // v3.2 Intuition: Build Ancestry (Depth 3)
55
+ const filePath = f.path || f.name || '';
56
+ const parts = filePath.split(/[\\/]/);
57
+ const ancestry = parts.length > 4 ? parts.slice(-4, -1) : parts.slice(0, -1);
58
+
59
+ // v3.2 Intuition: Resolve Bundle affinity
60
+ let bundleId = null;
61
+ for (const [bPath, bId] of bundles.entries()) {
62
+ if (filePath.startsWith(bPath)) {
63
+ bundleId = bId;
64
+ break;
65
+ }
66
+ }
67
+
68
+ // Keep only a few samples per category to save tokens
69
+ if (clusters[cat].samples.length < 5) {
70
+ clusters[cat].samples.push({
71
+ name: f.name,
72
+ path: f.path,
73
+ ext: path.extname(f.name),
74
+ ancestry,
75
+ bundleId
76
+ });
77
+ }
78
+ }
79
+
80
+ return {
81
+ summary: {
82
+ totalFiles: files.length,
83
+ totalSize: totalBytes,
84
+ categories: Object.keys(clusters),
85
+ bundleCount: bundles.size
86
+ },
87
+ clusters
88
+ };
89
+ }
90
+ }
91
+
92
+ module.exports = MetadataSentry;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * ═══════════════════════════════════════════════════════════════════
3
+ * FILEMAYOR AGENTIC CREW — INTENT STRATEGIST
4
+ *
5
+ * Universal domain-aware intent classification.
6
+ * Detects folder "archetypes" from filenames, extensions, and
7
+ * directory names — then maps to the optimal organization strategy.
8
+ *
9
+ * Supports: Music Production, Civics/Law, Agri-Tech, Business,
10
+ * Technical/Code, Library, and General Archive.
11
+ * ═══════════════════════════════════════════════════════════════════
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const path = require('path');
17
+
18
+ // ─── Domain Archetype Definitions ─────────────────────────────────
19
+
20
+ const ARCHETYPES = {
21
+ production: {
22
+ label: 'Music Production',
23
+ extSignals: ['.wav', '.mp3', '.flac', '.als', '.flp', '.mid', '.aif', '.ogg', '.aac', '.stem'],
24
+ nameSignals: ['_v1', '_v2', '_final', '_master', '_mix', 'stem', 'beat', 'vocal', 'inst'],
25
+ strategy: 'atomic_bundle_preservation',
26
+ hierarchy: 'Project Name → Stems / Masters / Project Files'
27
+ },
28
+ civics: {
29
+ label: 'Civics & Law (RSA)',
30
+ extSignals: ['.pdf', '.docx', '.doc', '.odt', '.jpg', '.png'],
31
+ nameSignals: ['sars', 'gazette', 'court', 'affidavit', 'invoice', 'contract', 'act_',
32
+ 'regulation', 'compliance', 'permit', 'certificate', 'agsa', 'pfma', 'sca',
33
+ 'judgment', 'bill', 'irp5', 'it3a', 'green_paper', 'white_paper', 'audit_report',
34
+ 'fica', 'cipc', 'bbbee', 'labour_', 'municipal'],
35
+ strategy: 'entity_year_doctype',
36
+ hierarchy: 'Authority → Year → Document Type',
37
+ folders: ['01_Legislation', '02_Compliance', '03_Oversight', '04_Judiciary']
38
+ },
39
+ agritech: {
40
+ label: 'Agri-Tech (Farm)',
41
+ extSignals: ['.csv', '.xlsx', '.xls', '.tsv'],
42
+ nameSignals: ['tag_', 'vet_', 'herd', 'cattle', 'livestock', 'feed', 'breed',
43
+ 'pasture', 'dip_', 'auction', 'kraal', 'bovine', 'vaccination', 'fmd',
44
+ 'brucella', 'deworming', 'weaner', 'heifer', 'bull_', 'calf_', 'stud_',
45
+ 'transport_permit', 'movement_permit', 'feedlot'],
46
+ strategy: 'category_datestamp',
47
+ hierarchy: 'Livestock Category → Date-Stamped Logs',
48
+ folders: ['Inventory', 'Health', 'Logistics']
49
+ },
50
+ business: {
51
+ label: 'Business',
52
+ extSignals: ['.pdf', '.png', '.jpg', '.xlsx'],
53
+ nameSignals: ['invoice_', 'receipt', 'quotation', 'po_', 'statement', 'expense', 'tax_', 'vat', 'payslip', 'order_'],
54
+ strategy: 'quarter_vendor_status',
55
+ hierarchy: 'Quarter → Vendor → Status'
56
+ },
57
+ technical: {
58
+ label: 'Technical / Code',
59
+ extSignals: ['.js', '.ts', '.py', '.go', '.rs', '.java', '.cpp', '.c', '.h', '.json', '.yaml', '.yml', '.toml', '.env', '.sh'],
60
+ nameSignals: ['config', 'test_', '_spec', 'build', 'deploy', 'docker', 'readme', 'license', 'makefile'],
61
+ strategy: 'substrate_preservation',
62
+ hierarchy: 'Project Root → src / tests / config / docs'
63
+ },
64
+ library: {
65
+ label: 'Library / Reference',
66
+ extSignals: ['.pdf', '.epub', '.mobi', '.djvu'],
67
+ nameSignals: ['book', 'chapter', 'isbn', 'edition', 'vol_', 'author'],
68
+ strategy: 'ancestry_matching',
69
+ hierarchy: 'Genre / Subject → Author → Title'
70
+ }
71
+ };
72
+
73
+ class IntentStrategist {
74
+ /**
75
+ * Classify user intent from a prompt string.
76
+ * Uses keyword matching for fast, local classification.
77
+ */
78
+ classify(prompt, context = {}) {
79
+ const p = prompt.toLowerCase();
80
+ let strategy = 'clustering';
81
+ let intent = 'organize';
82
+ let archetype = null;
83
+
84
+ const isRefine = p.includes('refine') || p.includes('structure') || p.includes('within');
85
+
86
+ if (p.includes('clean') || p.includes('space') || p.includes('junk')) {
87
+ intent = 'cleanup';
88
+ strategy = 'retention';
89
+ } else if (p.includes('tax') || p.includes('invoice') || p.includes('receipt')) {
90
+ intent = 'financial_sorting';
91
+ strategy = 'deep_scan';
92
+ archetype = 'business';
93
+ } else if (p.includes('music') || p.includes('stem') || p.includes('production') || p.includes('beat')) {
94
+ intent = 'music_production';
95
+ strategy = 'atomic_bundle_preservation';
96
+ archetype = 'production';
97
+ } else if (p.includes('book') || p.includes('library') || p.includes('read')) {
98
+ intent = 'library_curation';
99
+ strategy = 'ancestry_matching';
100
+ archetype = 'library';
101
+ } else if (p.includes('farm') || p.includes('cattle') || p.includes('herd') || p.includes('livestock')) {
102
+ intent = 'agritech_management';
103
+ strategy = 'category_datestamp';
104
+ archetype = 'agritech';
105
+ } else if (p.includes('law') || p.includes('legal') || p.includes('court') || p.includes('compliance')) {
106
+ intent = 'civics_filing';
107
+ strategy = 'entity_year_doctype';
108
+ archetype = 'civics';
109
+ } else if (p.includes('code') || p.includes('project') || p.includes('repo')) {
110
+ intent = 'codebase_org';
111
+ strategy = 'substrate_preservation';
112
+ archetype = 'technical';
113
+ }
114
+
115
+ return {
116
+ intent,
117
+ strategy,
118
+ isRefine,
119
+ archetype: archetype ? ARCHETYPES[archetype] : null,
120
+ rules: {
121
+ lockBundles: true,
122
+ respectAncestry: true,
123
+ depthLimit: 3
124
+ }
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Detect the folder archetype from file metadata (zero-content).
130
+ * Scores each archetype by counting matching extension and name signals.
131
+ * Includes "Parental Hint" — the directory name itself influences scoring.
132
+ *
133
+ * @param {Array} files - Array of { name, ext?, size? }
134
+ * @param {string} [folderName] - Name of the parent directory (parental hint)
135
+ * @returns {{ archetype: string, label: string, confidence: number, strategy: string }}
136
+ */
137
+ detectArchetype(files, folderName = '') {
138
+ const scores = {};
139
+ const folderHint = (folderName || '').toLowerCase();
140
+
141
+ for (const [key, arch] of Object.entries(ARCHETYPES)) {
142
+ let score = 0;
143
+
144
+ for (const file of files) {
145
+ const name = (file.name || '').toLowerCase();
146
+ const ext = (file.ext || path.extname(name)).toLowerCase();
147
+
148
+ // Extension match
149
+ if (arch.extSignals.includes(ext)) score += 2;
150
+
151
+ // Name signal match
152
+ for (const signal of arch.nameSignals) {
153
+ if (name.includes(signal)) score += 3;
154
+ }
155
+ }
156
+
157
+ // Parental hint bonus (directory name contains archetype keywords)
158
+ for (const signal of arch.nameSignals) {
159
+ if (folderHint.includes(signal)) score += 5;
160
+ }
161
+
162
+ scores[key] = score;
163
+ }
164
+
165
+ // Find the winner
166
+ const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
167
+ const [topKey, topScore] = sorted[0];
168
+ const totalSignals = Object.values(scores).reduce((a, b) => a + b, 0);
169
+ const confidence = totalSignals > 0 ? Math.round((topScore / totalSignals) * 100) : 0;
170
+
171
+ if (topScore === 0) {
172
+ return { archetype: 'general', label: 'General / Mixed', confidence: 0, strategy: 'clustering' };
173
+ }
174
+
175
+ const arch = ARCHETYPES[topKey];
176
+ return {
177
+ archetype: topKey,
178
+ label: arch.label,
179
+ confidence,
180
+ strategy: arch.strategy,
181
+ hierarchy: arch.hierarchy
182
+ };
183
+ }
184
+ }
185
+
186
+ module.exports = IntentStrategist;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * ═══════════════════════════════════════════════════════════════════
3
+ * FILEMAYOR AGENTIC CREW — SECURITY ARCHITECT
4
+ * Deterministic validator. Never trusts AI output.
5
+ * ═══════════════════════════════════════════════════════════════════
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const { isFileSafe, validatePath } = require('../security');
11
+ const os = require('os');
12
+
13
+ class SecurityArchitect {
14
+ constructor() {
15
+ // Dynamically resolve home directory parts for path normalization
16
+ const homeParts = os.homedir().toLowerCase().replace(/\\/g, '/').split('/').filter(x => x.length > 0);
17
+ this._homePrefixParts = new Set(homeParts); // e.g. Set{'c:', 'users', 'hloni'}
18
+ }
19
+
20
+ /**
21
+ * Final validation of an AI-generated plan
22
+ * @param {Object} plan - The Curative Plan from Planner
23
+ * @param {Object} sentryData - The ground-truth telemetry
24
+ * @returns {Object} Validated/Sanitized Plan
25
+ */
26
+ validate(plan, sentryData) {
27
+ if (!plan || !Array.isArray(plan.plan)) {
28
+ return { ...plan, plan: [], status: 'invalid' };
29
+ }
30
+
31
+ const domainTriggerFolders = ['books', 'library', 'projects', 'music', 'work', 'downloads'];
32
+ const clusters = sentryData?.clusters || {};
33
+ const allSamples = Object.values(clusters).flatMap(c => c.samples || []);
34
+ const homeParts = this._homePrefixParts;
35
+
36
+ // Robust Normalizer: strips drive letter and home dir parts, leaving only domain-relevant segments
37
+ const normalize = (p) => {
38
+ if (!p) return [];
39
+ return p.toLowerCase()
40
+ .replace(/\\/g, '/')
41
+ .split('/')
42
+ .filter(x => x.length > 0 && !homeParts.has(x));
43
+ };
44
+
45
+ const safeSteps = plan.plan.filter(step => {
46
+ try {
47
+ if (!step.source || !step.destination) return false;
48
+
49
+ const srcCheck = isFileSafe(step.source);
50
+ if (!srcCheck.safe) return false;
51
+
52
+ // 1. Domain Preservation (Rule of Ancestry)
53
+ const srcParts = normalize(step.source);
54
+ const destParts = normalize(step.destination);
55
+
56
+ const triggerFound = srcParts.find(p => domainTriggerFolders.includes(p));
57
+
58
+ if (triggerFound) {
59
+ // A move is valid if the EXACT trigger folder is present in the destination path
60
+ const preserved = destParts.includes(triggerFound);
61
+
62
+ if (!preserved && !step.reason.includes('COLLECTION_MOVE')) {
63
+ console.warn(`[ARCHITECTURE] Blocked domain scattering for ${step.source} -> ${step.destination}`);
64
+ return false;
65
+ }
66
+ }
67
+
68
+ // 2. Atomic Bundle Lock
69
+ const sourceMeta = allSamples.find(s => s.path === step.source);
70
+ if (sourceMeta && sourceMeta.bundleId) {
71
+ const bundleId = sourceMeta.bundleId;
72
+ const bundleMembers = allSamples.filter(s => s.bundleId === bundleId);
73
+ const planMembers = plan.plan.filter(s => {
74
+ const meta = allSamples.find(m => m.path === s.source);
75
+ return meta && meta.bundleId === bundleId;
76
+ });
77
+
78
+ // If not all members are moving, it's a split.
79
+ if (planMembers.length < bundleMembers.length) {
80
+ console.warn(`[ARCHITECTURE] Blocked bundle split for ${step.source} (Bundle: ${bundleId})`);
81
+ return false;
82
+ }
83
+
84
+ // Ensure all members are going to the same parent domain
85
+ const firstDestParts = normalize(planMembers[0].destination);
86
+ const bundleDestRoot = firstDestParts.slice(0, -1).join('/');
87
+
88
+ const matches = planMembers.every(s => {
89
+ const dParts = normalize(s.destination);
90
+ const dRoot = dParts.slice(0, -1).join('/');
91
+ return dRoot === bundleDestRoot;
92
+ });
93
+
94
+ if (!matches) {
95
+ console.warn(`[ARCHITECTURE] Blocked bundle scattering for ${step.source}`);
96
+ return false;
97
+ }
98
+ }
99
+
100
+ return true;
101
+ } catch (err) {
102
+ console.error('[ARCHITECTURE] Validation Error:', err);
103
+ return false;
104
+ }
105
+ });
106
+
107
+ return {
108
+ ...plan,
109
+ plan: safeSteps,
110
+ validatedCount: safeSteps.length,
111
+ status: safeSteps.length > 0 ? 'safe' : 'blocked'
112
+ };
113
+ }
114
+ }
115
+
116
+ module.exports = SecurityArchitect;
package/core/analyzer.js CHANGED
@@ -76,15 +76,26 @@ function analyzeDirectory(dirPath, options = {}) {
76
76
  }
77
77
  }
78
78
  }
79
-
80
- // 3. Bloat Mapping (Top Directories)
79
+ // 3. Bloat Mapping (Top Directories) & Progress Reporting
81
80
  const dirMap = new Map();
82
- for (const file of files) {
81
+ for (let i = 0; i < files.length; i++) {
82
+ const file = files[i];
83
+
84
+ // Progress callback
85
+ if (options.onProgress) {
86
+ options.onProgress({
87
+ current: i + 1,
88
+ total: files.length,
89
+ percent: Math.round(((i + 1) / files.length) * 100),
90
+ file: file.name
91
+ });
92
+ }
93
+
83
94
  const parts = file.relativePath.split(path.sep);
84
95
  let currentPath = '';
85
96
  // Only track top-level and second-level folders for bloat mapping
86
- for (let i = 0; i < Math.min(parts.length - 1, 2); i++) {
87
- currentPath = currentPath ? path.join(currentPath, parts[i]) : parts[i];
97
+ for (let j = 0; j < Math.min(parts.length - 1, 2); j++) {
98
+ currentPath = currentPath ? path.join(currentPath, parts[j]) : parts[j];
88
99
  if (!dirMap.has(currentPath)) {
89
100
  dirMap.set(currentPath, { size: 0, count: 0 });
90
101
  }
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ═══════════════════════════════════════════════════════════════════
5
+ * FILEMAYOR CORE — EMERGENCY HALT HANDLER ("The Black Box")
6
+ *
7
+ * Intercepts Ctrl+C, SIGTERM, and uncaught exceptions to ensure
8
+ * the Master Journal is flushed to disk before the process dies.
9
+ * Without this, a partial batch leaves an un-recoverable mess.
10
+ *
11
+ * Uses writeFileSync to BLOCK the event loop during flush —
12
+ * guaranteeing the JSON data reaches the SSD even during panic.
13
+ * ═══════════════════════════════════════════════════════════════════
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ let _isInitialized = false;
22
+ let _journalRef = null;
23
+ let _journalPath = null;
24
+
25
+ /**
26
+ * Initialize the Emergency Halt handler.
27
+ * Call this ONCE at the start of any batch operation.
28
+ *
29
+ * @param {Array} journalRef - Reference to the live journal array
30
+ * @param {string} journalPath - Absolute path to the journal file
31
+ */
32
+ function initEmergencyHalt(journalRef, journalPath) {
33
+ if (_isInitialized) return; // Prevent double-registration
34
+
35
+ _journalRef = journalRef;
36
+ _journalPath = journalPath;
37
+ _isInitialized = true;
38
+
39
+ const flushJournal = (reason) => {
40
+ if (!_journalRef || _journalRef.length === 0) return;
41
+
42
+ console.log(`\n\n🛑 [EMERGENCY HALT] ${reason}`);
43
+ console.log(' Saving progress to Master Journal...');
44
+
45
+ try {
46
+ const data = JSON.stringify(_journalRef, null, 2);
47
+
48
+ // CRITICAL: writeFileSync blocks the event loop to guarantee persistence
49
+ // Permission 0o600 = owner-only read/write (anti-journal-poisoning)
50
+ fs.writeFileSync(_journalPath, data, { mode: 0o600 });
51
+
52
+ console.log(` ✅ Progress saved (${_journalRef.length} entries).`);
53
+ console.log(` Run 'filemayor undo' to revert.`);
54
+ } catch (err) {
55
+ console.error(` ❌ CRITICAL: Could not save Journal: ${err.message}`);
56
+ }
57
+ };
58
+
59
+ // Intercept Ctrl+C (SIGINT), Terminal Closure (SIGTERM), HUP
60
+ const signals = ['SIGINT', 'SIGTERM'];
61
+ // SIGHUP is not available on Windows, add conditionally
62
+ if (process.platform !== 'win32') signals.push('SIGHUP');
63
+
64
+ for (const signal of signals) {
65
+ process.on(signal, () => {
66
+ flushJournal(`Received ${signal}`);
67
+ process.exit(130); // Standard exit code for signal termination
68
+ });
69
+ }
70
+
71
+ // Catch unexpected crashes (the "Vibe-Coding Guard")
72
+ process.on('uncaughtException', (err) => {
73
+ console.error(`\n💥 [INTERNAL CRASH]: ${err.message}`);
74
+ console.error(err.stack);
75
+ flushJournal('Uncaught Exception');
76
+ process.exit(1);
77
+ });
78
+
79
+ process.on('unhandledRejection', (reason) => {
80
+ console.error(`\n💥 [UNHANDLED REJECTION]: ${reason}`);
81
+ flushJournal('Unhandled Promise Rejection');
82
+ process.exit(1);
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Update the journal reference during a batch operation.
88
+ * Use this to keep the halt handler pointed at the latest data.
89
+ * @param {Array} journalRef - Updated reference
90
+ */
91
+ function updateJournalRef(journalRef) {
92
+ _journalRef = journalRef;
93
+ }
94
+
95
+ /**
96
+ * Clean shutdown — call after a batch completes successfully.
97
+ * Resets the halt handler state.
98
+ */
99
+ function clearEmergencyHalt() {
100
+ _journalRef = null;
101
+ _isInitialized = false;
102
+ }
103
+
104
+ module.exports = { initEmergencyHalt, updateJournalRef, clearEmergencyHalt };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * ═══════════════════════════════════════════════════════════════════
3
+ * FILEMAYOR v3.5 — APPLY ENGINE
4
+ * The Executioner: Reifies the Curative Plan into reality.
5
+ * ═══════════════════════════════════════════════════════════════════
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const FileMayorFS = require('../fs-abstraction');
11
+
12
+ class ApplyEngine {
13
+ constructor(options = {}) {
14
+ this.fs = new FileMayorFS(options);
15
+ }
16
+
17
+ /**
18
+ * Execute a validated Curative Plan
19
+ * @param {Object} plan - The plan from CureEngine
20
+ * @param {Object} progressCb - Optional progress callback
21
+ * @returns {Promise<Object>} Execution results
22
+ */
23
+ async apply(plan, progressCb = null) {
24
+ if (!plan || !Array.isArray(plan.plan)) {
25
+ throw new Error('Invalid plan: Nothing to apply.');
26
+ }
27
+
28
+ const stats = {
29
+ total: plan.plan.length,
30
+ success: 0,
31
+ failed: 0
32
+ };
33
+
34
+ for (let i = 0; i < plan.plan.length; i++) {
35
+ const step = plan.plan[i];
36
+ try {
37
+ await this.fs.move(step.source, step.destination);
38
+ stats.success++;
39
+ if (progressCb) progressCb({
40
+ index: i + 1,
41
+ total: stats.total,
42
+ current: step.source,
43
+ status: 'success'
44
+ });
45
+ } catch (err) {
46
+ stats.failed++;
47
+ if (progressCb) progressCb({
48
+ index: i + 1,
49
+ total: stats.total,
50
+ current: step.source,
51
+ status: 'error',
52
+ error: err.message
53
+ });
54
+ }
55
+ }
56
+
57
+ return {
58
+ status: stats.failed === 0 ? 'completed' : 'partial',
59
+ stats,
60
+ journalCount: this.fs.getJournal().length
61
+ };
62
+ }
63
+
64
+ async rollback() {
65
+ return await this.fs.rollback();
66
+ }
67
+ }
68
+
69
+ module.exports = ApplyEngine;