← back to Directory Core

README.md

138 lines

# directory-core

Shared TypeScript primitives for Steve's **directory verticals** — currently lawyer/doctor/animals/restaurant, future architects/dentists/etc.

Carved out **2026-05-04** after the overnight YOLO review found the same `provider → directory → 3-mockups → claim → upgrade` pattern duplicated across 4 separate codebases.

## What's a directory vertical?

A site that:
1. **Ingests** a roster of providers from public records (CalBar / NPI / state boards / LA County DPH)
2. **Enriches** with website/phone/email/social
3. **Renders** a directory page per provider with SEO copy
4. **Generates** 3 mockup variants per provider (x1/x2/x3) for outreach
5. **Claims** flow — practitioner verifies + upgrades to a paid tier
6. **Monetizes** via ads + leads + $499 site upgrade + B2B data lists

If a project doesn't do all 6, it's NOT a directory vertical:
- ❌ stayclaim — multi-tenant address-history (place-graph-core instead)
- ❌ trademarks-copyright — opportunity-discovery + newsletter (drops-core instead)
- ❌ jill-website — bespoke 4-property rental site
- ❌ philipperomano — DW vendor storefront (`/dw-site-build` skill)
- ❌ site/ai/visual-factory — pipeline factories (factory-core instead)

## Confirmed consumers (4)

| Project | DB | Path | TS native? |
|---|---|---|---|
| `lawyer-directory-builder` | `lawyer_professional_directory` | `~/Projects/lawyer-directory-builder/` | ✅ TS — **canonical source** |
| `professional-directory` (doctor) | `doctor_professional_directory` | `~/Projects/professional-directory/` | ❌ JS — adapter via `.d.ts` |
| `animals` | `animals_directory` | `~/Projects/animals/` | ❌ JS — adapter via `.d.ts` |
| `restaurant-directory` (lacountyeats) | `restaurant_professional_directory` | `~/Projects/restaurant-directory/` | ✅ TS |

## Module list (extraction targets)

| Module | LOC est | Source-of-truth | Purpose |
|---|---|---|---|
| `src/db.ts` | ~30 | lawyer-directory `src/db/pool.ts` | `Pool` + `query<T>()` + `FacilityRow` types |
| `src/geo.ts` | ~100 | animals `src/lib/geo.js` | Nominatim + zip-centroid lookup, real-25mi radius |
| `src/auth.ts` | ~250 | lawyer-directory `src/server/auth.ts` | Claim flow, `requireUser`, `requireAdmin`, signed-token verification |
| `src/mockups.ts` | ~400 | lawyer-directory `src/lib/mockup_templates.ts` + `src/enrich/render_mockups.ts` | x1/x2/x3 variant generation, Playwright capture |
| `src/compliance.ts` | ~80 | lawyer-directory `src/lib/compliance.ts` | Mode gate (directory \| hybrid \| lrs_certified), §6155 enforcement |
| `src/html.ts` | ~50 | lawyer-directory `src/server/_html.ts` | Shared `esc()`, `layout()`, pagination, grade-pill |
| `src/match.ts` | ~90 | (vertical-agnostic geo match — extract from animals) | Provider↔inquiry matching scorer |
| `src/index.ts` | ~10 | (re-exports) | Barrel |

## Stays per-vertical

- Specialty / category lists (`lawyer.practiceAreas`, `doctor.specialties`, etc.)
- Per-vertical schema migrations
- Per-vertical scrapers (CalBar, NPI, state vet boards, LA County DPH)
- Domain-specific compliance (Cal §6155 for lawyer, HIPAA-adjacent for doctor)
- Per-vertical SEO copy templates

## Migration plan (4 phases)

### Phase 1 — Scaffold + extract `db.ts` (today)
- Create this package
- Extract `pool.ts` from lawyer-directory (already cleanest impl)
- Wire one consumer to import from `directory-core/db` instead of local
- Verify lawyer-directory still boots

### Phase 2 — Extract `auth.ts` + `compliance.ts`
- These are the most-load-bearing legally
- Codify §6155 mode gate so the next compliance fix lands once

### Phase 3 — Extract `mockups.ts`
- Biggest LOC win (~400 across 4 verticals = 1,600 LOC dedup)
- Most fragile (Playwright sandbox, template rotation, FS writes)

### Phase 4 — Extract `html.ts` + `geo.ts` + `match.ts`
- Quick polish, low risk

## Why TS-native

Lawyer-directory is the only TS sibling. TS gives shared compliance types real teeth (`type Mode = 'directory' | 'hybrid' | 'lrs_certified'`). JS consumers (doctor, animals) consume via `.d.ts` shims — Node 20+ handles ESM TS via `tsx` or built `dist/`.

## What this is NOT

- A monorepo. Each vertical stays its own project, just imports from this one.
- A framework. No opinions on routing, ORM, render layer.
- A CLI. Pure library.

## Status

- [x] Scaffolded 2026-05-04
- [x] Phase 1: db.ts extracted
- [x] Phase 2: auth.ts + compliance.ts extracted
- [ ] Phase 3: mockups.ts extracted ⚠️ **DEFERRED — see "Architect findings" below**
- [x] Phase 4a: geo.ts extracted (haversineMi/Km + geocodeZip + makePgZipCache)
- [x] Phase 4b: match.ts extracted (resolveZipAnchor + EmptyReason + bbox + SQL fragments)
- [ ] Phase 4c: html.ts extracted (lowest priority)
- [x] **All 4 consumers migrated to directory-core/db** (2026-05-04 ticks 20–23):
  - restaurant-directory (TS+ESM, direct import) ✓
  - lawyer-directory-builder (TS+ESM, src/db/pool.ts → 1-line re-export) ✓
  - animals (JS+ESM, src/lib/db.js → shim with `tx`/`one`/`many` preserved) ✓
  - professional-directory (JS+CJS via `require(esm)`, agents/shared/db.js → shim with slow-query wrapper preserved) ✓
- [x] **dist/ build pipeline** (tsconfig.build.json, emits .js + .d.ts + source maps) — JS consumers can import without tsx
- [x] **83/83 tests passing** (auth: 16, auth-factory: 15, compliance: 5, db: 7, geo: 20, match: 20)
- [x] **auth.ts factory refactor** (tick 24, architect's #1 priority): `createAuth({...})` returns scoped functions; BaseAppUser shrunk to `{id, status?}`; configurable userTable / sessionTable / sessionPkColumn / sessionUserFkColumn / cookieHostPrefix / userColumns / isAdmin
- [x] **`findUserAndVerifyPassword` with WORKING DUMMY_HASH defense** — animals' current DUMMY_HASH is broken (rejects in 0ms), defeating its own timing-attack protection. directory-core's version takes ~65ms (verified at test time)
- [x] **Singleton verified**: same `pool` instance across all 4 verticals — no duplicate PG connections per box

## Architect findings (2026-05-04 review pass)

Independent architect-reviewer flagged serious schema-coupling and ordering concerns. Read before any vertical migrates.

**1. `BaseAppUser` leaks lawyer-isms.** Animals will break — different role enum (`'business_owner'|'admin'|'staff'` vs `'user'|'admin'`), no `plan`/`status`/`professional_id`/`organization_id` columns; uses `suspended_at TIMESTAMPTZ` instead. Doctor + restaurant likely diverge similarly. The hard-coded SELECT in `findUserById` bakes in column names → consumers will throw `column "X" does not exist`.

**Fix before migrating any vertical:** invert the dependency. Make `auth.ts` a `createAuth({ userColumns, userTable, sessionTable, roleAdminCheck })` factory. `BaseAppUser` shrinks to `{ id; status }`. Vertical declares column list + admin predicate.

**2. Hardcoded `app_users` / `app_sessions` table names.** Per MEMORY: dw_unified_sub replicates 421 tables from Kamatera via logical replication. If a vertical's PG happens to have a colliding `app_users` from replication, directory-core would auth against it. Make table names env-configurable: `AUTH_USERS_TABLE`, `AUTH_SESSIONS_TABLE`.

**3. Module-level side effect in `db.ts`** — `throw new Error('DATABASE_URL is required')` at module load makes the lib impossible to unit-test in isolation. Move into lazy `pool` init.

**4. Phase 3 `mockups.ts` is the WRONG next move.** The 3 verticals diverged past the point where shared abstraction helps:
- **animals** uses generative 54k-combo design system (`specsFor(biz, 3)`)
- **doctor** uses 16-vertical org-type classifier + tainted-output validator + Ollama qwen3:32b
- **lawyer** delegates entirely to launchd-orchestrated `lawyer-build-agent` (Mac1 worker)
- **restaurant** uses filesystem `iterations/` snapshots, no PG `site_mockups` table

Three storage models, three rendering pipelines, three selection criteria. The "3-mockup-per-record" pattern is a coincidence, not a shared abstraction.

**Reprioritized Phase 4 → next:** do `geo.ts` (haversine + zip-centroid lookup, pure functions, vertical-neutral) and `match.ts` (fuzzy firm-name matching) FIRST. Mockups extraction comes later (or never) once 2+ verticals organically converge.

**5. `compliance.ts` per-process rate limit caveat.** Module-level `lastFetchByHost` Map = each Node worker process gets its own RPS budget. If lawyer-overnight + doctor pd-crawler run on the same box, they double-bombard hosts. Document, or move to Redis for prod.

**6. Anti-pattern: should have copied animals' login (with `DUMMY_HASH` constant-time defense and `__Host-` cookie) rather than lawyer's.** Animals has battle-scars lawyer hasn't earned yet. Port those defenses before declaring v1.0.

**7. Mixed export shapes in `index.ts`** — `export * from './auth.ts'` plus `export * as compliance` is inconsistent. Pick one. Recommend `export * as auth, * as compliance, * as db` to prevent symbol collision when phase 4 lands `match.score()`.

**8. `peerDependencies` check** — verify `pg`/`express`/`bcryptjs` are peers, not deps. Otherwise 4 verticals bundle 4 copies.

**Action items, ranked:**
1. Don't migrate any vertical until auth.ts is parameterized (otherwise it'll break on first contact with animals or doctor schema).
2. Add tests/ that exercise auth against fixtures from all 4 vertical schemas.
3. Skip Phase 3 indefinitely — do Phase 4 next, mockups never (or much later).
4. Doc: per-process rate limit, `__Host-` cookie option, animals being a better source of truth.