docs(12-03): complete pax8 company matcher plan
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
ae44669b9e
commit
8868f2c9bd
1 changed files with 113 additions and 0 deletions
|
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
phase: 12-orders-invoices-company-matching
|
||||
plan: 03
|
||||
subsystem: database
|
||||
tags: [postgres, pg_trgm, fuzzy-matching, typescript, pax8]
|
||||
|
||||
requires:
|
||||
- phase: 12-orders-invoices-company-matching (Plan 01)
|
||||
provides: pg_trgm extension enabled, pax8_companies auto-match columns (autotask_company_id, match_confidence, match_method, matched_at) via migration 093
|
||||
provides:
|
||||
- matchPax8Companies() — the D-01..D-05 fuzzy company matcher, isolated in its own service + test module
|
||||
- Exported AUTO_LINK_THRESHOLD (0.90) / TIE_MARGIN (0.05) / CANDIDATE_FLOOR (0.3) tunables
|
||||
- Pax8CompanyMatchResult result shape (scanned/autoLinked/flaggedAmbiguous/flaggedNoCandidate/durationMs)
|
||||
affects: [12-04-pax8-sync-service-invoice-sync (wires matchPax8Companies into Pax8SyncService.fullSync())]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Confidence-ranked matching, ported from device-link-reconciler.ts's findBy*/applyLink/recordConflict/pickBestCandidate shape, adapted from a cascade of exact-match strategies to a single pg_trgm similarity() score"
|
||||
- "Idempotency guard via NOT EXISTS on a resolved review row + match_method IS DISTINCT FROM 'manual', preventing automated re-scoring from ever overwriting a human decision"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- lib/services/pax8-company-matcher.ts
|
||||
- lib/services/pax8-company-matcher.test.ts
|
||||
modified: []
|
||||
|
||||
key-decisions:
|
||||
- "CANDIDATE_FLOOR is inlined as a template-literal constant in the SQL string (not a bound param) since it's a hardcoded module constant, not external input — only the PAX8 company name (external input) is bound as $1, per the Security Domain requirement"
|
||||
- "A now-confident auto-match closes any open (never-human-touched) review row for that company (DELETE ... WHERE resolved_at IS NULL) — a stale ambiguous flag shouldn't linger once the matcher becomes confident again on a later run"
|
||||
|
||||
patterns-established:
|
||||
- "Single-strategy scored matcher shape reusable for any future fuzzy-match problem: findCandidates (parameterized, floor-filtered) -> decide (threshold + tie-margin) -> applyLink/recordConflict (idempotency-guarded)"
|
||||
|
||||
requirements-completed: [PAX8-10, PAX8-11]
|
||||
|
||||
duration: 25min
|
||||
completed: 2026-07-11
|
||||
---
|
||||
|
||||
# Phase 12 Plan 03: PAX8 Company Fuzzy Matcher Summary
|
||||
|
||||
**pg_trgm-based fuzzy matcher (`matchPax8Companies`) ports device-link-reconciler.ts's confidence-ranked match/review shape to PAX8-Autotask company matching, at a validated 0.90 auto-link floor with a 0.05 tie-margin, six unit tests proving every decision branch plus the human-resolution idempotency guard.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~25 min
|
||||
- **Started:** 2026-07-11T02:26:00Z
|
||||
- **Completed:** 2026-07-11T02:47:50Z
|
||||
- **Tasks:** 2/2 completed
|
||||
- **Files modified:** 2 (both created)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- `lib/services/pax8-company-matcher.ts` implements the full D-01..D-05 match policy: `findCandidates` (parameterized `similarity($1, company_name)` query, `is_active = true` filter per Pitfall 5), `decide` (auto-link vs. review), `applyLink` (resolved-row idempotency guard), `recordConflict` (top-3/empty-array review upsert with stale-match cleanup)
|
||||
- `matchPax8Companies(opts?)` scans the re-scoring-eligible subset of `pax8_companies` (excludes `match_method = 'manual'` and rows with a human-resolved review row) and returns a `Pax8CompanyMatchResult` rollup
|
||||
- `lib/services/pax8-company-matcher.test.ts` proves all five decision branches plus `dryRun`, mocking `postgresClient.query`'s default export — 6/6 tests passing
|
||||
- Caught and fixed a block-comment-terminator bug during Task 1 (`findBy*/pickBestCandidate` in a doc comment prematurely closed the `/* */` block, breaking every downstream parse) before it ever reached a commit
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: Create lib/services/pax8-company-matcher.ts** - `691bb47` (feat)
|
||||
2. **Task 2: Create lib/services/pax8-company-matcher.test.ts** - `ae44669` (test)
|
||||
|
||||
_Plan metadata commit follows this summary (worktree mode — orchestrator merges and updates STATE.md/ROADMAP.md after the wave)._
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `lib/services/pax8-company-matcher.ts` - Fuzzy PAX8-to-Autotask company matcher: `matchPax8Companies`, `AUTO_LINK_THRESHOLD`, `TIE_MARGIN`, `CANDIDATE_FLOOR`, `Pax8CompanyMatchResult`
|
||||
- `lib/services/pax8-company-matcher.test.ts` - Unit tests for all five decision branches (auto-link, below-threshold review, near-tie review, empty-candidate review, idempotency guard) plus dryRun
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- Kept `CANDIDATE_FLOOR` as an inlined SQL literal rather than a bound param — it's a hardcoded internal constant, not user/external input, so parameterizing it would add no security value while the PAX8 company name (the actual external input) stays strictly bound as `$1`
|
||||
- `applyLink` closes any open review row for the same company on a fresh auto-match, since a newly confident automated match supersedes a previously-flagged (never human-touched) ambiguity — only rows with `resolved_at IS NOT NULL` (human-resolved) are left untouched
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None — plan executed exactly as written. One inline bug was caught and fixed during Task 1 before any commit (see Deferred Issues below — not a deviation from the plan's design, a syntax slip in a doc comment).
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] Fixed a block-comment-terminating `*/` sequence inside a doc comment**
|
||||
- **Found during:** Task 1, immediately after first `npx tsc --noEmit` run
|
||||
- **Issue:** The header doc comment wrote `findBy*/pickBestCandidate` (intending "findBy-star, pickBestCandidate") — TypeScript parsed the `*/` as the end of the `/** ... */` block comment, causing ~100 cascading parse errors for the rest of the file
|
||||
- **Fix:** Reworded to `findBy-x / pickBestCandidate / applyLink / recordConflict` — no `*/` substring remains in any comment
|
||||
- **Files modified:** `lib/services/pax8-company-matcher.ts`
|
||||
- **Commit:** `691bb47` (fixed before the file was ever committed — not a separate commit)
|
||||
|
||||
## Verification Results
|
||||
|
||||
- `npx tsc --noEmit --pretty` — no new errors; the only 2 remaining errors (`sync-scheduler.ts:446,450`, missing `appgate-factory`/`appgate-sync-service` modules) are the same pre-existing, unrelated failures documented as Deferred in `12-01-SUMMARY.md`
|
||||
- `grep -Fc 'similarity($1, company_name)' lib/services/pax8-company-matcher.ts` -> 2 (both `findCandidates` occurrences bind the PAX8 name as `$1`)
|
||||
- `grep -c "company_name +"` -> 0 (no string concatenation into SQL)
|
||||
- `npx vitest run lib/services/pax8-company-matcher.test.ts` -> 6/6 tests passed
|
||||
- Manually confirmed `applyLink`'s UPDATE SQL contains both `match_method IS DISTINCT FROM 'manual'` and the `resolved_at IS NOT NULL` `NOT EXISTS` guard
|
||||
- Manually confirmed `recordConflict`'s upsert targets `pax8_company_match_review` with `ON CONFLICT (pax8_company_id) WHERE resolved_at IS NULL`
|
||||
|
||||
## Deferred Issues
|
||||
|
||||
- **Pre-existing type-check failure, unrelated to this plan.** `lib/services/sync-scheduler.ts:446,450` references `@/lib/services/appgate-factory` and `@/lib/services/appgate-sync-service` via dynamic `import()`, neither of which exists at this worktree's commit. Already logged in `12-01-SUMMARY.md` and `.planning/phases/12-orders-invoices-company-matching/deferred-items.md`. Confirmed still present and still unrelated to this plan's two new files (both type-check clean in isolation).
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None — this plan is a pure service + test module, no UI, no partial data wiring.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None — this plan's only new surface is the matcher module itself, and every threat register item from the plan's `<threat_model>` (T-12-01, T-12-02, T-12-04, T-12-06) is directly mitigated in the implementation (parameterized queries, conservative threshold/tie-margin with unit-tested branches, resolved-row idempotency guard, bounded scan). No new network endpoints, auth paths, or schema changes were introduced.
|
||||
|
||||
## Self-Check: PASSED
|
||||
Loading…
Add table
Add a link
Reference in a new issue