@arela/uploader 1.1.4 → 1.2.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/.claude/worktrees/agent-multi-profile/.env.template +224 -0
- package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
- package/.claude/worktrees/agent-multi-profile/README.md +405 -0
- package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
- package/.claude/worktrees/agent-multi-profile/package.json +78 -0
- package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
- package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
- package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
- package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
- package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
- package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
- package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
- package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
- package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
- package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
- package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
- package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
- package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
- package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
- package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
- package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
- package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
- package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
- package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
- package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
- package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
- package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
- package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
- package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
- package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
- package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
- package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
- package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
- package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
- package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
- package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
- package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
- package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
- package/README.md +65 -0
- package/package.json +1 -1
- package/src/commands/AgentCommand.js +210 -0
- package/src/commands/AgentInitCommand.js +316 -0
- package/src/commands/PollWorkerCommand.js +11 -322
- package/src/config/config.js +44 -6
- package/src/index.js +55 -0
- package/src/services/LoggingService.js +35 -0
- package/src/services/PipelineApiService.js +7 -1
- package/src/services/PipelineJobRunner.js +389 -0
- package/src/services/ProfileManager.js +164 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arela/uploader",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "CLI to upload files/directories to Arela",
|
|
5
|
+
"bin": {
|
|
6
|
+
"arela": "./src/index.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node ./src/index.js",
|
|
11
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
12
|
+
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
|
|
13
|
+
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
|
|
14
|
+
"format": "prettier --write \"src/**/*.js\""
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/inspiraCode/arela-uploader.git"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"arela",
|
|
22
|
+
"cli",
|
|
23
|
+
"uploader",
|
|
24
|
+
"record-keeping"
|
|
25
|
+
],
|
|
26
|
+
"author": "Alfredo Pacheco",
|
|
27
|
+
"license": "ISC",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/inspiraCode/arela-uploader/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/inspiraCode/arela-uploader#readme",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@supabase/supabase-js": "2.49.4",
|
|
34
|
+
"bullmq": "^5.71.0",
|
|
35
|
+
"chokidar": "^4.0.3",
|
|
36
|
+
"cli-progress": "3.12.0",
|
|
37
|
+
"commander": "13.1.0",
|
|
38
|
+
"dotenv": "16.5.0",
|
|
39
|
+
"form-data": "4.0.4",
|
|
40
|
+
"formdata-node": "^6.0.3",
|
|
41
|
+
"globby": "14.1.0",
|
|
42
|
+
"googleapis": "^171.4.0",
|
|
43
|
+
"ioredis": "^5.10.0",
|
|
44
|
+
"mime-types": "3.0.1",
|
|
45
|
+
"node-fetch": "3.3.2",
|
|
46
|
+
"office-text-extractor": "3.0.3",
|
|
47
|
+
"p-limit": "^7.2.0",
|
|
48
|
+
"pdf-parse": "^2.4.5"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@jest/globals": "^30.2.0",
|
|
52
|
+
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
|
53
|
+
"jest": "^30.2.0",
|
|
54
|
+
"prettier": "3.5.3"
|
|
55
|
+
},
|
|
56
|
+
"jest": {
|
|
57
|
+
"testEnvironment": "node",
|
|
58
|
+
"transform": {},
|
|
59
|
+
"testMatch": [
|
|
60
|
+
"**/tests/**/*.test.js"
|
|
61
|
+
],
|
|
62
|
+
"testPathIgnorePatterns": [
|
|
63
|
+
"/node_modules/",
|
|
64
|
+
"/scripts/"
|
|
65
|
+
],
|
|
66
|
+
"collectCoverageFrom": [
|
|
67
|
+
"src/commands/**/*.js",
|
|
68
|
+
"!src/commands/**/index.js"
|
|
69
|
+
],
|
|
70
|
+
"coverageDirectory": "coverage",
|
|
71
|
+
"coverageReporters": [
|
|
72
|
+
"text",
|
|
73
|
+
"text-summary",
|
|
74
|
+
"html",
|
|
75
|
+
"lcov"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cleanup-ds-store.js
|
|
5
|
+
* Remove .DS_Store and other system file records from the uploader table
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import logger from './src/services/LoggingService.js';
|
|
11
|
+
import { databaseService } from './src/services/DatabaseService.js';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
|
17
|
+
console.log('║ Cleaning System Files from Uploader Table ║');
|
|
18
|
+
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
|
19
|
+
|
|
20
|
+
async function cleanupSystemFiles() {
|
|
21
|
+
try {
|
|
22
|
+
const supabase = await databaseService.getSupabaseClient();
|
|
23
|
+
|
|
24
|
+
// System file patterns to remove
|
|
25
|
+
const systemFiles = [
|
|
26
|
+
'.DS_Store',
|
|
27
|
+
'Thumbs.db',
|
|
28
|
+
'desktop.ini',
|
|
29
|
+
'.directory',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
let totalDeleted = 0;
|
|
33
|
+
|
|
34
|
+
for (const fileName of systemFiles) {
|
|
35
|
+
console.log(`\n🔍 Searching for ${fileName}...`);
|
|
36
|
+
|
|
37
|
+
// Find records with this filename
|
|
38
|
+
const { data: records, error: selectError } = await supabase
|
|
39
|
+
.from('uploader')
|
|
40
|
+
.select('*')
|
|
41
|
+
.eq('filename', fileName);
|
|
42
|
+
|
|
43
|
+
if (selectError) {
|
|
44
|
+
console.error(`❌ Error searching for ${fileName}: ${selectError.message}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!records || records.length === 0) {
|
|
49
|
+
console.log(` ✓ No records found for ${fileName}`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(` Found ${records.length} record(s) for ${fileName}`);
|
|
54
|
+
|
|
55
|
+
// Get the IDs to delete
|
|
56
|
+
const idsToDelete = records.map(r => r.id);
|
|
57
|
+
|
|
58
|
+
// Delete records
|
|
59
|
+
const { error: deleteError } = await supabase
|
|
60
|
+
.from('uploader')
|
|
61
|
+
.delete()
|
|
62
|
+
.in('id', idsToDelete);
|
|
63
|
+
|
|
64
|
+
if (deleteError) {
|
|
65
|
+
console.error(` ❌ Error deleting: ${deleteError.message}`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(` ✅ Deleted ${records.length} record(s)`);
|
|
70
|
+
totalDeleted += records.length;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Also delete any records with original_path containing .DS_Store
|
|
74
|
+
console.log(`\n🔍 Searching for records with .DS_Store in path...`);
|
|
75
|
+
const { data: pathRecords, error: pathError } = await supabase
|
|
76
|
+
.from('uploader')
|
|
77
|
+
.select('*')
|
|
78
|
+
.ilike('original_path', '%.DS_Store%');
|
|
79
|
+
|
|
80
|
+
if (!pathError && pathRecords && pathRecords.length > 0) {
|
|
81
|
+
console.log(` Found ${pathRecords.length} record(s)`);
|
|
82
|
+
const idsToDelete = pathRecords.map(r => r.id);
|
|
83
|
+
|
|
84
|
+
const { error: deleteError } = await supabase
|
|
85
|
+
.from('uploader')
|
|
86
|
+
.delete()
|
|
87
|
+
.in('id', idsToDelete);
|
|
88
|
+
|
|
89
|
+
if (!deleteError) {
|
|
90
|
+
console.log(` ✅ Deleted ${pathRecords.length} record(s)`);
|
|
91
|
+
totalDeleted += pathRecords.length;
|
|
92
|
+
} else {
|
|
93
|
+
console.error(` ❌ Error deleting: ${deleteError.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
|
98
|
+
console.log(`║ ✅ Cleanup Complete: Deleted ${totalDeleted} system file record(s) ║`);
|
|
99
|
+
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
|
100
|
+
|
|
101
|
+
process.exit(0);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('\n❌ ERROR');
|
|
104
|
+
console.error(error.message);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
cleanupSystemFiles();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cleanup-system-files.js
|
|
5
|
+
* Remove system files (.DS_Store, etc) from the uploader table
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
|
15
|
+
console.log('║ Cleaning System Files from Database ║');
|
|
16
|
+
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const databaseService = (await import('./src/services/DatabaseService.js')).default;
|
|
20
|
+
const supabase = await databaseService.getSupabaseClient();
|
|
21
|
+
|
|
22
|
+
// System file patterns to remove
|
|
23
|
+
const systemFilePatterns = ['.DS_Store', '__pycache__', '.pyc', '.swp', '.swo', 'Thumbs.db', 'desktop.ini'];
|
|
24
|
+
|
|
25
|
+
console.log('🔍 Searching for system files...\n');
|
|
26
|
+
|
|
27
|
+
let totalRemoved = 0;
|
|
28
|
+
|
|
29
|
+
for (const pattern of systemFilePatterns) {
|
|
30
|
+
const { data: records, error } = await supabase
|
|
31
|
+
.from('uploader')
|
|
32
|
+
.select('id, filename')
|
|
33
|
+
.ilike('filename', `%${pattern}%`);
|
|
34
|
+
|
|
35
|
+
if (!error && records && records.length > 0) {
|
|
36
|
+
console.log(`📝 Found ${records.length} record(s) with "${pattern}"`);
|
|
37
|
+
records.forEach(r => {
|
|
38
|
+
console.log(` - ID: ${r.id}, Filename: ${r.filename}`);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Delete them
|
|
42
|
+
const ids = records.map(r => r.id);
|
|
43
|
+
const { error: deleteError } = await supabase
|
|
44
|
+
.from('uploader')
|
|
45
|
+
.delete()
|
|
46
|
+
.in('id', ids);
|
|
47
|
+
|
|
48
|
+
if (!deleteError) {
|
|
49
|
+
console.log(` ✅ Deleted ${records.length} record(s)\n`);
|
|
50
|
+
totalRemoved += records.length;
|
|
51
|
+
} else {
|
|
52
|
+
console.log(` ❌ Error deleting records: ${deleteError.message}\n`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log('╔════════════════════════════════════════════════════════════════╗');
|
|
58
|
+
console.log(`║ ✅ CLEANUP COMPLETED ║`);
|
|
59
|
+
console.log(`║ ║`);
|
|
60
|
+
console.log(`║ Total system files removed: ${totalRemoved}`);
|
|
61
|
+
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
|
62
|
+
|
|
63
|
+
process.exit(0);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('\n❌ CLEANUP FAILED');
|
|
66
|
+
console.error(error.message);
|
|
67
|
+
console.error(error.stack);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoring engine validation harness (PROTOTYPE).
|
|
3
|
+
*
|
|
4
|
+
* Runs the CURRENT first-match-wins engine (`extractDocumentFields`) and the new
|
|
5
|
+
* best-match scoring engine (`classifyDocument`) over the same corpus and prints
|
|
6
|
+
* a side-by-side comparison so we can confirm best-match reproduces (or
|
|
7
|
+
* improves on) the current behaviour before wiring it into the pipeline.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/scoring-compare.js # built-in synthetic samples
|
|
11
|
+
* node scripts/scoring-compare.js <folder> # + real .pdf/.xml/.txt files
|
|
12
|
+
*
|
|
13
|
+
* The built-in samples include the `factura_inter_agencia` vs
|
|
14
|
+
* `factura_comercial` case, which the current engine only resolves via
|
|
15
|
+
* registration order — the harness shows best-match resolving it by score,
|
|
16
|
+
* independent of matcher order.
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
|
|
21
|
+
import { extractDocumentFields } from '../src/document-type-shared.js';
|
|
22
|
+
import FileDetectionService from '../src/file-detection.js';
|
|
23
|
+
import { classifyDocument, scoreAll } from '../src/scoring/scoring-engine.js';
|
|
24
|
+
import { scoringMatchers } from '../src/scoring/matchers-seed.js';
|
|
25
|
+
|
|
26
|
+
// --------------------------- synthetic corpus -------------------------------
|
|
27
|
+
// Compact, representative texts that trigger the relevant clues. Real pdf-parse
|
|
28
|
+
// output is messier — pass a folder to validate against production documents.
|
|
29
|
+
const SAMPLES = [
|
|
30
|
+
{
|
|
31
|
+
name: 'simplificado-paid',
|
|
32
|
+
extension: 'pdf',
|
|
33
|
+
expected: 'pedimento_simplificado',
|
|
34
|
+
text: `FORMA SIMPLIFICADA DEL PEDIMENTO
|
|
35
|
+
NUM. PEDIMENTO: 26 07 3429 6000079
|
|
36
|
+
CVE. PEDIMENTO: A1
|
|
37
|
+
T. OPER: IMP
|
|
38
|
+
RFC: CSM9204097Q1
|
|
39
|
+
FECHA DE PAGO: 04/03/2026
|
|
40
|
+
*** PAGO ELECTRONICO ***`,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'simplificado-unpaid (proforma)',
|
|
44
|
+
extension: 'pdf',
|
|
45
|
+
expected: 'proforma',
|
|
46
|
+
text: `FORMA SIMPLIFICADA DE PEDIMENTO
|
|
47
|
+
NUM. PEDIMENTO: 26 07 3429 6000080
|
|
48
|
+
CVE. PEDIMENTO: A1
|
|
49
|
+
T. OPER: IMP
|
|
50
|
+
RFC: CSM9204097Q1
|
|
51
|
+
*** NO PAGADO ***`,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'completo',
|
|
55
|
+
extension: 'pdf',
|
|
56
|
+
expected: 'pedimento_completo',
|
|
57
|
+
text: `NUM. PEDIMENTO: 26 07 3429 2002089
|
|
58
|
+
CVE. PEDIMENTO: A1
|
|
59
|
+
T. OPER: IMP
|
|
60
|
+
SEGUNDA COPIA TRANSPORTISTA
|
|
61
|
+
CERTIFICACIONES
|
|
62
|
+
CUADRO DE LIQUIDACION
|
|
63
|
+
*** PAGO ELECTRONICO ***
|
|
64
|
+
FECHA DE PAGO: 02/03/2026`,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'completo-xml',
|
|
68
|
+
extension: 'xml',
|
|
69
|
+
filePath: '/tmp/260734296016642.xml',
|
|
70
|
+
expected: 'pedimento_completo_xml',
|
|
71
|
+
text: `<?xml version="1.0"?>
|
|
72
|
+
<ns2:consultarPedimentoCompletoRespuesta>
|
|
73
|
+
<ns2:pedimento>6016642</ns2:pedimento>
|
|
74
|
+
<ns2:aduanaEntradaSalida><ns2:clave>70</ns2:clave></ns2:aduanaEntradaSalida>
|
|
75
|
+
<ns2:fechas><ns2:clave>2</ns2:clave><ns2:fecha>2026-03-02-06:00</ns2:fecha></ns2:fechas>
|
|
76
|
+
<ns2:fechas><ns2:clave>5</ns2:clave><ns2:fecha>2026-02-20-06:00</ns2:fecha></ns2:fechas>
|
|
77
|
+
<ns2:rfc>CSM9204097Q1</ns2:rfc>
|
|
78
|
+
</ns2:consultarPedimentoCompletoRespuesta>`,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'doda-pdf',
|
|
82
|
+
extension: 'pdf',
|
|
83
|
+
expected: 'doda_pdf',
|
|
84
|
+
text: `DOCUMENTO DE OPERACION PARA DESPACHO ADUANERO
|
|
85
|
+
DODA
|
|
86
|
+
VUCEM
|
|
87
|
+
||070|3429|2|4009029|109335668|A231|
|
|
88
|
+
2026-03-02`,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'doda-xml',
|
|
92
|
+
extension: 'xml',
|
|
93
|
+
expected: 'doda_xml',
|
|
94
|
+
text: `<?xml version="1.0"?>
|
|
95
|
+
<documentoOperacion>
|
|
96
|
+
<numPedimento>260734292002089</numPedimento>
|
|
97
|
+
<patenteAduanal>3429</patenteAduanal>
|
|
98
|
+
<aduanaDespacho>07</aduanaDespacho>
|
|
99
|
+
</documentoOperacion>`,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'inter-agencia (vs comercial)',
|
|
103
|
+
extension: 'xml',
|
|
104
|
+
expected: 'factura_inter_agencia',
|
|
105
|
+
text: `<cfdi:Comprobante xmlns:cfdi="..." TipoDeComprobante="I">
|
|
106
|
+
<cfdi:Emisor Rfc="NAA120215F20"/>
|
|
107
|
+
<cfdi:Receptor Rfc="PCC1008161WA"/>
|
|
108
|
+
<cfdi:Concepto ClaveProdServ="78141502" Descripcion="Servicios de agente aduanal"/>
|
|
109
|
+
</cfdi:Comprobante>`,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'factura-comercial',
|
|
113
|
+
extension: 'xml',
|
|
114
|
+
expected: 'factura_comercial',
|
|
115
|
+
text: `<cfdi:Comprobante xmlns:cfdi="..." TipoDeComprobante="I">
|
|
116
|
+
<cfdi:Emisor Rfc="ABC010101AB1"/>
|
|
117
|
+
<cfdi:Receptor Rfc="XYZ020202CD2"/>
|
|
118
|
+
<tfd:TimbreFiscalDigital/>
|
|
119
|
+
pedimento 26 07 3429 6016477
|
|
120
|
+
</cfdi:Comprobante>`,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'support-document',
|
|
124
|
+
extension: 'xml',
|
|
125
|
+
expected: 'support_document',
|
|
126
|
+
text: `<?xml version="1.0"?>
|
|
127
|
+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
|
128
|
+
<oxml:tipoOperacion>IMP</oxml:tipoOperacion>
|
|
129
|
+
<oxml:patenteAduanal>3429</oxml:patenteAduanal>
|
|
130
|
+
</soapenv:Envelope>`,
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
// --------------------------- comparison -------------------------------------
|
|
135
|
+
function firstMatchType(source, extension, filePath) {
|
|
136
|
+
const [type] = extractDocumentFields(source, extension, filePath);
|
|
137
|
+
return type;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function bestMatchResult(source, extension, filePath) {
|
|
141
|
+
return classifyDocument(scoringMatchers, { source, extension, filePath });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function topCandidates(source, extension, filePath, n = 3) {
|
|
145
|
+
return scoreAll(scoringMatchers, {
|
|
146
|
+
source,
|
|
147
|
+
extension,
|
|
148
|
+
fileName: filePath ? path.basename(filePath) : '',
|
|
149
|
+
})
|
|
150
|
+
.slice(0, n)
|
|
151
|
+
.map((c) => `${c.documentType}:${c.score}`)
|
|
152
|
+
.join(', ');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function row(name, first, best, expected) {
|
|
156
|
+
const agree = first === best ? 'sí ' : 'NO ';
|
|
157
|
+
const vsExp = expected ? (best === expected ? 'ok ' : '⚠️ ') : ' ';
|
|
158
|
+
return (
|
|
159
|
+
`${name.padEnd(34)} first=${String(first).padEnd(24)} ` +
|
|
160
|
+
`best=${String(best).padEnd(24)} coinciden=${agree} esperado=${vsExp}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function run() {
|
|
165
|
+
const folder = process.argv[2];
|
|
166
|
+
let total = 0;
|
|
167
|
+
let disagreements = 0;
|
|
168
|
+
|
|
169
|
+
console.log('\n=== Muestras sintéticas ===');
|
|
170
|
+
for (const s of SAMPLES) {
|
|
171
|
+
const first = firstMatchType(s.text, s.extension, s.filePath);
|
|
172
|
+
const best = bestMatchResult(s.text, s.extension, s.filePath).detectedType;
|
|
173
|
+
total++;
|
|
174
|
+
if (first !== best) disagreements++;
|
|
175
|
+
console.log(row(s.name, first, best, s.expected));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Order-independence demonstration for the inter-agencia/comercial case.
|
|
179
|
+
const ia = SAMPLES.find((s) => s.name.startsWith('inter-agencia'));
|
|
180
|
+
const reversed = [...scoringMatchers].reverse();
|
|
181
|
+
const normalWinner = classifyDocument(scoringMatchers, {
|
|
182
|
+
source: ia.text,
|
|
183
|
+
extension: ia.extension,
|
|
184
|
+
}).detectedType;
|
|
185
|
+
const reversedWinner = classifyDocument(reversed, {
|
|
186
|
+
source: ia.text,
|
|
187
|
+
extension: ia.extension,
|
|
188
|
+
}).detectedType;
|
|
189
|
+
console.log('\n=== Independencia de orden (inter-agencia) ===');
|
|
190
|
+
console.log(`candidatos (por score): ${topCandidates(ia.text, ia.extension)}`);
|
|
191
|
+
console.log(`seed normal -> ${normalWinner}`);
|
|
192
|
+
console.log(`seed invertido-> ${reversedWinner}`);
|
|
193
|
+
console.log(
|
|
194
|
+
`order-independent: ${normalWinner === reversedWinner ? 'sí ✅' : 'NO ❌'}`,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Optional: real files from a folder.
|
|
198
|
+
if (folder) {
|
|
199
|
+
if (!fs.existsSync(folder)) {
|
|
200
|
+
console.error(`\nCarpeta no existe: ${folder}`);
|
|
201
|
+
} else {
|
|
202
|
+
console.log(`\n=== Archivos reales (${folder}) ===`);
|
|
203
|
+
const detection = new FileDetectionService();
|
|
204
|
+
const files = walk(folder).filter((f) =>
|
|
205
|
+
['.pdf', '.xml', '.txt'].includes(path.extname(f).toLowerCase()),
|
|
206
|
+
);
|
|
207
|
+
for (const file of files) {
|
|
208
|
+
const ext = path.extname(file).toLowerCase().replace('.', '');
|
|
209
|
+
let text = '';
|
|
210
|
+
try {
|
|
211
|
+
text =
|
|
212
|
+
ext === 'pdf'
|
|
213
|
+
? await detection.extractTextFromPDF(file)
|
|
214
|
+
: fs.readFileSync(file, 'utf8');
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.log(`${path.basename(file).padEnd(34)} ERROR: ${err.message}`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const first = firstMatchType(text, ext, file);
|
|
220
|
+
const best = bestMatchResult(text, ext, file).detectedType;
|
|
221
|
+
total++;
|
|
222
|
+
if (first !== best) disagreements++;
|
|
223
|
+
console.log(row(path.basename(file), first, best, null));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log(
|
|
229
|
+
`\n=== Resumen: ${total} documentos, ${disagreements} divergencias first-vs-best ===\n`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function walk(dir) {
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
236
|
+
const full = path.join(dir, entry.name);
|
|
237
|
+
if (entry.isDirectory()) out.push(...walk(full));
|
|
238
|
+
else out.push(full);
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
run();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 validation: runs the REAL runtime path the uploader now uses
|
|
3
|
+
* (DB-shape matchers -> adaptDbMatchers -> classifyDocument with rich extraction)
|
|
4
|
+
* against a corpus and compares it to the legacy engine (extractDocumentFields).
|
|
5
|
+
*
|
|
6
|
+
* Usage: node scripts/scoring-phase4-check.js <folder>
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
import { extractDocumentFields } from '../src/document-type-shared.js';
|
|
12
|
+
import FileDetectionService from '../src/file-detection.js';
|
|
13
|
+
import { adaptDbMatchers } from '../src/scoring/db-matcher-adapter.js';
|
|
14
|
+
import { scoringMatchers } from '../src/scoring/matchers-seed.js';
|
|
15
|
+
import { classifyDocument } from '../src/scoring/scoring-engine.js';
|
|
16
|
+
|
|
17
|
+
// Serialize the local seed to the shape the API `/resolved` endpoint returns,
|
|
18
|
+
// so we exercise the adapter exactly as in production.
|
|
19
|
+
function toDbShape(matchers) {
|
|
20
|
+
return matchers.map((m) => ({
|
|
21
|
+
documentType: m.documentType,
|
|
22
|
+
extensions: m.extensions,
|
|
23
|
+
minScore: m.minScore ?? null,
|
|
24
|
+
priority: m.priority ?? 0,
|
|
25
|
+
qualify: m.qualify ?? null,
|
|
26
|
+
clues: (m.clues || []).map((c) => ({
|
|
27
|
+
kind: c.kind,
|
|
28
|
+
pattern: c.pattern instanceof RegExp ? c.pattern.source : c.pattern,
|
|
29
|
+
flags: c.pattern instanceof RegExp ? c.pattern.flags : c.flags || '',
|
|
30
|
+
weight: c.weight ?? 1,
|
|
31
|
+
group: c.group ?? null,
|
|
32
|
+
required: !!c.required,
|
|
33
|
+
negative: !!c.negative,
|
|
34
|
+
})),
|
|
35
|
+
fieldExtractors: [], // rich extraction comes from the registry by documentType
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const adapted = adaptDbMatchers(toDbShape(scoringMatchers));
|
|
40
|
+
|
|
41
|
+
function walk(dir) {
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
44
|
+
const full = path.join(dir, e.name);
|
|
45
|
+
if (e.isDirectory()) out.push(...walk(full));
|
|
46
|
+
else out.push(full);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function run() {
|
|
52
|
+
const folder = process.argv[2];
|
|
53
|
+
if (!folder) {
|
|
54
|
+
console.error('Pass a folder: node scripts/scoring-phase4-check.js <folder>');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const detection = new FileDetectionService();
|
|
58
|
+
const files = walk(folder).filter((f) =>
|
|
59
|
+
['.pdf', '.xml', '.txt'].includes(path.extname(f).toLowerCase()),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
let total = 0;
|
|
63
|
+
let diverge = 0;
|
|
64
|
+
const patterns = {};
|
|
65
|
+
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
const ext = path.extname(file).toLowerCase().replace('.', '');
|
|
68
|
+
let text = '';
|
|
69
|
+
try {
|
|
70
|
+
text =
|
|
71
|
+
ext === 'pdf'
|
|
72
|
+
? await detection.extractTextFromPDF(file)
|
|
73
|
+
: fs.readFileSync(file, 'utf8');
|
|
74
|
+
} catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const legacy = extractDocumentFields(text, ext, file)[0];
|
|
78
|
+
const phase4 = classifyDocument(adapted, {
|
|
79
|
+
source: text,
|
|
80
|
+
extension: ext,
|
|
81
|
+
filePath: file,
|
|
82
|
+
}).detectedType;
|
|
83
|
+
total++;
|
|
84
|
+
if (legacy !== phase4) {
|
|
85
|
+
diverge++;
|
|
86
|
+
const key = `${legacy} -> ${phase4}`;
|
|
87
|
+
patterns[key] = (patterns[key] || 0) + 1;
|
|
88
|
+
console.log(`NO ${path.basename(file).padEnd(40)} ${key}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`\n=== Fase 4 vs legacy: ${total} docs, ${diverge} divergencias ===`);
|
|
93
|
+
for (const [k, n] of Object.entries(patterns)) console.log(` ${n}× ${k}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
run();
|