Skip to content

feat(readiness): canonical trust-readiness boundary (P0-P2 convergence)#339

Open
ctol3r wants to merge 1 commit into
mainfrom
wave/trust-convergence-p0-p2
Open

feat(readiness): canonical trust-readiness boundary (P0-P2 convergence)#339
ctol3r wants to merge 1 commit into
mainfrom
wave/trust-convergence-p0-p2

Conversation

@ctol3r
Copy link
Copy Markdown
Owner

@ctol3r ctol3r commented May 12, 2026

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, and TrustPosture because all three now route through one canonical engine.

Convergence boundary

Layer Before After
passportService.buildPassport inline launch-spine-count × 25 math + inline derivePassportReadinessLevel calls derivePassportReadiness
trustCore.computeDeterministicTrustReadiness weighted-confidence (identity 20 + exclusion 30 + licensure 30 + enrollment 20) × confidence formula — INDEPENDENT score builds canonical input, calls derivePassportReadiness, keeps DeterministicTrustReadiness shape
trustCore.deriveTrustBandFromReadiness regex authority on blocker strings (/excluded|license expired|pecos.../i) structural blocker severity via deriveCanonicalTrustLevel
canonical authority DID NOT EXIST apps/api/backend/src/services/readiness/canonical.ts

New files

  • apps/api/backend/src/services/readiness/canonical.tsCanonicalBlocker, CanonicalReadiness, CanonicalTrustLevel, derivePassportReadiness, deriveCanonicalTrustLevel, mapLevelToBand, categorizeBlocker and structured-blocker helpers
  • apps/api/backend/src/services/readiness/readinessAdapter.ts — translates canonical output to legacy shapes: toLegacyTrustBand, toLegacyBlockingReasons, toLegacyStringBlockers, toPassportReadinessFields, toTrustStateEngineReadiness
  • apps/api/backend/src/services/readiness/__tests__/canonical.test.ts — 30 tests pinning canonical math + level/band derivation

Preserved (no API contract change)

  • PassportReadiness shape on /api/passport/:npi{score, readiness_score, level, blockers, gaps, ...} unchanged
  • DeterministicTrustReadiness shape returned by trustCore — same field set
  • ClinicianTrustState consumers unaffected — trustStateEngine.deriveTrustBandFromReadiness keeps same signature
  • packages/trust-state (behavior-frozen) is UNTOUCHED — canonical authority lives in the backend, not in the shared legacy package

Test impact

  • NEW: 30 canonical tests passing
  • UPDATED: 3 trustCore assertions migrated from old weighted-score values to canonical values:
    • all-four-spine-checked: 96 → 100
    • 3/4 checked, licensure stale: 67 → 75
    • 2/4 checked + gated + pending: 48 → 50

These new scores are what derivePassportReadiness returns and what /api/passport/:npi already produces — the test update closes the divergence the convergence audit identified, it does not introduce drift.

Truth rules

  • No banned phrases in changed surfaces (truth-scan CLEAN).
  • Demo / scoring honesty preserved: HARD blocker still zeros the score, SOFT still caps at 20, gap still caps at 75 — these are the passportService rules promoted to canonical, not new claims.

Validation

  • pnpm exec jest src/services/readiness/__tests__/canonical.test.ts30/30
  • pnpm exec jest src/services/trust/__tests__/trustCore.test.ts6/6
  • Broader sweep: 157/157 tests passing across trust/readiness/entity scopes that compile. 6 unrelated pre-existing test suites fail to compile due to Prisma InputJsonObject / VcvCredentialDomain drift on origin/main — not introduced by this PR.

What's NOT in this PR (per brief constraints)

  • No public-API field renames
  • No verifier / replay / frontend UI changes
  • No DID / OID4VCI work
  • No deletion of packages/trust-state (still adapter-stage)

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.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vcv-web Ready Ready Preview, Comment May 12, 2026 5:10am
vitalcv Ready Ready Preview, Comment May 12, 2026 5:10am

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +101 to +102
if (EXCLUSION_RE.test(trimmed)) {
return { id: deterministicId('EXCLUSION', 'HARD', trimmed), message: trimmed, severity: 'HARD', category: 'EXCLUSION' };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +154 to +156
sourceCoverage: [...readiness.blockers].length // placeholder no-op; coverage filled by caller
? []
: [],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants