@kansei-link/bantou 0.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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/data/exclusion-rules/README.md +104 -0
  4. package/data/exclusion-rules/jp-tax-baseline-v1.json +185 -0
  5. package/data/exclusion-rules-schema.json +109 -0
  6. package/data/keyword-dict/README.md +91 -0
  7. package/data/keyword-dict/jp-tax-baseline-v1.json +398 -0
  8. package/data/keyword-dict-schema.json +117 -0
  9. package/data/tax-rules/jp-tax-rules-v1.json +170 -0
  10. package/dist/adapters/csv-parser.d.ts +11 -0
  11. package/dist/adapters/csv-parser.js +133 -0
  12. package/dist/adapters/freee-csv-adapter.d.ts +14 -0
  13. package/dist/adapters/freee-csv-adapter.js +67 -0
  14. package/dist/adapters/generic-adapter.d.ts +20 -0
  15. package/dist/adapters/generic-adapter.js +73 -0
  16. package/dist/adapters/index.d.ts +23 -0
  17. package/dist/adapters/index.js +386 -0
  18. package/dist/adapters/types.d.ts +111 -0
  19. package/dist/adapters/types.js +9 -0
  20. package/dist/adapters/yayoi-adapter.d.ts +46 -0
  21. package/dist/adapters/yayoi-adapter.js +181 -0
  22. package/dist/bin/freee-doctor.d.ts +3 -0
  23. package/dist/bin/freee-doctor.js +15 -0
  24. package/dist/classifier/claude-classifier.d.ts +24 -0
  25. package/dist/classifier/claude-classifier.js +154 -0
  26. package/dist/classifier/keyword-classifier.d.ts +22 -0
  27. package/dist/classifier/keyword-classifier.js +124 -0
  28. package/dist/classifier/keyword-match.d.ts +21 -0
  29. package/dist/classifier/keyword-match.js +57 -0
  30. package/dist/classifier/normalize.d.ts +3 -0
  31. package/dist/classifier/normalize.js +27 -0
  32. package/dist/classifier/two-stage-classifier.d.ts +21 -0
  33. package/dist/classifier/two-stage-classifier.js +51 -0
  34. package/dist/classifier/types.d.ts +31 -0
  35. package/dist/classifier/types.js +3 -0
  36. package/dist/connectors/freee.d.ts +115 -0
  37. package/dist/connectors/freee.js +177 -0
  38. package/dist/exclusion/exclusion-checker.d.ts +10 -0
  39. package/dist/exclusion/exclusion-checker.js +162 -0
  40. package/dist/freee-doctor.d.ts +26 -0
  41. package/dist/freee-doctor.js +82 -0
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +656 -0
  44. package/dist/memory/cockpit-memory.d.ts +73 -0
  45. package/dist/memory/cockpit-memory.js +473 -0
  46. package/dist/memory/types.d.ts +114 -0
  47. package/dist/memory/types.js +11 -0
  48. package/dist/pipeline/confidence-router.d.ts +38 -0
  49. package/dist/pipeline/confidence-router.js +129 -0
  50. package/dist/pipeline/nightly-pipeline.d.ts +44 -0
  51. package/dist/pipeline/nightly-pipeline.js +497 -0
  52. package/dist/pipeline/types.d.ts +84 -0
  53. package/dist/pipeline/types.js +12 -0
  54. package/dist/reports/monthly-report.d.ts +64 -0
  55. package/dist/reports/monthly-report.js +230 -0
  56. package/dist/secrets.d.ts +14 -0
  57. package/dist/secrets.js +86 -0
  58. package/dist/tax-rules/tax-rule-engine.d.ts +103 -0
  59. package/dist/tax-rules/tax-rule-engine.js +449 -0
  60. package/dist/tax-rules/types.d.ts +103 -0
  61. package/dist/tax-rules/types.js +7 -0
  62. package/package.json +74 -0
@@ -0,0 +1,497 @@
1
+ // Nightly batch pipeline orchestrator.
2
+ //
3
+ // Faithfully reproduces the 60-company nightly batch pattern:
4
+ //
5
+ // [21:00] Anthropic Routine → "今日の N 社 自動仕訳 batch を実行して"
6
+ // [21:00] list_companies() → N company IDs
7
+ // [21:01] Per company (concurrency-limited parallel):
8
+ // fetch_unprocessed → exclusion → classify → confidence route → (register)
9
+ // [21:50] Summary → Slack DM
10
+ //
11
+ // Key design decisions:
12
+ //
13
+ // 1. Concurrency limit (default 3) — freee API rate limit = 3600 req/h (= 60/min).
14
+ // With ~10 deals/company × ~2 API calls/deal, 3 concurrent companies stays safe.
15
+ //
16
+ // 2. Promise.allSettled per batch — one company failure doesn't kill the run.
17
+ // Error is captured in CompanyBatchResult, pipeline continues.
18
+ //
19
+ // 3. Memo extraction fallback chain — freee /deals list API quirk:
20
+ // memo || description || details[0].description || ref_number || ''
21
+ //
22
+ // 4. Partner detection — fetches existing partners per company, checks if
23
+ // deal.partner_name is in master. New partners → human_review.
24
+ //
25
+ // 5. Dry-run by default — Phase 1.A doesn't write back to freee.
26
+ // Write-back (freee.register_journal) deferred to Phase 1.B.
27
+ import { ConfidenceRouter } from './confidence-router.js';
28
+ const DEFAULT_CONFIG = {
29
+ dry_run: true,
30
+ concurrency: 3,
31
+ deals_per_company: 100,
32
+ };
33
+ // ============================================================
34
+ // Pipeline
35
+ // ============================================================
36
+ export class NightlyPipeline {
37
+ connector;
38
+ classifier;
39
+ exclusion;
40
+ router;
41
+ memory;
42
+ config;
43
+ constructor(connector, classifier, exclusion, config = {}, memory) {
44
+ this.connector = connector;
45
+ this.classifier = classifier;
46
+ this.exclusion = exclusion;
47
+ this.router = new ConfidenceRouter();
48
+ this.memory = memory || null;
49
+ this.config = { ...DEFAULT_CONFIG, ...config };
50
+ }
51
+ /**
52
+ * Run the full Full nightly batch.
53
+ *
54
+ * Pipeline:
55
+ * 1. List all accessible companies (or use override list)
56
+ * 2. For each company (concurrency-limited parallel):
57
+ * a. Fetch unprocessed deals (status = 'unsettled')
58
+ * b. Fetch existing partners (for new-partner detection)
59
+ * c. For each deal: exclusion → classify → route
60
+ * d. Aggregate per-company results
61
+ * 3. Aggregate all company results
62
+ * 4. Generate Batch summary (Slack-ready format)
63
+ */
64
+ async run() {
65
+ const startedAt = new Date().toISOString();
66
+ // ── Step 1: Determine target companies ──────────────────────
67
+ let companies;
68
+ if (this.config.company_ids && this.config.company_ids.length > 0) {
69
+ // Explicit company list provided — create minimal objects
70
+ companies = this.config.company_ids.map(id => ({
71
+ id,
72
+ display_name: `Company ${id}`,
73
+ tax_at_source_calc_type: 0,
74
+ contact_name: '',
75
+ }));
76
+ }
77
+ else {
78
+ // Discover all accessible companies from the token
79
+ try {
80
+ companies = await this.connector.listCompanies();
81
+ }
82
+ catch (err) {
83
+ // Fallback: use default company from secrets (= single-company mode)
84
+ companies = [{
85
+ id: this.connector.companyId,
86
+ display_name: this.connector.companyName || `Company ${this.connector.companyId}`,
87
+ tax_at_source_calc_type: 0,
88
+ contact_name: '',
89
+ }];
90
+ }
91
+ }
92
+ // ── Step 2: Process companies with concurrency control ──────
93
+ const companyResults = [];
94
+ const batches = chunkArray(companies, this.config.concurrency);
95
+ for (const batch of batches) {
96
+ const settled = await Promise.allSettled(batch.map(company => this.processCompany(company)));
97
+ for (let i = 0; i < settled.length; i++) {
98
+ const result = settled[i];
99
+ if (result.status === 'fulfilled') {
100
+ companyResults.push(result.value);
101
+ }
102
+ else {
103
+ // Entire company processing failed — capture error, continue
104
+ companyResults.push({
105
+ company_id: batch[i].id,
106
+ company_name: batch[i].display_name,
107
+ total_deals: 0,
108
+ processing_time_ms: 0,
109
+ ok: false,
110
+ error: result.reason?.message || String(result.reason),
111
+ summary: emptySummary(),
112
+ confidence_breakdown: { high: 0, medium: 0, low: 0, none: 0 },
113
+ review_queue: [],
114
+ sample: [],
115
+ });
116
+ }
117
+ }
118
+ }
119
+ // ── Step 3: Aggregate ──────────────────────────────────────
120
+ const aggregate = aggregateResults(companyResults);
121
+ const totalDeals = companyResults.reduce((sum, c) => sum + c.total_deals, 0);
122
+ const finishedAt = new Date().toISOString();
123
+ // ── Step 4: Build Slack-ready summary ──────────────────────
124
+ const memoryStats = this.memory?.getStats() || null;
125
+ const slackSummary = buildSlackSummary(companyResults, aggregate, startedAt, finishedAt, this.config.dry_run, memoryStats);
126
+ // ── Assemble final result ──────────────────────────────────
127
+ const s1 = this.classifier.getStage1();
128
+ const s2 = this.classifier.getStage2();
129
+ return {
130
+ ok: companyResults.some(c => c.ok), // OK if at least one company succeeded
131
+ dry_run: this.config.dry_run,
132
+ started_at: startedAt,
133
+ finished_at: finishedAt,
134
+ total_companies: companies.length,
135
+ total_deals: totalDeals,
136
+ aggregate,
137
+ companies: companyResults,
138
+ classifier: {
139
+ stage1_version: s1.getVersion(),
140
+ stage1_keywords: s1.getKeywordsCount(),
141
+ stage1_categories: s1.getCategoriesCount(),
142
+ stage2_enabled: this.classifier.hasStage2(),
143
+ stage2_model: s2?.getModel() || null,
144
+ },
145
+ exclusion_version: this.exclusion.getVersion(),
146
+ slack_summary: slackSummary,
147
+ note: this.config.dry_run
148
+ ? 'Phase 1.A dry-run — write-back to freee pending Phase 1.B.'
149
+ : 'Live mode — transactions registered to freee.',
150
+ };
151
+ }
152
+ // ============================================================
153
+ // Per-company processing
154
+ // ============================================================
155
+ async processCompany(company) {
156
+ const t0 = Date.now();
157
+ const companyId = company.id;
158
+ // Fetch unprocessed deals (Step 3: status='unsettled' filter)
159
+ const deals = await this.connector.listDeals({
160
+ company_id: companyId,
161
+ status: 'unsettled',
162
+ start_issue_date: this.config.period_start,
163
+ end_issue_date: this.config.period_end,
164
+ limit: this.config.deals_per_company,
165
+ });
166
+ // Fetch existing partners for new-partner detection
167
+ // (Best-effort: if this fails, treat all partners as existing = safer)
168
+ let knownPartners;
169
+ try {
170
+ const partners = await this.connector.listPartners({
171
+ company_id: companyId,
172
+ limit: 500,
173
+ });
174
+ knownPartners = new Set(partners.map(p => p.name));
175
+ }
176
+ catch {
177
+ knownPartners = new Set();
178
+ }
179
+ // Process each deal through the pipeline
180
+ const processed = [];
181
+ const summary = emptySummary();
182
+ const confidence = { high: 0, medium: 0, low: 0, none: 0 };
183
+ for (const deal of deals) {
184
+ try {
185
+ const pt = await this.processDeal(deal, companyId, knownPartners);
186
+ processed.push(pt);
187
+ // Update summary counters
188
+ if (pt.excluded) {
189
+ summary.excluded++;
190
+ }
191
+ else if (!pt.classified) {
192
+ summary.unclassified++;
193
+ }
194
+ else {
195
+ if (pt.stage === 1)
196
+ summary.classified_stage1++;
197
+ else if (pt.stage === 2)
198
+ summary.classified_stage2++;
199
+ }
200
+ // Confidence counter (including excluded = 'none')
201
+ const conf = pt.confidence || 'none';
202
+ if (conf in confidence)
203
+ confidence[conf]++;
204
+ // Routing action counter
205
+ switch (pt.routing.action) {
206
+ case 'auto_register':
207
+ summary.auto_registered++;
208
+ break;
209
+ case 'auto_register_with_log':
210
+ summary.auto_registered_with_log++;
211
+ break;
212
+ case 'human_review':
213
+ summary.human_review++;
214
+ break;
215
+ }
216
+ }
217
+ catch (err) {
218
+ summary.errors++;
219
+ processed.push(makeErrorTransaction(deal, companyId, err?.message || String(err)));
220
+ }
221
+ }
222
+ // Build review queue: all items that need human attention
223
+ const reviewQueue = processed.filter(p => p.routing.action === 'human_review'
224
+ || p.routing.action === 'auto_register_with_log');
225
+ // Sample: first 5 transactions for summary display
226
+ const sample = processed.slice(0, 5);
227
+ // Save memory after processing each company (batch-level persistence)
228
+ if (this.memory) {
229
+ this.memory.save();
230
+ }
231
+ return {
232
+ company_id: companyId,
233
+ company_name: company.display_name,
234
+ total_deals: deals.length,
235
+ processing_time_ms: Date.now() - t0,
236
+ ok: summary.errors === 0,
237
+ summary,
238
+ confidence_breakdown: confidence,
239
+ review_queue: reviewQueue,
240
+ sample,
241
+ };
242
+ }
243
+ // ============================================================
244
+ // Per-deal processing
245
+ // ============================================================
246
+ async processDeal(deal, companyId, knownPartners) {
247
+ // Extract memo (freee API quirk: details[0].description is the real memo)
248
+ const memo = deal.memo
249
+ || deal.description
250
+ || (deal.details?.[0]?.description)
251
+ || deal.ref_number
252
+ || '';
253
+ const tx = {
254
+ amount: deal.amount,
255
+ memo,
256
+ date: deal.issue_date,
257
+ partner_name: deal.partner_name,
258
+ company_id: companyId,
259
+ };
260
+ // ── Stage 0: Exclusion check ──
261
+ const exc = this.exclusion.check(tx);
262
+ if (exc.excluded) {
263
+ const routing = this.router.route(exc, null, {
264
+ amount: deal.amount,
265
+ partner_name: deal.partner_name,
266
+ is_new_partner: false,
267
+ date: deal.issue_date,
268
+ });
269
+ return {
270
+ deal_id: deal.id,
271
+ company_id: companyId,
272
+ issue_date: deal.issue_date,
273
+ amount: deal.amount,
274
+ memo,
275
+ partner_name: deal.partner_name,
276
+ excluded: true,
277
+ exclusion_rule: exc.rule_id,
278
+ exclusion_reason: exc.reason,
279
+ classified: false,
280
+ confidence: 'none',
281
+ routing,
282
+ };
283
+ }
284
+ // ── Memory recall: check past patterns before classification ──
285
+ //
286
+ // Rule: "過去パターン一致 → AI 処理 OK"
287
+ // If memory finds a correction or pattern, skip classification entirely.
288
+ if (this.memory) {
289
+ const recall = this.memory.recallPattern(tx);
290
+ if (recall.found && recall.source === 'correction' && recall.correction) {
291
+ // Correction hit — use corrected category, always high confidence
292
+ const isNewPartner = Boolean(deal.partner_name && deal.partner_name.trim() !== ''
293
+ && !knownPartners.has(deal.partner_name));
294
+ const routing = this.router.route({ excluded: false }, {
295
+ classified: true,
296
+ category_id: recall.correction.to_category_id,
297
+ category_name_ja: recall.correction.to_category_name_ja,
298
+ freee_account_code: recall.correction.to_freee_account_code,
299
+ tax_code: recall.correction.to_tax_code,
300
+ confidence: 'high',
301
+ match_reason: recall.reason,
302
+ classifier_version: 'memory-correction',
303
+ }, { amount: deal.amount, partner_name: deal.partner_name, is_new_partner: isNewPartner, date: deal.issue_date });
304
+ return {
305
+ deal_id: deal.id,
306
+ company_id: companyId,
307
+ issue_date: deal.issue_date,
308
+ amount: deal.amount,
309
+ memo,
310
+ partner_name: deal.partner_name,
311
+ excluded: false,
312
+ classified: true,
313
+ category_id: recall.correction.to_category_id,
314
+ category_name_ja: recall.correction.to_category_name_ja,
315
+ confidence: 'high',
316
+ memory_source: 'correction',
317
+ memory_pattern_key: recall.correction.partner_key,
318
+ routing,
319
+ };
320
+ }
321
+ if (recall.found && recall.source === 'pattern' && recall.pattern) {
322
+ // Pattern hit — use remembered classification
323
+ const isNewPartner = Boolean(deal.partner_name && deal.partner_name.trim() !== ''
324
+ && !knownPartners.has(deal.partner_name));
325
+ const routing = this.router.route({ excluded: false }, {
326
+ classified: true,
327
+ category_id: recall.pattern.category_id,
328
+ category_name_ja: recall.pattern.category_name_ja,
329
+ freee_account_code: recall.pattern.freee_account_code,
330
+ tax_code: recall.pattern.tax_code,
331
+ confidence: recall.confidence,
332
+ match_reason: recall.reason,
333
+ classifier_version: 'memory-pattern',
334
+ }, { amount: deal.amount, partner_name: deal.partner_name, is_new_partner: isNewPartner, date: deal.issue_date });
335
+ // Update pattern match count
336
+ recall.pattern.match_count++;
337
+ recall.pattern.last_seen = new Date().toISOString().slice(0, 10);
338
+ return {
339
+ deal_id: deal.id,
340
+ company_id: companyId,
341
+ issue_date: deal.issue_date,
342
+ amount: deal.amount,
343
+ memo,
344
+ partner_name: deal.partner_name,
345
+ excluded: false,
346
+ classified: true,
347
+ category_id: recall.pattern.category_id,
348
+ category_name_ja: recall.pattern.category_name_ja,
349
+ confidence: recall.confidence,
350
+ memory_source: 'pattern',
351
+ memory_pattern_key: recall.pattern.partner_key,
352
+ memory_match_count: recall.pattern.match_count,
353
+ routing,
354
+ };
355
+ }
356
+ }
357
+ // ── Stage 1 + 2: Two-stage classification (memory miss) ──
358
+ const cls = await this.classifier.classify(tx);
359
+ // ── New partner detection ──
360
+ const isNewPartner = Boolean(deal.partner_name && deal.partner_name.trim() !== ''
361
+ && !knownPartners.has(deal.partner_name));
362
+ // ── Confidence routing ──
363
+ const routingContext = {
364
+ amount: deal.amount,
365
+ partner_name: deal.partner_name,
366
+ is_new_partner: isNewPartner,
367
+ date: deal.issue_date,
368
+ };
369
+ const routing = this.router.route({ excluded: false }, cls, routingContext);
370
+ // ── Remember classification for future recall ──
371
+ //
372
+ // Only remember successful classifications (auto_register / auto_register_with_log).
373
+ // Don't remember human_review items — those need human judgment first.
374
+ if (this.memory && cls.classified && (routing.action === 'auto_register' || routing.action === 'auto_register_with_log')) {
375
+ this.memory.rememberClassification(tx, cls.category_id, cls.category_name_ja, cls.confidence, cls.stage === 1 ? 'keyword' : 'claude', cls.freee_account_code, typeof cls.tax_code === 'number' ? cls.tax_code : undefined);
376
+ }
377
+ return {
378
+ deal_id: deal.id,
379
+ company_id: companyId,
380
+ issue_date: deal.issue_date,
381
+ amount: deal.amount,
382
+ memo,
383
+ partner_name: deal.partner_name,
384
+ excluded: false,
385
+ classified: cls.classified,
386
+ stage: cls.stage === 'unclassified' ? undefined : cls.stage,
387
+ category_id: cls.category_id,
388
+ category_name_ja: cls.category_name_ja,
389
+ confidence: cls.confidence,
390
+ matched_keyword: cls.matched_keyword,
391
+ routing,
392
+ };
393
+ }
394
+ }
395
+ // ============================================================
396
+ // Pure helper functions (no state)
397
+ // ============================================================
398
+ function emptySummary() {
399
+ return {
400
+ auto_registered: 0,
401
+ auto_registered_with_log: 0,
402
+ human_review: 0,
403
+ excluded: 0,
404
+ classified_stage1: 0,
405
+ classified_stage2: 0,
406
+ unclassified: 0,
407
+ errors: 0,
408
+ };
409
+ }
410
+ function aggregateResults(companies) {
411
+ const agg = emptySummary();
412
+ for (const c of companies) {
413
+ agg.auto_registered += c.summary.auto_registered;
414
+ agg.auto_registered_with_log += c.summary.auto_registered_with_log;
415
+ agg.human_review += c.summary.human_review;
416
+ agg.excluded += c.summary.excluded;
417
+ agg.classified_stage1 += c.summary.classified_stage1;
418
+ agg.classified_stage2 += c.summary.classified_stage2;
419
+ agg.unclassified += c.summary.unclassified;
420
+ agg.errors += c.summary.errors;
421
+ }
422
+ return agg;
423
+ }
424
+ /**
425
+ * Build Batch summary (Slack-ready format).
426
+ *
427
+ * Target format (practitioner-proven Slack notification):
428
+ * Cockpit nightly run 完了 (21:50 JST):
429
+ * - 全 60 社 / 計 750 件 処理
430
+ * - 自動 register: 712 件 (high/medium confidence)
431
+ * - 確認待ち: 38 件
432
+ * - エラー: 0 件
433
+ * - 処理時間: 50 分
434
+ */
435
+ function buildSlackSummary(companies, aggregate, startedAt, finishedAt, dryRun, memoryStats) {
436
+ const totalDeals = companies.reduce((s, c) => s + c.total_deals, 0);
437
+ const autoTotal = aggregate.auto_registered + aggregate.auto_registered_with_log;
438
+ const elapsedMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime();
439
+ const elapsedSec = (elapsedMs / 1000).toFixed(1);
440
+ const elapsedMin = (elapsedMs / 60000).toFixed(1);
441
+ const elapsedDisplay = elapsedMs >= 60000 ? `${elapsedMin} 分` : `${elapsedSec} 秒`;
442
+ // JST timestamp for display
443
+ const finishedJST = new Date(new Date(finishedAt).getTime() + 9 * 3600 * 1000)
444
+ .toISOString().slice(11, 16);
445
+ const lines = [
446
+ `Cockpit nightly run 完了 (${finishedJST} JST):`,
447
+ `- 全 ${companies.length} 社 / 計 ${totalDeals} 件 処理`,
448
+ `- 自動 register: ${autoTotal} 件 (high: ${aggregate.auto_registered}, medium+log: ${aggregate.auto_registered_with_log})`,
449
+ `- 確認待ち: ${aggregate.human_review} 件 (low + exclusion + 高額 + 新規取引先)`,
450
+ `- 除外: ${aggregate.excluded} 件 (Stage 0 exclusion)`,
451
+ `- 分類: Stage 1 = ${aggregate.classified_stage1} 件, Stage 2 = ${aggregate.classified_stage2} 件, 未分類 = ${aggregate.unclassified} 件`,
452
+ `- エラー: ${aggregate.errors} 件`,
453
+ `- 処理時間: ${elapsedDisplay}`,
454
+ ];
455
+ // Memory stats (if available)
456
+ if (memoryStats) {
457
+ const memTotal = memoryStats.pattern_hits + memoryStats.correction_hits + memoryStats.cache_misses;
458
+ if (memTotal > 0) {
459
+ lines.push(`- Memory: pattern hit ${memoryStats.pattern_hits} / correction hit ${memoryStats.correction_hits} / miss ${memoryStats.cache_misses}`);
460
+ }
461
+ }
462
+ if (dryRun) {
463
+ lines.push('- [DRY RUN] freee への書き込みは行っていません');
464
+ }
465
+ // Report failed companies
466
+ const failed = companies.filter(c => !c.ok);
467
+ if (failed.length > 0) {
468
+ lines.push(`- 失敗: ${failed.map(c => `${c.company_name} (${c.error?.slice(0, 60) || 'unknown'})`).join(', ')}`);
469
+ }
470
+ return lines.join('\n');
471
+ }
472
+ function makeErrorTransaction(deal, companyId, error) {
473
+ return {
474
+ deal_id: deal.id,
475
+ company_id: companyId,
476
+ issue_date: deal.issue_date,
477
+ amount: deal.amount,
478
+ memo: deal.memo || deal.description || '',
479
+ partner_name: deal.partner_name,
480
+ excluded: false,
481
+ classified: false,
482
+ confidence: 'none',
483
+ routing: {
484
+ action: 'human_review',
485
+ reasons: [`Pipeline error: ${error}`],
486
+ flags: [],
487
+ },
488
+ };
489
+ }
490
+ function chunkArray(arr, size) {
491
+ const chunks = [];
492
+ for (let i = 0; i < arr.length; i += size) {
493
+ chunks.push(arr.slice(i, i + size));
494
+ }
495
+ return chunks;
496
+ }
497
+ //# sourceMappingURL=nightly-pipeline.js.map
@@ -0,0 +1,84 @@
1
+ export type RoutingAction = 'auto_register' | 'auto_register_with_log' | 'human_review';
2
+ export type RoutingFlag = 'high_amount' | 'new_partner' | 'low_confidence' | 'medium_confidence' | 'excluded' | 'unclassified' | 'monthly_close_period';
3
+ export interface RoutingDecision {
4
+ action: RoutingAction;
5
+ reasons: string[];
6
+ flags: RoutingFlag[];
7
+ }
8
+ export interface ProcessedTransaction {
9
+ deal_id: number;
10
+ company_id: number;
11
+ issue_date: string;
12
+ amount: number;
13
+ memo: string;
14
+ partner_name?: string;
15
+ excluded: boolean;
16
+ exclusion_rule?: string;
17
+ exclusion_reason?: string;
18
+ classified: boolean;
19
+ stage?: 1 | 2;
20
+ category_id?: string;
21
+ category_name_ja?: string;
22
+ confidence?: 'high' | 'medium' | 'low' | 'none';
23
+ matched_keyword?: string;
24
+ memory_source?: 'pattern' | 'correction';
25
+ memory_pattern_key?: string;
26
+ memory_match_count?: number;
27
+ routing: RoutingDecision;
28
+ }
29
+ export interface CompanyBatchResult {
30
+ company_id: number;
31
+ company_name: string;
32
+ total_deals: number;
33
+ processing_time_ms: number;
34
+ ok: boolean;
35
+ error?: string;
36
+ summary: {
37
+ auto_registered: number;
38
+ auto_registered_with_log: number;
39
+ human_review: number;
40
+ excluded: number;
41
+ classified_stage1: number;
42
+ classified_stage2: number;
43
+ unclassified: number;
44
+ errors: number;
45
+ };
46
+ confidence_breakdown: {
47
+ high: number;
48
+ medium: number;
49
+ low: number;
50
+ none: number;
51
+ };
52
+ review_queue: ProcessedTransaction[];
53
+ sample: ProcessedTransaction[];
54
+ }
55
+ export interface NightlyRunResult {
56
+ ok: boolean;
57
+ dry_run: boolean;
58
+ started_at: string;
59
+ finished_at: string;
60
+ total_companies: number;
61
+ total_deals: number;
62
+ aggregate: {
63
+ auto_registered: number;
64
+ auto_registered_with_log: number;
65
+ human_review: number;
66
+ excluded: number;
67
+ classified_stage1: number;
68
+ classified_stage2: number;
69
+ unclassified: number;
70
+ errors: number;
71
+ };
72
+ companies: CompanyBatchResult[];
73
+ classifier: {
74
+ stage1_version: string;
75
+ stage1_keywords: number;
76
+ stage1_categories: number;
77
+ stage2_enabled: boolean;
78
+ stage2_model: string | null;
79
+ };
80
+ exclusion_version: string;
81
+ slack_summary: string;
82
+ note: string;
83
+ }
84
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,12 @@
1
+ // Pipeline result types for the nightly batch processor.
2
+ //
3
+ // These types represent the full lifecycle of a transaction through the pipeline:
4
+ // Stage 0 (exclusion) → Stage 1 (keyword) → Stage 2 (Claude) → Confidence routing → Action
5
+ //
6
+ // Routing decisions follow the CLAUDE.md business manual:
7
+ // high confidence + no flags → auto_register
8
+ // medium confidence → auto_register_with_log (= 自動仕訳 + 確認待ちmirror)
9
+ // low confidence → human_review only
10
+ // 100万円超 / 新規取引先 / 月次決算期間 → human_review regardless
11
+ export {};
12
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,64 @@
1
+ import { Transaction } from '../classifier/types.js';
2
+ import { TwoStageClassifier } from '../classifier/two-stage-classifier.js';
3
+ import { ExclusionChecker } from '../exclusion/exclusion-checker.js';
4
+ import { ConfidenceRouter } from '../pipeline/confidence-router.js';
5
+ /** Processed transaction with classification for reporting. */
6
+ interface ReportTransaction {
7
+ transaction: Transaction;
8
+ category_id?: string;
9
+ category_name_ja?: string;
10
+ confidence?: string;
11
+ excluded: boolean;
12
+ exclusion_rule?: string;
13
+ action: string;
14
+ flags: string[];
15
+ }
16
+ /** Category aggregate for the report. */
17
+ interface CategoryAggregate {
18
+ category_id: string;
19
+ category_name_ja: string;
20
+ count: number;
21
+ total_amount: number;
22
+ transactions: ReportTransaction[];
23
+ }
24
+ /** Anomaly detected in the data. */
25
+ interface Anomaly {
26
+ type: 'high_amount' | 'unusual_category' | 'new_partner' | 'frequency_spike';
27
+ severity: 'warning' | 'critical';
28
+ description: string;
29
+ details: string;
30
+ }
31
+ /** Monthly report output. */
32
+ export interface MonthlyReportResult {
33
+ ok: boolean;
34
+ company_name?: string;
35
+ month: string;
36
+ format: 'markdown' | 'json';
37
+ total_transactions: number;
38
+ total_expense: number;
39
+ total_income: number;
40
+ classification_rate: string;
41
+ auto_register_rate: string;
42
+ categories: CategoryAggregate[];
43
+ anomalies: Anomaly[];
44
+ review_items: ReportTransaction[];
45
+ markdown?: string;
46
+ }
47
+ /**
48
+ * Generate a monthly report from raw transactions.
49
+ *
50
+ * @param transactions - Array of raw transactions (from freee API, CSV import, etc.)
51
+ * @param classifier - TwoStageClassifier instance.
52
+ * @param exclusion - ExclusionChecker instance.
53
+ * @param router - ConfidenceRouter instance.
54
+ * @param opts - Report options.
55
+ */
56
+ export declare function generateMonthlyReport(transactions: Transaction[], classifier: TwoStageClassifier, exclusion: ExclusionChecker, router: ConfidenceRouter, opts: {
57
+ company_name?: string;
58
+ month: string;
59
+ compare_transactions?: Transaction[];
60
+ compare_label?: string;
61
+ format?: 'markdown' | 'json';
62
+ }): Promise<MonthlyReportResult>;
63
+ export {};
64
+ //# sourceMappingURL=monthly-report.d.ts.map