docs(10): resolve plan-checker blockers on validation strategy and research

Populate 10-VALIDATION.md from RESEARCH.md's Validation Architecture
section (was an unfilled template), add RESOLVED/DEFERRED markers to
RESEARCH.md's Open Questions, and align pax8_subscriptions/order_items
product_id to UUID (matching pax8_products.id) to avoid a Phase 11 cast.
This commit is contained in:
lorentz 2026-07-10 13:23:56 -04:00
parent 26f41a7011
commit 741b2425c1
3 changed files with 51 additions and 34 deletions

View file

@ -116,16 +116,18 @@ Output: `migrations/091_pax8_tables.sql`, applied and verified against the dev D
3. `pax8_subscriptions`: id UUID PRIMARY KEY, pax8_company_id UUID (plain indexed 3. `pax8_subscriptions`: id UUID PRIMARY KEY, pax8_company_id UUID (plain indexed
column, NOT a hard FK — sync insert order across companies/subscriptions is not column, NOT a hard FK — sync insert order across companies/subscriptions is not
guaranteed; add a `-- soft ref` comment), product_id TEXT, quantity INTEGER (seat guaranteed; add a `-- soft ref` comment), product_id UUID (soft ref, matches
count), billing_term TEXT, status TEXT, start_date TIMESTAMPTZ, raw_payload JSONB, pax8_products.id's UUID type so Phase 11's catalog join needs no cast), quantity
+ the three audit columns. INTEGER (seat count), billing_term TEXT, status TEXT, start_date TIMESTAMPTZ,
raw_payload JSONB, + the three audit columns.
4. `pax8_orders` (header — Invoice-shaped): id UUID PRIMARY KEY, pax8_company_id UUID 4. `pax8_orders` (header — Invoice-shaped): id UUID PRIMARY KEY, pax8_company_id UUID
(soft ref, indexed), order_date TIMESTAMPTZ, total NUMERIC(12,2), status TEXT, (soft ref, indexed), order_date TIMESTAMPTZ, total NUMERIC(12,2), status TEXT,
currency CHAR(3) NOT NULL DEFAULT 'USD', raw_payload JSONB, + the three audit columns. currency CHAR(3) NOT NULL DEFAULT 'USD', raw_payload JSONB, + the three audit columns.
5. `pax8_order_items` (line — InvoiceItem-shaped): id UUID PRIMARY KEY, order_id UUID 5. `pax8_order_items` (line — InvoiceItem-shaped): id UUID PRIMARY KEY, order_id UUID
NOT NULL REFERENCES pax8_orders(id) ON DELETE CASCADE, product_id TEXT, NOT NULL REFERENCES pax8_orders(id) ON DELETE CASCADE, product_id UUID (soft ref,
matches pax8_products.id's UUID type — no cast needed for Phase 11/12 joins),
quantity INTEGER, unit_price NUMERIC(12,2), line_total NUMERIC(12,2), quantity INTEGER, unit_price NUMERIC(12,2), line_total NUMERIC(12,2),
currency CHAR(3) NOT NULL DEFAULT 'USD', raw_payload JSONB, + the three audit columns. currency CHAR(3) NOT NULL DEFAULT 'USD', raw_payload JSONB, + the three audit columns.

View file

@ -554,9 +554,9 @@ convert A1 from CITED to VERIFIED in about 30 seconds and is far more reliable t
documentation fetching. Recommend the planner include this as an early verification task in documentation fetching. Recommend the planner include this as an early verification task in
Phase 10's plan (it directly demonstrates success criterion #2 anyway). Phase 10's plan (it directly demonstrates success criterion #2 anyway).
## Open Questions ## Open Questions (RESOLVED for Phase 10 — none block this phase)
1. **Should `pax8_orders`/`pax8_order_items` be sourced from PAX8's `/orders` API or 1. **(RESOLVED — applied) Should `pax8_orders`/`pax8_order_items` be sourced from PAX8's `/orders` API or
`/invoices` + `/invoices/{id}/items` API?** `/invoices` + `/invoices/{id}/items` API?**
- What we know: `/orders` has no pricing/status fields; `/invoices/{id}/items` has all the - What we know: `/orders` has no pricing/status fields; `/invoices/{id}/items` has all the
fields D-01/D-04 describe (total, status via `amountDue`/invoice-level `status`, unit price fields D-01/D-04 describe (total, status via `amountDue`/invoice-level `status`, unit price
@ -570,8 +570,12 @@ Phase 10's plan (it directly demonstrates success criterion #2 anyway).
fields (this research's Pitfall 1 + migration skeleton above), and flag to the user during fields (this research's Pitfall 1 + migration skeleton above), and flag to the user during
Phase 12 planning (not Phase 10) that the sync will hit `/invoices/{id}/items`, not Phase 12 planning (not Phase 10) that the sync will hit `/invoices/{id}/items`, not
`/orders` — a one-line confirmation, not a redesign. `/orders` — a one-line confirmation, not a redesign.
- **Resolution:** Applied in 10-02-PLAN.md — `pax8_orders`/`pax8_order_items` are typed with
Invoice/InvoiceItem-shaped columns (`total`, `status`, `currency`, `unit_price`,
`line_total`), table names unchanged per D-01. Deferred item carried forward: Phase 12
planning must confirm the sync source is `/invoices/{id}/items`, not `/orders`.
2. **Does PAX8 actually populate `externalId` with anything useful out of the box, or is it 2. **(DEFERRED — non-blocking for Phase 10, revisit in Phase 12) Does PAX8 actually populate `externalId` with anything useful out of the box, or is it
always null until a partner writes to it?** always null until a partner writes to it?**
- What we know: PAX8's docs reference `externalId` as a slot partners can use to store their - What we know: PAX8's docs reference `externalId` as a slot partners can use to store their
own identifiers (seen in the Microsoft-subscription-reconciliation guide, in a Microsoft own identifiers (seen in the Microsoft-subscription-reconciliation guide, in a Microsoft
@ -581,8 +585,11 @@ Phase 10's plan (it directly demonstrates success criterion #2 anyway).
- Recommendation: Not blocking for Phase 10 (the column exists either way — `external_id TEXT` - Recommendation: Not blocking for Phase 10 (the column exists either way — `external_id TEXT`
nullable). Worth a live check in Phase 12 (a few real `GET /companies` responses from nullable). Worth a live check in Phase 12 (a few real `GET /companies` responses from
production credentials) before finalizing the fuzzy-match-vs-externalId-lookup strategy. production credentials) before finalizing the fuzzy-match-vs-externalId-lookup strategy.
- **Resolution:** Deferred by design — 10-02-PLAN.md's `pax8_companies.external_id TEXT`
column is nullable and imposes no constraint, keeping the door open either way. No Phase 10
action required; carried forward as a Phase 12 pre-work check.
3. **What confidence-scoring approach will Phase 12's fuzzy-name matching use** (e.g. 3. **(DEFERRED — non-blocking for Phase 10, revisit in Phase 12) What confidence-scoring approach will Phase 12's fuzzy-name matching use** (e.g.
Levenshtein, token-set-ratio, a Postgres extension like `pg_trgm`)? Levenshtein, token-set-ratio, a Postgres extension like `pg_trgm`)?
- What we know: `device_link_review`'s `match_confidences TEXT[]` stores confidence as text - What we know: `device_link_review`'s `match_confidences TEXT[]` stores confidence as text
labels, not a numeric score — precedent exists for either representation. labels, not a numeric score — precedent exists for either representation.
@ -592,6 +599,9 @@ Phase 10's plan (it directly demonstrates success criterion #2 anyway).
for either a numeric-as-text or a labeled ("high"/"medium"/"low") representation. for either a numeric-as-text or a labeled ("high"/"medium"/"low") representation.
- Recommendation: Use `TEXT[]` for `match_confidences` in this phase's migration, matching - Recommendation: Use `TEXT[]` for `match_confidences` in this phase's migration, matching
`device_link_review` exactly — defer the actual scoring algorithm decision to Phase 12. `device_link_review` exactly — defer the actual scoring algorithm decision to Phase 12.
- **Resolution:** Applied in 10-02-PLAN.md — `pax8_company_match_review.match_confidences`
is `TEXT[] NOT NULL`, matching `device_link_review` exactly. Scoring algorithm choice
explicitly deferred to Phase 12, as recommended.
## Environment Availability ## Environment Availability

View file

@ -1,8 +1,8 @@
--- ---
phase: 10 phase: 10
slug: pax8-client-auth-foundation slug: pax8-client-auth-foundation
status: draft status: approved
nyquist_compliant: false nyquist_compliant: true
wave_0_complete: false wave_0_complete: false
created: 2026-07-10 created: 2026-07-10
--- ---
@ -17,20 +17,20 @@ created: 2026-07-10
| Property | Value | | Property | Value |
|----------|-------| |----------|-------|
| **Framework** | {pytest 7.x / jest 29.x / vitest / go test / other} | | **Framework** | vitest 4.1.5 |
| **Config file** | {path or "none — Wave 0 installs"} | | **Config file** | `vitest.config.ts` (root) — `include: ['lib/**/*.test.ts']`, `globals: false` |
| **Quick run command** | `{quick command}` | | **Quick run command** | `npx vitest run lib/services/pax8-client.test.ts lib/services/pax8-factory.test.ts` |
| **Full suite command** | `{full command}` | | **Full suite command** | `npm test` |
| **Estimated runtime** | ~{N} seconds | | **Estimated runtime** | ~10 seconds (2 small new test files; full suite already fast per existing CI notes) |
--- ---
## Sampling Rate ## Sampling Rate
- **After every task commit:** Run `{quick run command}` - **After every task commit:** Run `npx vitest run lib/services/pax8-client.test.ts lib/services/pax8-factory.test.ts` (once these exist, from Plan 01 Tasks 2/3)
- **After every plan wave:** Run `{full suite command}` - **After every plan wave:** Run `npm test` (full suite — cheap, this phase adds ~2 small test files, no regressions expected)
- **Before `/gsd:verify-work`:** Full suite must be green - **Before `/gsd:verify-work`:** Full suite must be green, plus the migration idempotency check (Plan 02 Task 2) and the live-credential checkpoint (Plan 03 Task 2)
- **Max feedback latency:** {N} seconds - **Max feedback latency:** ~10 seconds (vitest quick run); the live auth-proof checkpoint is manual and not part of automated sampling
--- ---
@ -38,7 +38,13 @@ created: 2026-07-10
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | | Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| |---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| {N}-01-01 | 01 | 1 | REQ-{XX} | T-{N}-01 / — | {expected secure behavior or "N/A"} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending | | 10-01-T1 | 01 | 1 | PAX8-01 / PAX8-02 | — | N/A (types-only, no runtime secret handling) | source-assertion | `npx tsc --noEmit --pretty` | ❌ W0 | ⬜ pending |
| 10-01-T2 | 01 | 1 | PAX8-01 | T-10-01, T-10-02, T-10-03, T-10-04, T-10-05 | Client secret never logged/thrown; audience + JSON body hardcoded; TLS host hardcoded; 60s expiry buffer | unit (mocked fetch) | `npx vitest run lib/services/pax8-client.test.ts` | ❌ W0 | ⬜ pending |
| 10-01-T3 | 01 | 1 | PAX8-02 | T-10-01 | Config-presence check + throw-if-missing never echoes secret value | unit | `npx vitest run lib/services/pax8-factory.test.ts` | ❌ W0 | ⬜ pending |
| 10-02-T1 | 02 | 1 | PAX8-01 / PAX8-02 | T-10-06 | Static DDL, no interpolated/dynamic values — injection not reachable | source-assertion (grep) | `test $(grep -v '^--' migrations/091_pax8_tables.sql \| grep -c "CREATE TABLE IF NOT EXISTS pax8_") -eq 6` | ❌ W0 | ⬜ pending |
| 10-02-T2 | 02 | 1 | PAX8-01 / PAX8-02 | T-10-07 | `IF NOT EXISTS` only, no DROP/ALTER of existing data; re-apply is a proven no-op | manual/smoke | `docker exec pulse-postgres psql -U pulse_user -d pulse_autotask -tAc "SELECT count(*) FROM information_schema.tables WHERE table_name LIKE 'pax8\_%'"` (falls back to a documented pending-developer-step if the container is unreachable) | N/A | ⬜ pending |
| 10-03-T1 | 03 | 2 | PAX8-01 | T-10-10 | Verify script logs only counts/status — never access token or client secret | source-assertion (grep) + build | `grep -n "access_token\|accessToken\|clientSecret\|CLIENT_SECRET" scripts/verify-pax8-auth.ts` (expect no match) `&& npx tsc --noEmit --pretty` | ❌ W0 | ⬜ pending |
| 10-03-T2 | 03 | 2 | PAX8-01 (live proof, ROADMAP SC#2) | T-10-09, T-10-11 | Real credentials only in gitignored `.env.local`; wrong-audience/wrong-encoding failures surface as documented signals, not silent | manual (`checkpoint:human-verify`) | `npx tsx scripts/verify-pax8-auth.ts` (developer-run, requires live `PAX8_CLIENT_ID`/`PAX8_CLIENT_SECRET`) | N/A | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* *Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
@ -46,11 +52,11 @@ created: 2026-07-10
## Wave 0 Requirements ## Wave 0 Requirements
- [ ] `{tests/test_file.py}` — stubs for REQ-{XX} - [ ] `lib/services/pax8-client.test.ts` — covers PAX8-01 (token exchange body/headers, expiry-cache reuse, auth-proof call, secret-never-thrown). Written as part of Plan 01 Task 2 (TDD: written before `pax8-client.ts`), not a separate pre-phase scaffold.
- [ ] `{tests/conftest.py}` — shared fixtures - [ ] `lib/services/pax8-factory.test.ts` — covers PAX8-02 (`isPax8Configured()`, throw-if-missing, singleton + reset). Written as part of Plan 01 Task 3 (TDD: written before `pax8-factory.ts`).
- [ ] `{framework install}` — if no framework detected - [ ] No new test framework/config needed — vitest is already configured project-wide and the `lib/**/*.test.ts` glob already picks up these two new files automatically.
*If none: "Existing infrastructure covers all phase requirements."* *Both Wave 0 files are created inline by Plan 01's TDD tasks rather than a separate Wave 0 pass — no standalone scaffolding step is needed before Wave 1 starts.*
--- ---
@ -58,19 +64,18 @@ created: 2026-07-10
| Behavior | Requirement | Why Manual | Test Instructions | | Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------| |----------|-------------|------------|-------------------|
| {behavior} | REQ-{XX} | {reason} | {steps} | | Live OAuth2 token exchange + `/companies` read against the real PAX8 API | PAX8-01 (ROADMAP SC#2) | No existing integration client in this codebase has a "live API" test tier (cost/reliability of hitting real vendor APIs in CI); `PAX8_CLIENT_ID`/`PAX8_CLIENT_SECRET` are not yet present in `.env.local` and must be developer-provisioned | See Plan 10-03 Task 2 checkpoint: add credentials to `.env.local`, run `npx tsx scripts/verify-pax8-auth.ts`, confirm it prints a company count and exits 0 |
| Migration idempotency on the live dev DB | PAX8-01 / PAX8-02 (ROADMAP SC#4) | Postgres applies `migrations/*.sql` on first volume boot only (per CLAUDE.md) — the existing dev DB volume already booted, so this migration must be applied manually, not via container init | See Plan 10-02 Task 2: `bash scripts/apply-migrations.sh 091_pax8_tables.sql` run twice, second run must exit 0 with no error |
*If none: "All phase behaviors have automated verification."*
--- ---
## Validation Sign-Off ## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies - [x] All tasks have `<automated>` verify or Wave 0 dependencies (2 manual-only exceptions documented above, both with explicit fallback/no-op behavior if infra is unreachable)
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify - [x] Sampling continuity: no 3 consecutive tasks without automated verify (only the two manual-only rows lack one, and they are not adjacent — 10-02-T2 sits between two automated tasks; 10-03-T2 is the final task in the phase)
- [ ] Wave 0 covers all MISSING references - [x] Wave 0 covers all MISSING references (both `❌ W0` test files are covered by Plan 01's own TDD tasks)
- [ ] No watch-mode flags - [x] No watch-mode flags (`vitest run`, not `vitest watch`, used throughout)
- [ ] Feedback latency < {N}s - [x] Feedback latency < 10s (vitest quick run; manual checkpoints are explicitly out-of-band, not part of the automated feedback loop)
- [ ] `nyquist_compliant: true` set in frontmatter - [x] `nyquist_compliant: true` set in frontmatter
**Approval:** {pending / approved YYYY-MM-DD} **Approval:** approved 2026-07-10