From 1a0acd478d1d167eb39e807913249faa3f2f3a88 Mon Sep 17 00:00:00 2001 From: "Victor \"David\" Medina" Date: Wed, 1 Jul 2026 14:12:54 -0400 Subject: [PATCH] fix(honesty): hedge proof-ledger copy to readiness at proof_events=0 The header asserted "Every dollar we win back lands here" in present tense, claiming active recovery while proof_events=0 (no real recovered dollar yet). That is a recovered-as-fact overclaim (FTC 5 / substantiation exposure) and contradicts the honesty gate that forbids treating a recovered dollar as fact before First Light. Now state-dependent, honest in every state: - proof_events=0 (loading/empty/Sample): future-tense "will land here" copy plus a DOM-measurable Sample marker (data-proof-state="sample" + a visible "Sample" chip) so a Playwright/regex honesty check can assert readiness. - proof_events>0 (post First Light): the present-tense "lands here" copy is earned and renders (data-proof-state="live"). State is derived from the existing ledger source the component already reads (no new data source). The CSV/JSON export path is untouched. New test asserts the readiness copy + Sample marker render at proof_events=0 and that the present-tense copy returns once real proof exists. No em-dashes in copy. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/proof/ProofLedger.test.tsx | 119 ++++++++++++++++++++++++++ components/proof/ProofLedger.tsx | 30 ++++++- 2 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 components/proof/ProofLedger.test.tsx diff --git a/components/proof/ProofLedger.test.tsx b/components/proof/ProofLedger.test.tsx new file mode 100644 index 00000000..ba89fc35 --- /dev/null +++ b/components/proof/ProofLedger.test.tsx @@ -0,0 +1,119 @@ +/* @vitest-environment jsdom */ + +import { afterEach, describe, expect, it, vi } from "vitest"; +import "@testing-library/jest-dom/vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import React from "react"; + +import { ProofLedger } from "./ProofLedger"; +import type { BuyerProofLedger } from "@/lib/proof-ledger/buyer-proof-ledger"; + +/** + * ProofLedger honesty contract (readiness copy at proof_events=0). + * + * At proof_events=0 the buyer has recovered no real dollar yet, so the header + * MUST read as future/readiness ("will land here") + carry a DOM-measurable + * Sample marker. The present-tense "we win back ... lands here" claim is only + * honest once real proof_events exist. This guards against the recovered-as-fact + * overclaim (FTC 5 / substantiation) the honesty gate forbids while proof=0. + */ + +function makeLedger(proofEvents: number): BuyerProofLedger { + return { + tenant_id: "tenant-1", + owner_approval_policy: { + owner_approval_required: true, + outbound_requires_owner_approval: true, + no_automatic_customer_contact: true, + }, + recovery_numbers_basis: "projected", + billing_live: false, + totals: { + proof_events: proofEvents, + outcome_receipts: 0, + pending_receipts: 0, + wins: 0, + verified_wins: 0, + projected_recovery_cents: 0, + verified_projected_recovery_cents: 0, + blocked_events: 0, + }, + sources: { + supabase: { status: "ok" }, + pulse_blocked_events: { status: "not_configured" }, + }, + proof_events: Array.from({ length: proofEvents }, (_, i) => ({ + id: `event-${i}`, + event_type: "approved", + recovery_item_id: null, + opportunity_id: null, + projected_value_cents: 0, + recovery_numbers_basis: "projected" as const, + owner_approved: true, + outcome_basis: "owner_attested" as const, + created_at: "2026-06-10T00:00:00.000Z", + })), + outcome_receipts: [], + blocked_events: [], + }; +} + +function stubFetchWith(ledger: BuyerProofLedger) { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ ok: true, json: async () => ledger }), + ); +} + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("ProofLedger - SAMPLE state (zero proof_events)", () => { + it("renders readiness (future-tense) copy, not the present-tense recovered claim", async () => { + stubFetchWith(makeLedger(0)); + const { container } = render(); + + await waitFor(() => + expect(container.querySelector('[data-proof-state]')).not.toBeNull(), + ); + + // Future/readiness tense is honest at proof=0: money "will land here". + expect(screen.getByText(/will land here/i)).toBeInTheDocument(); + + // The recovered-as-fact overclaim must be absent while proof_events=0. + expect(container.textContent ?? "").not.toMatch(/we win back[^.]*lands here/i); + }); + + it("carries a DOM-measurable Sample marker for the honesty check", async () => { + stubFetchWith(makeLedger(0)); + const { container } = render(); + + await waitFor(() => + expect( + container.querySelector('[data-proof-state="sample"]'), + ).not.toBeNull(), + ); + // A human-visible Sample chip too (not only a hidden attribute). + expect(screen.getByText(/^Sample$/i)).toBeInTheDocument(); + }); +}); + +describe("ProofLedger - LIVE state (real proof_events)", () => { + it("shows the present-tense recovered-revenue copy once proof_events exist", async () => { + stubFetchWith(makeLedger(1)); + const { container } = render(); + + await waitFor(() => + expect( + container.querySelector('[data-proof-state="live"]'), + ).not.toBeNull(), + ); + + // Present tense is earned once there is a real recovered dollar. + expect(container.textContent ?? "").toMatch(/we win back[^.]*lands here/i); + // And the readiness hedge / Sample chip must be gone. + expect(container.textContent ?? "").not.toMatch(/will land here/i); + expect(screen.queryByText(/^Sample$/i)).not.toBeInTheDocument(); + }); +}); diff --git a/components/proof/ProofLedger.tsx b/components/proof/ProofLedger.tsx index e7a64670..d1891af1 100644 --- a/components/proof/ProofLedger.tsx +++ b/components/proof/ProofLedger.tsx @@ -86,16 +86,38 @@ export function ProofLedger() { if (state === "error") return null; // fail quiet; the rest of the page stands alone + // Honesty gate: the present-tense "lands here" claim asserts we ARE recovering + // dollars. That is only true once a real proof_event exists (First Light). At + // proof_events=0 (loading, empty, or Sample) we hedge to future tense and mark + // the surface as a Sample so a DOM/regex honesty check can assert readiness. + const hasRealProof = state === "ready" && !!ledger && ledger.totals.proof_events > 0; + return ( -
+
-

Your recovered-revenue record

+
+

Your recovered-revenue record

+ {!hasRealProof ? ( + + Sample + + ) : null} +

- Every dollar we win back lands here, owner-approved and timestamped. Yours to keep, - export, and show your accountant. A record no CRM can hand you. + {hasRealProof + ? "Every dollar we win back lands here, owner-approved and timestamped. Yours to keep, export, and show your accountant. A record no CRM can hand you." + : "Every dollar we win back will land here, owner-approved and timestamped. Yours to keep, export, and show your accountant. A record no CRM can hand you."}