feat(readiness): canonical trust-readiness boundary (P0-P2 convergence)#339
feat(readiness): canonical trust-readiness boundary (P0-P2 convergence)#339ctol3r wants to merge 1 commit into
Conversation
Eliminates competing trust/readiness authorities by routing all score,
level, and band derivation through a single canonical engine.
NEW: apps/api/backend/src/services/readiness/
- canonical.ts — CanonicalBlocker, CanonicalReadiness,
CanonicalTrustLevel, derivePassportReadiness,
deriveCanonicalTrustLevel, mapLevelToBand,
categorizeBlocker (string→structured adapter)
- readinessAdapter.ts — translates canonical output to legacy shapes:
toLegacyTrustBand, toLegacyBlockingReasons,
toLegacyStringBlockers, toPassportReadinessFields,
toTrustStateEngineReadiness
- __tests__/canonical.test.ts — 30 tests pinning the canonical math
WIRED: trustCore.computeDeterministicTrustReadiness now consumes
derivePassportReadiness for the readinessScore. The previous
weighted-confidence formula (identity 20 + exclusion 30 + licensure 30 +
enrollment 20) × confidence is no longer used; dimension confidence is
preserved on confidenceWeighting for informational purposes only.
WIRED: trustCore.deriveTrustBandFromReadiness no longer pattern-matches
blocker strings via regex. It categorizes via categorizeBlocker (the
regex authority moved into a clearly-labeled string→structured adapter)
and calls deriveCanonicalTrustLevel, which uses structural severity
(HARD/SOFT/INFO) instead of regex.
WIRED: passportService.buildPassport replaces its inline score/level
math with a single derivePassportReadiness call. The file-local
derivePassportReadinessLevel becomes a backward-compat shim that
forwards to deriveCanonicalTrustLevel.
PRESERVED: All public API response shapes — PassportReadiness fields,
DeterministicTrustReadiness shape, ClinicianTrustState shape — remain
identical. Behavior-frozen packages/trust-state is untouched.
Test-suite migration: 3 trustCore assertions updated to canonical scores
(96→100, 67→75, 48→50) — those scores now match what
/api/passport/:npi returns for the same coverage, closing the
score-divergence gap the audit identified.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c2673e2d38
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (EXCLUSION_RE.test(trimmed)) { | ||
| return { id: deterministicId('EXCLUSION', 'HARD', trimmed), message: trimmed, severity: 'HARD', category: 'EXCLUSION' }; |
There was a problem hiding this comment.
Don't treat all OIG/LEIE text as hard exclusion
The exclusion categorizer currently marks any blocker containing oig or leie as HARD, which incorrectly escalates review-only messages (for example, the OIG/LEIE possible match requires review blocker produced in review-required flows) into hard blocks. When this passes through computeDeterministicTrustReadiness, it drives readiness to score 0 / L0 instead of pending verification behavior, so non-confirmed exclusion matches can be reported as fully blocked.
Useful? React with 👍 / 👎.
| sourceCoverage: [...readiness.blockers].length // placeholder no-op; coverage filled by caller | ||
| ? [] | ||
| : [], |
There was a problem hiding this comment.
Preserve source coverage in adapter output
toTrustStateEngineReadiness currently hardcodes sourceCoverage to an empty array in both branches of its ternary expression, so every adapted readiness object loses coverage provenance. Any downstream consumer using this adapter will see no source coverage even when canonical readiness was built from real checks, which breaks traceability and can degrade follow-on readiness logic.
Useful? React with 👍 / 👎.
Summary
Eliminates competing trust/readiness authorities. The same NPI can no longer produce different (score, level, band) triples across
/api/passport/:npi,/api/passport/:npi/trust, andTrustPosturebecause all three now route through one canonical engine.Convergence boundary
derivePassportReadinessidentity 20 + exclusion 30 + licensure 30 + enrollment 20) × confidence formula — INDEPENDENT scorederivePassportReadiness, keepsDeterministicTrustReadinessshape/excluded|license expired|pecos.../i)deriveCanonicalTrustLevelapps/api/backend/src/services/readiness/canonical.tsNew files
apps/api/backend/src/services/readiness/canonical.ts—CanonicalBlocker,CanonicalReadiness,CanonicalTrustLevel,derivePassportReadiness,deriveCanonicalTrustLevel,mapLevelToBand,categorizeBlockerand structured-blocker helpersapps/api/backend/src/services/readiness/readinessAdapter.ts— translates canonical output to legacy shapes:toLegacyTrustBand,toLegacyBlockingReasons,toLegacyStringBlockers,toPassportReadinessFields,toTrustStateEngineReadinessapps/api/backend/src/services/readiness/__tests__/canonical.test.ts— 30 tests pinning canonical math + level/band derivationPreserved (no API contract change)
PassportReadinessshape on/api/passport/:npi—{score, readiness_score, level, blockers, gaps, ...}unchangedDeterministicTrustReadinessshape returned bytrustCore— same field setClinicianTrustStateconsumers unaffected —trustStateEngine.deriveTrustBandFromReadinesskeeps same signaturepackages/trust-state(behavior-frozen) is UNTOUCHED — canonical authority lives in the backend, not in the shared legacy packageTest impact
These new scores are what
derivePassportReadinessreturns and what/api/passport/:npialready produces — the test update closes the divergence the convergence audit identified, it does not introduce drift.Truth rules
HARDblocker still zeros the score,SOFTstill caps at 20,gapstill caps at 75 — these are the passportService rules promoted to canonical, not new claims.Validation
pnpm exec jest src/services/readiness/__tests__/canonical.test.ts— 30/30pnpm exec jest src/services/trust/__tests__/trustCore.test.ts— 6/6InputJsonObject/VcvCredentialDomaindrift on origin/main — not introduced by this PR.What's NOT in this PR (per brief constraints)
packages/trust-state(still adapter-stage)