@arela/uploader 1.1.3 → 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/document-type-shared.js +1 -1
- package/src/document-types/pedimento-completo.js +8 -1
- package/src/file-detection.js +9 -0
- package/src/index.js +55 -0
- package/src/scoring/scoring-engine.js +1 -1
- 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,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoring-based document classification engine (PROTOTYPE).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the "first-match-wins" selection in `document-type-shared.js`
|
|
5
|
+
* (`extractDocumentFields`) with "best-match": every applicable matcher is
|
|
6
|
+
* scored by the weight of the clues it satisfies, and the highest score wins.
|
|
7
|
+
* This removes the order-dependent registration that the current registry
|
|
8
|
+
* relies on (e.g. `factura_inter_agencia` MUST be evaluated before
|
|
9
|
+
* `facturas_comerciales`) — precedence now lives in clue weights / `required`
|
|
10
|
+
* / `negative` flags, not in array order.
|
|
11
|
+
*
|
|
12
|
+
* Matcher shape (see `matchers-seed.js`):
|
|
13
|
+
* {
|
|
14
|
+
* documentType, extensions[], minScore, priority,
|
|
15
|
+
* clues: [{ kind, pattern, flags?, weight=1, group?, required=false, negative=false }],
|
|
16
|
+
* qualify?: [{ <group>: minCount, ... }, ...], // OR-of-ANDs gate
|
|
17
|
+
* extractors, resolveType?, extractNumPedimento?, extractPedimentoYear?
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Two separable concerns:
|
|
21
|
+
* - QUALIFICATION (does this matcher apply at all?): `required`/`negative`
|
|
22
|
+
* clues plus an optional `qualify` rule set. `qualify` is a list of
|
|
23
|
+
* alternative rules (OR); a rule is a map of `group -> minimum matched
|
|
24
|
+
* clues` (AND across its entries). This expresses the grouped boolean gates
|
|
25
|
+
* of the original matchers, e.g. `(cfdi>=2) OR (invoice>=1 AND customs>=1)`.
|
|
26
|
+
* When `qualify` is absent, the gate falls back to `score >= minScore`.
|
|
27
|
+
* - RANKING (which qualifying matcher wins?): always the weighted sum of
|
|
28
|
+
* matched clues (`score`), tie-broken by fraction -> priority -> type.
|
|
29
|
+
*
|
|
30
|
+
* Selection only depends on clues/qualify. The winning matcher's `extractors` /
|
|
31
|
+
* `resolveType` / `extractNumPedimento` / `extractPedimentoYear` run AFTER
|
|
32
|
+
* selection with the same post-processing as `extractDocumentFields`, so a
|
|
33
|
+
* side-by-side comparison isolates the selection change.
|
|
34
|
+
*/
|
|
35
|
+
import path from 'path';
|
|
36
|
+
|
|
37
|
+
import { FieldResult } from '../document-type-shared.js';
|
|
38
|
+
|
|
39
|
+
// Compile cache for string patterns (from DB matchers): stable across every
|
|
40
|
+
// document in a run, so compile once instead of per document. Invalid patterns
|
|
41
|
+
// cache as null and are treated as a non-match (parity with the TS engine).
|
|
42
|
+
const regexCache = new Map();
|
|
43
|
+
const REGEX_CACHE_MAX = 5000;
|
|
44
|
+
|
|
45
|
+
function toRegExp(clue) {
|
|
46
|
+
if (clue.pattern instanceof RegExp) {
|
|
47
|
+
// Local-seed patterns are already compiled; reset lastIndex so a g/y flag
|
|
48
|
+
// can't make repeated .test() calls stateful across documents.
|
|
49
|
+
clue.pattern.lastIndex = 0;
|
|
50
|
+
return clue.pattern;
|
|
51
|
+
}
|
|
52
|
+
const safeFlags = (clue.flags ?? '').replace(/[gy]/g, '');
|
|
53
|
+
const key = `${safeFlags} ${clue.pattern}`;
|
|
54
|
+
let re = regexCache.get(key);
|
|
55
|
+
if (re === undefined) {
|
|
56
|
+
try {
|
|
57
|
+
re = new RegExp(clue.pattern, safeFlags);
|
|
58
|
+
} catch {
|
|
59
|
+
re = null;
|
|
60
|
+
}
|
|
61
|
+
if (regexCache.size >= REGEX_CACHE_MAX) regexCache.clear();
|
|
62
|
+
regexCache.set(key, re);
|
|
63
|
+
}
|
|
64
|
+
return re;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Bound the text a single regex runs on so an allowed (quadratic) pattern
|
|
68
|
+
// can't blow up on a megabyte-scale adversarial string. Real documents can
|
|
69
|
+
// legitimately exceed the cap (edocument XMLs embed multi-MB base64 PDFs with
|
|
70
|
+
// signature tags AFTER the blob), so instead of truncating we sample a HEAD +
|
|
71
|
+
// TAIL window — signatures live at the extremes, the blob in the middle.
|
|
72
|
+
// (Parity with the API TS engine.)
|
|
73
|
+
const MATCH_HEAD_CAP = 1_000_000;
|
|
74
|
+
const MATCH_TAIL_CAP = 262_144;
|
|
75
|
+
|
|
76
|
+
function clueTarget(clue, ctx) {
|
|
77
|
+
// FILENAME_REGEX tests the file name; every other kind tests the content.
|
|
78
|
+
const raw =
|
|
79
|
+
clue.kind === 'FILENAME_REGEX' ? (ctx.fileName ?? '') : (ctx.source ?? '');
|
|
80
|
+
if (raw.length <= MATCH_HEAD_CAP + MATCH_TAIL_CAP) return raw;
|
|
81
|
+
return `${raw.slice(0, MATCH_HEAD_CAP)}\n${raw.slice(-MATCH_TAIL_CAP)}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Score a single matcher against a document context.
|
|
86
|
+
* @returns {null} when the matcher does not apply (extension mismatch),
|
|
87
|
+
* `{ disqualified: true, reason }` when a `required`/`negative` clue
|
|
88
|
+
* rules it out, otherwise a scored result object.
|
|
89
|
+
*/
|
|
90
|
+
export function scoreMatcher(matcher, ctx) {
|
|
91
|
+
const ext = (ctx.extension ?? '').toLowerCase();
|
|
92
|
+
if (
|
|
93
|
+
Array.isArray(matcher.extensions) &&
|
|
94
|
+
matcher.extensions.length > 0 &&
|
|
95
|
+
ext &&
|
|
96
|
+
!matcher.extensions.includes(ext)
|
|
97
|
+
) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let matchedWeight = 0;
|
|
102
|
+
let totalWeight = 0;
|
|
103
|
+
const matchedClues = [];
|
|
104
|
+
const groupCounts = {};
|
|
105
|
+
|
|
106
|
+
for (const clue of matcher.clues ?? []) {
|
|
107
|
+
const weight = clue.weight ?? 1;
|
|
108
|
+
const re = toRegExp(clue);
|
|
109
|
+
const hit = re ? re.test(clueTarget(clue, ctx)) : false;
|
|
110
|
+
|
|
111
|
+
if (clue.negative) {
|
|
112
|
+
if (hit) {
|
|
113
|
+
return {
|
|
114
|
+
documentType: matcher.documentType,
|
|
115
|
+
disqualified: true,
|
|
116
|
+
reason: `negative:${clue.pattern}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (clue.required && !hit) {
|
|
123
|
+
return {
|
|
124
|
+
documentType: matcher.documentType,
|
|
125
|
+
disqualified: true,
|
|
126
|
+
reason: `required-missing:${clue.pattern}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
totalWeight += weight;
|
|
131
|
+
if (hit) {
|
|
132
|
+
matchedWeight += weight;
|
|
133
|
+
matchedClues.push(clue);
|
|
134
|
+
if (clue.group) {
|
|
135
|
+
groupCounts[clue.group] = (groupCounts[clue.group] ?? 0) + 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Qualification gate: `qualify` rules (OR-of-ANDs over group counts) when
|
|
141
|
+
// present, otherwise the weighted-score threshold.
|
|
142
|
+
const passed = Array.isArray(matcher.qualify)
|
|
143
|
+
? matcher.qualify.some((rule) =>
|
|
144
|
+
Object.entries(rule).every(
|
|
145
|
+
([group, min]) => (groupCounts[group] ?? 0) >= min,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
: matchedWeight >= (matcher.minScore ?? 1);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
documentType: matcher.documentType,
|
|
152
|
+
matcher,
|
|
153
|
+
disqualified: false,
|
|
154
|
+
score: matchedWeight,
|
|
155
|
+
totalWeight,
|
|
156
|
+
fraction: totalWeight > 0 ? matchedWeight / totalWeight : 0,
|
|
157
|
+
priority: matcher.priority ?? 0,
|
|
158
|
+
passed,
|
|
159
|
+
matchedClues,
|
|
160
|
+
groupCounts,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* All qualifying candidates, sorted best-first.
|
|
166
|
+
* Order: score desc → fraction desc → priority desc → documentType (stable).
|
|
167
|
+
*/
|
|
168
|
+
export function scoreAll(matchers, ctx) {
|
|
169
|
+
const candidates = [];
|
|
170
|
+
for (const matcher of matchers) {
|
|
171
|
+
const result = scoreMatcher(matcher, ctx);
|
|
172
|
+
if (!result || result.disqualified || !result.passed) continue;
|
|
173
|
+
candidates.push(result);
|
|
174
|
+
}
|
|
175
|
+
candidates.sort(
|
|
176
|
+
(a, b) =>
|
|
177
|
+
b.score - a.score ||
|
|
178
|
+
b.fraction - a.fraction ||
|
|
179
|
+
b.priority - a.priority ||
|
|
180
|
+
String(a.documentType).localeCompare(String(b.documentType)),
|
|
181
|
+
);
|
|
182
|
+
return candidates;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function selectBestMatch(matchers, ctx) {
|
|
186
|
+
return scoreAll(matchers, ctx)[0] ?? null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Full classification: pick the best matcher, then run ITS extractors /
|
|
191
|
+
* resolveType / pedimento helpers. Post-selection logic mirrors
|
|
192
|
+
* `extractDocumentFields` so a comparison isolates the selection change.
|
|
193
|
+
*
|
|
194
|
+
* @returns {{ detectedType, fields, detectedPedimento, detectedPedimentoYear,
|
|
195
|
+
* winner, candidates }}
|
|
196
|
+
*/
|
|
197
|
+
export function classifyDocument(matchers, { source, extension, filePath }) {
|
|
198
|
+
const ctx = {
|
|
199
|
+
source,
|
|
200
|
+
extension,
|
|
201
|
+
fileName: filePath ? path.basename(filePath) : '',
|
|
202
|
+
};
|
|
203
|
+
const candidates = scoreAll(matchers, ctx);
|
|
204
|
+
const winner = candidates[0] ?? null;
|
|
205
|
+
|
|
206
|
+
if (!winner) {
|
|
207
|
+
return {
|
|
208
|
+
detectedType: null,
|
|
209
|
+
fields: [],
|
|
210
|
+
detectedPedimento: null,
|
|
211
|
+
detectedPedimentoYear: null,
|
|
212
|
+
winner: null,
|
|
213
|
+
candidates,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const def = winner.matcher;
|
|
218
|
+
const fields = [];
|
|
219
|
+
for (const extractor of def.extractors ?? []) {
|
|
220
|
+
try {
|
|
221
|
+
fields.push(extractor.extract(source));
|
|
222
|
+
} catch {
|
|
223
|
+
fields.push(new FieldResult(extractor.field, false, null));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const resolvedType = def.resolveType
|
|
228
|
+
? def.resolveType(fields, filePath)
|
|
229
|
+
: def.documentType;
|
|
230
|
+
const pedimento = def.extractNumPedimento
|
|
231
|
+
? def.extractNumPedimento(source, fields, filePath)
|
|
232
|
+
: null;
|
|
233
|
+
const year = def.extractPedimentoYear
|
|
234
|
+
? def.extractPedimentoYear(source, fields, filePath)
|
|
235
|
+
: null;
|
|
236
|
+
|
|
237
|
+
// Backfill numPedimento as a field (same as extractDocumentFields) so
|
|
238
|
+
// downstream consumers (composeArelaPath) see a consistent shape.
|
|
239
|
+
if (pedimento && !fields.some((f) => f.name === 'numPedimento')) {
|
|
240
|
+
fields.push(new FieldResult('numPedimento', true, pedimento));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
detectedType: resolvedType,
|
|
245
|
+
fields,
|
|
246
|
+
detectedPedimento: pedimento,
|
|
247
|
+
detectedPedimentoYear: year,
|
|
248
|
+
winner,
|
|
249
|
+
candidates,
|
|
250
|
+
};
|
|
251
|
+
}
|