@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.
Files changed (95) hide show
  1. package/.claude/worktrees/agent-multi-profile/.env.template +224 -0
  2. package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
  3. package/.claude/worktrees/agent-multi-profile/README.md +405 -0
  4. package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
  5. package/.claude/worktrees/agent-multi-profile/package.json +78 -0
  6. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
  7. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
  8. package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
  9. package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
  10. package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
  11. package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
  12. package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
  13. package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
  14. package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
  15. package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
  16. package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
  17. package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
  18. package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
  19. package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
  20. package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
  21. package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
  22. package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
  23. package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
  24. package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
  25. package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
  26. package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
  27. package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
  28. package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
  29. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
  30. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
  31. package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
  32. package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
  33. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
  34. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
  35. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
  36. package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
  37. package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
  38. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
  39. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
  40. package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
  41. package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
  42. package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
  43. package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
  44. package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
  45. package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
  46. package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
  47. package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
  48. package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
  49. package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
  50. package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
  51. package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
  52. package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
  53. package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
  54. package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
  55. package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
  56. package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
  57. package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
  58. package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
  59. package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
  60. package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
  61. package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
  62. package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
  63. package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
  64. package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
  65. package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
  66. package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
  67. package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
  68. package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
  69. package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
  70. package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
  71. package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
  72. package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
  73. package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
  74. package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
  75. package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
  76. package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
  77. package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
  78. package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
  79. package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
  80. package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
  81. package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
  82. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
  83. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
  84. package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
  85. package/README.md +65 -0
  86. package/package.json +1 -1
  87. package/src/commands/AgentCommand.js +210 -0
  88. package/src/commands/AgentInitCommand.js +316 -0
  89. package/src/commands/PollWorkerCommand.js +11 -322
  90. package/src/config/config.js +44 -6
  91. package/src/index.js +55 -0
  92. package/src/services/LoggingService.js +35 -0
  93. package/src/services/PipelineApiService.js +7 -1
  94. package/src/services/PipelineJobRunner.js +389 -0
  95. 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();