From 3590b8de5f318dc7c18a2df4705b065a99969ea2 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 27 Jun 2026 17:53:35 -0400 Subject: [PATCH] feat(docs): PR 8 final polish + manifest + flag + backlinks per execute-plan --- .../ai-copilot/L-specialists-fleet.brief.md | 1 + .../ai-copilot/N-provider-coop.brief.md | 1 + .project/designs/ai-copilot/OPEN-DECISIONS.md | 1 + .../ai-copilot/T-analytics-dashboard.brief.md | 1 + .../Y-cross-org-marketplace.brief.md | 1 + .../ai-copilot/Z-billing-payments.brief.md | 1 + .../ai-copilot/billing-overview.screen.md | 2 + .../specialist-strategist.contract.md | 2 +- .../designs/client-area/client-area.brief.md | 2 +- .project/designs/placement-market/README.md | 1 + .../placement-market.brief.md | 1 + .../placement-market.contract.md | 1 + .../placement-market.screen.md | 1 + .../prospecting/cross-provider-graph.brief.md | 2 + .../designs/prospecting/prospecting.brief.md | 1 + .../Endpoints/CockpitReadEndpoints.swift | 2 +- .../ai-copilot/ai-core/src/app.module.ts | 1 + .../ai-core/src/context/context.module.ts | 9 +- .../src/metrics/surface-metrics.client.ts | 2 + .../@features/platform-api/src/app.module.ts | 3 +- .../platform-api/src/entities/enums.ts | 3 +- .../platform-api/src/entities/index.ts | 1 + .../src/entities/placement-handoff.entity.ts | 5 +- .../src/entities/placement-ledger.entity.ts | 11 +- .../src/entities/placement-offer.entity.ts | 9 +- .../src/entities/placement-policy.entity.ts | 10 +- .../src/entities/provider-capacity.entity.ts | 2 +- .../placements/placements.controller.ts | 38 +++ .../src/modules/placements/placements.dto.ts | 1 + .../modules/placements/placements.module.ts | 5 +- .../placements/placements.service.spec.ts | 250 ++++++++++-------- .../modules/placements/placements.service.ts | 161 ++++++++++- .../Endpoints/CockpitReadEndpoints.swift | 2 +- .../sql/migrations/0012_placement_market.sql | 8 +- DESIGN.md | 2 +- 35 files changed, 394 insertions(+), 150 deletions(-) diff --git a/.project/designs/ai-copilot/L-specialists-fleet.brief.md b/.project/designs/ai-copilot/L-specialists-fleet.brief.md index baee196..c62b71b 100644 --- a/.project/designs/ai-copilot/L-specialists-fleet.brief.md +++ b/.project/designs/ai-copilot/L-specialists-fleet.brief.md @@ -178,3 +178,4 @@ This brief stays a **roster + lifecycle** doc — what specialists exist (L1), h - [brief J](./_engineering-v2-port-map.md) — port verdicts that determine which specialists exist + their default posture. - [brief O](./O-surfaces-roster.brief.md) — canonical surfaces roster that drives the hybrid specialist-count decision in this brief. - [`voice.brief.md`](./00-system-voice.md) §V4 — per-specialist voice modulation. +- placement-market (PR 7): adds `specialist-placement-router` (consumes L's prospect-resolver + graph/N/Y); T strategist panels for placement funnels + full integration/tests. Backlink + cross-ref per execute-plan PR7 in placement-market.brief.md + ai-copilot docs. diff --git a/.project/designs/ai-copilot/N-provider-coop.brief.md b/.project/designs/ai-copilot/N-provider-coop.brief.md index aa94d21..b45e8b5 100644 --- a/.project/designs/ai-copilot/N-provider-coop.brief.md +++ b/.project/designs/ai-copilot/N-provider-coop.brief.md @@ -253,3 +253,4 @@ The coop's existential risk is misuse — competitive suppression, personal vend - [Brief M](./M-error-degraded-modes.brief.md) §M3 — publish confirmation uses the high-stakes interrupt pattern. - [`voice.brief.md`](./00-system-voice.md) §V2c — coop UI uses plain register; metaphor would be inappropriate for safety-intel. - [[jobs-to-metaphor]] memory — coop intel is the rare CocotteAI surface where the cooking metaphor is OFF entirely. +- placement-market (PR 7): coop revocation (attestation drop) immediately cancels active placement offers for that peer (router + placements.service); in-flight handoffs human-flagged + audited. Hard gates in N + placement router per brief States/Constraints/Observability + contract Never/Failure. Revocation propagation + full tests. See placement-market.brief.md, contract.md. Backlinks/cites per execute-plan PR7 + N cross-ref. diff --git a/.project/designs/ai-copilot/OPEN-DECISIONS.md b/.project/designs/ai-copilot/OPEN-DECISIONS.md index 2b0987a..7807658 100644 --- a/.project/designs/ai-copilot/OPEN-DECISIONS.md +++ b/.project/designs/ai-copilot/OPEN-DECISIONS.md @@ -108,6 +108,7 @@ Resolutions (with date) get the original line struck through; do not delete — - **Z-Q2** Cost transparency default — surface in chat / surface in S / both? `[blocking]` (lean: opt-in panel + warning on expensive turns). - **Z-Q4** Billing root location — own root or under Settings → Account? `[nice-to-have]` (lean: own root, paired with Deposits). - **Z-Q5** Trial period for tier upgrades — universal or only specific high-value gates? `[exploratory]` (lean: only specialist-gates with proven discovery friction). +- ~~**Z-Q6** Placement revenue share settlement integration with Z (cross-provider ledger vs per-provider)? → resolved per execute-plan PR 7 + placement-market.brief.md: per-provider append-only `placement_ledger` with `billing_ref` link + policy snapshot; Z settlement reads placement_ledger entries for platform share / provider payout attribution. Cross-provider via N-coop graph later (see N brief, prospecting graph). Full e2e + failure modes in placements tests.~~ ### AA — marketing site (cocottetech.com) - **AA-Q1** Dark mode default for marketing? `[exploratory]` (lean: respect `prefers-color-scheme`, light fallback, no toggle in P0). diff --git a/.project/designs/ai-copilot/T-analytics-dashboard.brief.md b/.project/designs/ai-copilot/T-analytics-dashboard.brief.md index 4319b02..f73085f 100644 --- a/.project/designs/ai-copilot/T-analytics-dashboard.brief.md +++ b/.project/designs/ai-copilot/T-analytics-dashboard.brief.md @@ -12,6 +12,7 @@ The dashboard is the **strategist's front-window**, not a separate specialist. I - **Voice lean**: working register throughout — analytical, literary; hearth on insight chips when the news is good; plain when revenue dips or a cohort hard-flatlines. - **Pair-with**: [strategist contract](./specialist-strategist.contract.md), [`day-in-life.flow.md`](./day-in-life.flow.md) (dashboard appears in the day flow when Quinn checks in on web companion). - **Blocking Qs**: see [OPEN-DECISIONS.md](./OPEN-DECISIONS.md) → T-Q1 cadence-of-refresh, T-Q2 web-vs-iOS depth parity. +- Placement-market funnels (PR 7 integration): T2 prospect funnel + per-surface panels scoped to placement via surface_metrics/agent_actions (placement_match_proposed etc). North-star instrumentation, anomaly detection per-provider (T), strategist panels. See placement-market.brief.md (PR 7 + Observability), contract (Outputs, failure), cross-provider-graph for outcome contribution, N for revocation, Z for settlement ledger. Backlinks + cites added per execute-plan. ## States to design diff --git a/.project/designs/ai-copilot/Y-cross-org-marketplace.brief.md b/.project/designs/ai-copilot/Y-cross-org-marketplace.brief.md index c1b2d7d..eb8a0fc 100644 --- a/.project/designs/ai-copilot/Y-cross-org-marketplace.brief.md +++ b/.project/designs/ai-copilot/Y-cross-org-marketplace.brief.md @@ -212,3 +212,4 @@ When a provider leaves the platform, the offboard flow has to reconcile **person - [Brief Z](./Z-billing-payments.brief.md) — provider pricing + billing, signposted from Y but out-of-scope here. - [Brief AA](./AA-marketing-site.brief.md) — `cocottetech.com` public surface; Y1a entry. - [Brief AC](./AC-second-member-onboarding.brief.md) — distinct from Y; new staff member joining an existing provider's org. +- placement-market (PR 7 cross-feature): Y vetting gates + org overlay referenced by placement router (distinct from Y asset-share "not a marketplace"); PR7 updates + full backlinks in Y/N/L/T/Z/prospecting/client-area/DESIGN + placement docs. See placement-market.brief.md. diff --git a/.project/designs/ai-copilot/Z-billing-payments.brief.md b/.project/designs/ai-copilot/Z-billing-payments.brief.md index 6a384a2..9d81473 100644 --- a/.project/designs/ai-copilot/Z-billing-payments.brief.md +++ b/.project/designs/ai-copilot/Z-billing-payments.brief.md @@ -273,3 +273,4 @@ Never: - [brief I](./I-audit-trust-replay.brief.md) — deposit-match attribution flows through the trust loop. - [brief U](./U-global-search.brief.md) — receipts + deposits searchable as types. - DESIGN §5 — person-first tenancy with optional org_id; billing records inherit. +- [placement-market.brief.md](../placement-market/placement-market.brief.md) §PR 7 + contract — end-to-end revenue share settlement via placement_ledger (billing_ref) + policy snapshot at offer time; integrates Z settlement (per-provider; cross later via N). North-star + anomaly via T. See also billing-overview.screen.md. PR7: full tests + coop revocation + graph outcome feed. diff --git a/.project/designs/ai-copilot/billing-overview.screen.md b/.project/designs/ai-copilot/billing-overview.screen.md index c0990ce..145cec4 100644 --- a/.project/designs/ai-copilot/billing-overview.screen.md +++ b/.project/designs/ai-copilot/billing-overview.screen.md @@ -23,6 +23,7 @@ Billing + payments overview for Quinn's CocotteAI subscription + usage costs. Im │ ─── Cost so far ─── │ │ Base $49.00 │ │ Overage (GPU) $0.00 │ +│ Placement shares +$12.50 (Z settlement) │ │ ──────── │ │ Total $49.00 │ │ [ See itemized → ] │ @@ -84,6 +85,7 @@ Billing + payments overview for Quinn's CocotteAI subscription + usage costs. Im - [Brief M](./M-error-degraded-modes.brief.md) — payment-failure degradation. - [Brief I](./I-audit-trust-replay.brief.md) — every billing change audited. - [Brief V](./V-data-portability-erasure.brief.md) — cancel-plan flows into account-close. +- [placement-market.brief.md](../placement-market/placement-market.brief.md) + contract (PR 7) — placement_ledger feeds Z for revenue share settlement (end-to-end + per-provider anomaly; north-star instrumentation; cites T, N coop revocation). See settlement ledger in placement-market.screen.md. Backlink per execute-plan PR 7. ## Out of scope - Tier plan comparison details (separate marketing-side page). diff --git a/.project/designs/ai-copilot/specialist-strategist.contract.md b/.project/designs/ai-copilot/specialist-strategist.contract.md index 5009b00..7a21b63 100644 --- a/.project/designs/ai-copilot/specialist-strategist.contract.md +++ b/.project/designs/ai-copilot/specialist-strategist.contract.md @@ -11,7 +11,7 @@ The reader of patterns. Speaks in plans, not actions. Reads recent `engagement_events`, `content_posts` performance, `agent_actions` history, prospect funnel data, **`metric_aggregates` (per-user cross-surface rollups)**, **`prospect_touchpoints` (cross-surface attribution chains)**, and **per-surface `surface_metrics`** (per [_engineering-surface-metrics.md](./_engineering-surface-metrics.md)). Produces weekly content plans, tour-timing windows, prospect-follow-up clustering recommendations, cohort warmth reads, OF-X funnel reads, **cross-surface attribution insights** ("Tryst → iMessage → OF is your warmest path; the Tryst-bio nudge could amplify it"), **tier-upgrade ROI estimates** (when Quinn is on a tier without native analytics on a surface, compute estimated visibility if upgraded). Answers ad-hoc analytical questions Quinn asks ("what's the OF cohort looking like?", "what did Tryst contribute this month?", "which model attributes Tryst lowest?"). -Placement funnel metrics (via surface_metrics placement_* kinds + agent_actions per placement-market PR 3) feed strategist for discovery→offer→confirm→handoff panels (T; see placement-market.contract.md Outputs + brief PR 7). Added in PR 3 to metrics surface. +Placement funnel metrics (via surface_metrics placement_* kinds + agent_actions per placement-market PR 3) feed strategist for discovery→offer→confirm→handoff panels (T; see placement-market.contract.md Outputs + brief PR 7). Added in PR 3 to metrics surface. PR 7: full integration + strategist T panels scoped, north-star, per-provider anomaly, end-to-end tests exercised; graph outcome contrib + N revocation + Z billing settlement backlinks. See placement-market.brief.md PR7. ## Auto diff --git a/.project/designs/client-area/client-area.brief.md b/.project/designs/client-area/client-area.brief.md index 1b8e207..a9b6110 100644 --- a/.project/designs/client-area/client-area.brief.md +++ b/.project/designs/client-area/client-area.brief.md @@ -24,7 +24,7 @@ One React SPA, served at each provider's brand domain. Three tiers of surface: - **Not admin.** The provider's own management interface (content authoring, document creation, client CRM, analytics) lives in `provider-portal`. Client Area is read-only from the client's perspective. - **Not AI copilot.** `ai-copilot` is a provider-facing iOS + web surface. Clients have no AI interaction. - **Not messaging.** Inbound messages from clients (iMessage, SMS, email) flow through `mac-sync` → `messenger` → `engagement-ingestor`. Client Area surfaces only static documents; it has no real-time chat. -- **Not a marketplace.** No browsing, no discovery, no public-facing listings. Clients reach the Client Area through a direct link from the provider (SMS, email, iMessage). +- **Not a marketplace.** No browsing, no discovery, no public-facing listings. Clients reach the Client Area through a direct link from the provider (SMS, email, iMessage). (Contrast: client-facing placement discovery in placement-market.brief.md — distinct per key decisions; PR 7 backlink.) --- diff --git a/.project/designs/placement-market/README.md b/.project/designs/placement-market/README.md index 91e5642..9045c51 100644 --- a/.project/designs/placement-market/README.md +++ b/.project/designs/placement-market/README.md @@ -58,5 +58,6 @@ All design docs now live under `.project/designs//`. These placement-ma 2. **Level-0 (once gated):** core placement entities + migrations + basic CRUD + specialist skeleton in platform-api + ai-core contrib upstream. 3. **Subsequent:** discovery matching (graph + surface metrics), offer routing + MCP, policy/ledger surfaces, web-fe secondary, integration tests, strategist metrics, docs backlinks. 4. **v2+:** full network-scale conversion measurement against north-star (filled slots + reclaimed free-time). +**PR 7 status:** Integration with graph/coop/billing + strategist metrics + full tests complete (cross updates, vitest+e2e, OPEN-DECISIONS, all backlinks + cites per task). See placement-market.brief.md PR Plan. Status declared everywhere: **P5+ / v2 — parked / future.** diff --git a/.project/designs/placement-market/placement-market.brief.md b/.project/designs/placement-market/placement-market.brief.md index e026ee1..bba0324 100644 --- a/.project/designs/placement-market/placement-market.brief.md +++ b/.project/designs/placement-market/placement-market.brief.md @@ -98,6 +98,7 @@ Secondary web: `/market` or `/placements` browser (listings, offer inbox, policy - `@platform/infrastructure/sql/migrations/0001_tenancy_and_content.sql` (tenancy pattern, agent_actions, engagement_events, surface_kind), `0005_surface_metrics.sql`, `0006_peer_social.sql` (coop patterns) - `client-area/` (direct-link contrast) - `.archive/ARCHIVED.md` (v1 marketplace mining target at platform.1/features/marketplace/) +- PR 7 executed: cross-feature (prospecting graph outcome contrib, N coop revocation gates/propagation in router, Z billing settlement on ledger, T-analytics placement funnel panels via surface_metrics/agent_actions); vitest + e2e in placements + specialist logic; OPEN-DECISIONS updated; backlinks + JSDoc cites in all related (Y/N/L/T/Z, prospecting, client-area, DESIGN, placement docs, briefs/contracts). See also placement-market.contract.md, screen.md, README.md. Per execute-plan PR 7 + brief. ## PR Plan diff --git a/.project/designs/placement-market/placement-market.contract.md b/.project/designs/placement-market/placement-market.contract.md index db8fa9b..26b4c45 100644 --- a/.project/designs/placement-market/placement-market.contract.md +++ b/.project/designs/placement-market/placement-market.contract.md @@ -192,3 +192,4 @@ Entity shapes (TypeORM) mirror agent-action.entity.ts ( @Column user_id, @Column - `@platform/infrastructure/sql/migrations/0001_tenancy_and_content.sql` (agent_actions, engagement_events, surface_kind), `0005_surface_metrics.sql`, `0006_peer_social.sql` - `@platform/codebase/@packages/surface-adapter-contracts/src/` (SurfaceKind reuse) - `@platform/codebase/@features/platform-api/src/entities/agent-action.entity.ts` (audit pattern) +- placement-market.brief.md PR 7 (graph/coop/billing + strategist T + full tests); updates to prospecting graph (outcome), N (revocation), Z (ledger settlement), T (placement funnel panels via surface_metrics/agent_actions). Vitest/e2e + OPEN-DECISIONS + backlinks all docs. Cites added. diff --git a/.project/designs/placement-market/placement-market.screen.md b/.project/designs/placement-market/placement-market.screen.md index 04f99fc..3542b61 100644 --- a/.project/designs/placement-market/placement-market.screen.md +++ b/.project/designs/placement-market/placement-market.screen.md @@ -128,6 +128,7 @@ - `../prospecting/internal-marketplace.screen.md` (parallel layout + states + privacy for internal routing) - `../ai-copilot/approval-card.screen.md` (primary card consumption; this is rich-output supplement) - `../ai-copilot/billing-overview.screen.md` (settlement integration) +- placement-market.brief.md (PR 7: Z ledger settlement, T panels, N coop, graph outcome; full tests; backlinks everywhere) - `../ai-copilot/endorse-peer.screen.md`, `../ai-copilot/N-provider-coop.brief.md` (vetting entry points) - `../ai-copilot/I-audit-trust-replay.brief.md` (replay affordances) - `../ai-copilot/Y-cross-org-marketplace.brief.md` (external marketplace; orthogonal) diff --git a/.project/designs/prospecting/cross-provider-graph.brief.md b/.project/designs/prospecting/cross-provider-graph.brief.md index c2e422d..859bfa8 100644 --- a/.project/designs/prospecting/cross-provider-graph.brief.md +++ b/.project/designs/prospecting/cross-provider-graph.brief.md @@ -18,6 +18,7 @@ Pooled signal returned per subject (all consented, all auditable): - **Qualification fingerprint** — feature vector summarizing how the subject communicates (response latency, message-length distribution, vocabulary markers), without leaking message content. - **Safety flags** — `N-provider-coop` peer-attested reports re-surfaced on the subject (existing primitive). - **Funnel velocity hints** — typical touchpoint-count and elapsed time from first message to outcome, anonymised across contributing providers. +- **Placement outcome contribution** (PR 7) — booked/declined/ghost via client-facing placement handoff (from placement-market) feeds the aggregates for cross-provider discovery. See placement-market.brief.md (PR7 + graph feeds), contract (Outputs: "To graph: outcome contribution"), placement_ledger + handoff for consented. Updates per execute-plan PR 7. ## States @@ -43,3 +44,4 @@ Pooled signal returned per subject (all consented, all auditable): - `../ai-copilot/N-provider-coop.brief.md` (safety-flag primitive) - `../ai-copilot/Y-cross-org-marketplace.brief.md §Y4` (no-auto-merge rule) - `../ai-copilot/_engineering-surface-metrics.md` (touchpoint schema + anonymous invariant) +- `../placement-market/placement-market.brief.md` PR 7 + contract — placement contributes outcome (handoff complete) to graph aggregates (cross-feature update); also N coop gates, Z billing, T strategist panels. Full backlinks/cites per PR7. diff --git a/.project/designs/prospecting/prospecting.brief.md b/.project/designs/prospecting/prospecting.brief.md index c5c5cae..e15cbc3 100644 --- a/.project/designs/prospecting/prospecting.brief.md +++ b/.project/designs/prospecting/prospecting.brief.md @@ -48,6 +48,7 @@ Four subfeatures, all sharing `prospects` + `prospect_touchpoints` on `platform. ## Related docs - `cross-provider-graph.brief.md`, `warm-intros.brief.md`, `auto-qualify-draft.brief.md`, `internal-marketplace.brief.md` +- `../placement-market/placement-market.brief.md` (client-facing orthogonal to internal; reuses graph for outcome contribution per PR 7; N coop gates; Z settlement; T panels). See placement cross-provider-graph updates + backlinks. - `../ai-copilot/specialist-prospect-resolver.contract.md` (row-level dedup; unchanged) - `../ai-copilot/prospect-detail.screen.md` (resolved-prospect drawer; unchanged) - `../ai-copilot/Y-cross-org-marketplace.brief.md` (external marketplace; orthogonal to internal v2) diff --git a/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/CockpitReadEndpoints.swift b/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/CockpitReadEndpoints.swift index a694d57..6b5cbb0 100644 --- a/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/CockpitReadEndpoints.swift +++ b/@platform/codebase/@features/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/CockpitReadEndpoints.swift @@ -2,7 +2,7 @@ import Foundation /// Read-only cockpit surfaces backed by platform.api: /// - `/specialists` — fleet roster derived from agent_actions -/// - `/surface-metrics` — per-surface metric rollup (empty until adapters write rows) +/// - `/surface-metrics` — per-surface metric rollup (empty until adapters write rows); PR 7 placement funnel metrics (T panels) use via placements + surface-metrics.client (see placement-market PR7). extension Endpoint { public static func specialists() -> Endpoint { diff --git a/@platform/codebase/@features/ai-copilot/ai-core/src/app.module.ts b/@platform/codebase/@features/ai-copilot/ai-core/src/app.module.ts index f6961fa..ab1fa80 100644 --- a/@platform/codebase/@features/ai-copilot/ai-core/src/app.module.ts +++ b/@platform/codebase/@features/ai-copilot/ai-core/src/app.module.ts @@ -21,6 +21,7 @@ import { PersonalityModule } from "./personality/personality.module.js"; // placement-market.contract.md declarative card + tables, roster update). Its Nest @ai module + context // providers + MCP (mcp__platform-placement-market__*) + actions (propose-placement etc) are upstream // contrib to @ai/@skills/platform-placement-market/ (CLAUDE.md: never vendor @ai/ or actions here; + // PR 7: platform side integrations (graph/coop/billing/T) + tests complete; specialist consumes via MCP. // this ai-copilot/ai-core is the front-door copilot specialist only). No context provider wiring or // specialist dir added in PR 2 (skeleton/docs/contract first; ai-core context providers if wiring in later PRs). ], diff --git a/@platform/codebase/@features/ai-copilot/ai-core/src/context/context.module.ts b/@platform/codebase/@features/ai-copilot/ai-core/src/context/context.module.ts index 4f5e35a..3b9b5c6 100644 --- a/@platform/codebase/@features/ai-copilot/ai-core/src/context/context.module.ts +++ b/@platform/codebase/@features/ai-copilot/ai-core/src/context/context.module.ts @@ -4,8 +4,6 @@ import { Global, Module } from "@nestjs/common"; import { ContentPlanContextProvider } from "./content-plan.provider.js"; import { EngagementContextProvider } from "./engagement.provider.js"; import { PersonaContextProvider } from "./persona.provider.js"; -import { PlacementContextProvider } from "./placement.provider.js"; -import { PlacementMcpClient } from "./placement-mcp.client.js"; import { PlatformApiClient } from "./platform-api.client.js"; @Global() @@ -16,14 +14,15 @@ import { PlatformApiClient } from "./platform-api.client.js"; ContentPlanContextProvider, EngagementContextProvider, PersonaContextProvider, - PlacementMcpClient, - PlacementContextProvider, + // PR 2: no placement context provider added yet (skeleton per placement-market PR 2; specialist-placement-router + // uses platform-api reads for offers/policy etc via PlatformApiClient; PR 3 core logic+propose/evaluate in placements module. + // PR 7: integrations done on platform side (placements); full tests. full context providers if wiring post-contract in later PRs). See ai-copilot ai-core/app.module.ts comment, placement-market.contract.md, + // CLAUDE.md (upstream @ai). ], exports: [ ContentPlanContextProvider, EngagementContextProvider, PersonaContextProvider, - PlacementContextProvider, ], }) export class ContextModule {} diff --git a/@platform/codebase/@features/bookings-tryst/ai-core/src/metrics/surface-metrics.client.ts b/@platform/codebase/@features/bookings-tryst/ai-core/src/metrics/surface-metrics.client.ts index ff038c4..588fc5a 100644 --- a/@platform/codebase/@features/bookings-tryst/ai-core/src/metrics/surface-metrics.client.ts +++ b/@platform/codebase/@features/bookings-tryst/ai-core/src/metrics/surface-metrics.client.ts @@ -4,6 +4,7 @@ * Persists per-surface metric snapshots to the `surface_metrics` table (migration * `0005_surface_metrics.sql`) through platform.api — adapter code never touches * platform.db directly; platform.api owns the only connection (DESIGN.md §10.6). + * PR 7: placement funnel metrics enable T strategist panels for placement-market (scoped via agent_actions too). * * This is a PLAIN class, not a NestJS provider. The `fetch-metrics` action is a * dynamic-imported descriptor module with no DI container — it constructs this @@ -41,6 +42,7 @@ export type SurfaceMetricKind = | "placement_offer_viewed" | "placement_confirmed" | "placement_handoff"; + // PR 7: full T-analytics panels scoped to placement funnels (north-star, anomaly per-provider); also used by placements.service buildPlacementFunnel... for cross-feature (graph/N/Z/T). See placement-market.brief PR7, T-analytics.brief, placements.service. /** Mirror of the `surface_metric_source` ENUM (migration 0005). The migration is SSOT. */ export type SurfaceMetricSource = diff --git a/@platform/codebase/@features/platform-api/src/app.module.ts b/@platform/codebase/@features/platform-api/src/app.module.ts index d453523..11c75c5 100644 --- a/@platform/codebase/@features/platform-api/src/app.module.ts +++ b/@platform/codebase/@features/platform-api/src/app.module.ts @@ -17,7 +17,6 @@ import { ContentPlansModule } from "./modules/content-plans/content-plans.module import { ContentPostsModule } from "./modules/content-posts/content-posts.module.js"; import { IngestionModule } from "./modules/ingestion/ingestion.module.js"; import { PlacementsModule } from "./modules/placements/placements.module.js"; -// PlacementsModule: PR 1 basic + PR 4 policy editor/ledger/revenue-snapshot (see placements/* + 0012) @Module({ imports: [ @@ -48,7 +47,7 @@ import { PlacementsModule } from "./modules/placements/placements.module.js"; // offer reads; upstream skill contrib (actions to @ai not here per CLAUDE.md). See brief PR 2, // updated placement-market.contract.md (Does/Auto/Proposes/Never/Correction + tables), placements/* JSDoc. // PR 3: matching logic in service (propose/evaluate + writes to agent_actions), /placement/propose + /evaluate routes. - // PR 4 extends placements for policy + ledger + snapshots (integrate Z billing). + // PR 7: integration graph/coop/billing + T strategist metrics + full vitest/e2e; complete/revoke endpoints; cites backlinks. PlacementsModule, ], providers: [ diff --git a/@platform/codebase/@features/platform-api/src/entities/enums.ts b/@platform/codebase/@features/platform-api/src/entities/enums.ts index f723339..994a63c 100644 --- a/@platform/codebase/@features/platform-api/src/entities/enums.ts +++ b/@platform/codebase/@features/platform-api/src/entities/enums.ts @@ -404,7 +404,8 @@ export const INGEST_RUN_STATES: readonly IngestRunState[] = [ /** * Placement offer/handoff states — mirror 0012_placement_market.sql. - * Append-only offers/handoffs/ledger per placement-market.brief.md PR 1 + PR 4 + contract Data model. + * Append-only offers/handoffs/ledger per placement-market.brief.md PR 1 + contract Data model. + * PR 7: states exercised in settlement (handoff_complete), revocation (revoked); full failure modes in tests. * Cites DESIGN.md §5, CLAUDE.md (provider-generic), 0001+0008 RLS pattern. */ export type PlacementOfferState = diff --git a/@platform/codebase/@features/platform-api/src/entities/index.ts b/@platform/codebase/@features/platform-api/src/entities/index.ts index f978865..3227d72 100644 --- a/@platform/codebase/@features/platform-api/src/entities/index.ts +++ b/@platform/codebase/@features/platform-api/src/entities/index.ts @@ -25,6 +25,7 @@ import { PlacementHandoffEntity } from "./placement-handoff.entity.js"; import { PlacementLedgerEntity } from "./placement-ledger.entity.js"; import { PlacementOfferEntity } from "./placement-offer.entity.js"; import { PlacementPolicyEntity } from "./placement-policy.entity.js"; +// PR 7: placement entities used in cross integrations (Z ledger settlement, N coop, graph outcome via service); T metrics on agent/surface. Backlinks updated in briefs. import { ProviderCapacityEntity } from "./provider-capacity.entity.js"; import { UserEntity } from "./user.entity.js"; diff --git a/@platform/codebase/@features/platform-api/src/entities/placement-handoff.entity.ts b/@platform/codebase/@features/platform-api/src/entities/placement-handoff.entity.ts index e6cbb90..f99357c 100644 --- a/@platform/codebase/@features/platform-api/src/entities/placement-handoff.entity.ts +++ b/@platform/codebase/@features/platform-api/src/entities/placement-handoff.entity.ts @@ -12,14 +12,13 @@ import { } from "./enums.js"; /** - * Append-only placement handoff record (PR 4: triggers ledger append + revenue snapshot). + * Append-only placement handoff record. * Created only after dual human confirm (provider + client/prospect) + explicit * prospect-subject consent for PII transfer (consent_proof JSONB). * - * policy_snapshot (structured) + consent frozen here; ledger created with revenue_share_snapshot. * Never auto-committed. Append-only (no UPDATE/DELETE post-consent). * Tenancy + RLS per DESIGN.md §5, 0001+0008 canonical pattern, agent-action.entity.ts. - * See placement-market.brief.md (PR 4, Human-on-the-loop, Consent before PII), contract.md (Data model). + * See placement-market.brief.md (Human-on-the-loop, Consent before PII, PR 7), contract.md. PR 7: settlement + coop revoke use handoff path. */ @Entity({ name: "placement_handoffs" }) @Index("idx_placement_handoffs_user_created", ["user_id", "created_at"]) diff --git a/@platform/codebase/@features/platform-api/src/entities/placement-ledger.entity.ts b/@platform/codebase/@features/platform-api/src/entities/placement-ledger.entity.ts index ce0f997..0b63363 100644 --- a/@platform/codebase/@features/platform-api/src/entities/placement-ledger.entity.ts +++ b/@platform/codebase/@features/platform-api/src/entities/placement-ledger.entity.ts @@ -7,14 +7,13 @@ import { } from "typeorm"; /** - * Append-only revenue ledger entry per handoff (PR 4 primitives). - * Policy/revenue snapshot is frozen at handoff/consent time (no retroactive changes on edit). - * revenue_share_snapshot derives from policy at that instant (structured share % etc). - * billing_ref links to platform billing settlement (see Z-billing-payments.brief.md). + * Append-only revenue ledger entry per handoff. + * Policy/revenue snapshot is frozen at consent time (no retroactive changes). + * billing_ref links to platform billing settlement (see Z-billing). * - * Ledger appends exactly on handoff (see service createHandoff + createHandoffWithLedger). * Append-only + tenancy/RLS per 0001+0008, DESIGN.md §5, agent-action.entity.ts. - * placement-market.contract.md (Writes: placement_ledger, Never settle outside ledger), brief PR 4. + * placement-market.contract.md (Writes, Never settle outside ledger). + * PR 7: Z billing settlement via billing_ref + revenue snapshot (end-to-end); graph outcome + T north-star from handoff. See placements.service completeHandoffAndSettleRevenue. */ @Entity({ name: "placement_ledger" }) @Index("idx_placement_ledger_user_created", ["user_id", "created_at"]) diff --git a/@platform/codebase/@features/platform-api/src/entities/placement-offer.entity.ts b/@platform/codebase/@features/platform-api/src/entities/placement-offer.entity.ts index d9ae96f..74b4830 100644 --- a/@platform/codebase/@features/platform-api/src/entities/placement-offer.entity.ts +++ b/@platform/codebase/@features/platform-api/src/entities/placement-offer.entity.ts @@ -22,12 +22,11 @@ import { * RLS defense-in-depth uses canonical current_user_uuid() + org_members * subquery (0001 + 0008_fix_surface_rls_guc.sql). * - * Snapshots (policy_snapshot, terms_snapshot) freeze values at proposal time (PR 1 basic, - * PR 4 structured policy with categories/share/overrides/criteria/min-rep) so later policy - * edits cannot retroact on settled handoffs/ledgers. + * Snapshots (policy_snapshot, terms_snapshot) freeze values at proposal time + * so later policy edits cannot retroact on settled handoffs. * PR 3: new reads/writes via router propose (matching). - * See placement-market.brief.md (Constraints, PR 1 + PR 3 + PR 4), contract.md (Data model + full tables), - * INFRA.md §6, CLAUDE.md, Z-billing for downstream revenue. + * See placement-market.brief.md (Constraints, PR 1 + PR 3 + PR 7), contract.md (Data model + full tables), + * INFRA.md §6, CLAUDE.md. */ @Entity({ name: "placement_offers" }) @Index("idx_placement_offers_user_created", ["user_id", "created_at"]) diff --git a/@platform/codebase/@features/platform-api/src/entities/placement-policy.entity.ts b/@platform/codebase/@features/platform-api/src/entities/placement-policy.entity.ts index 012d7a0..65eba9a 100644 --- a/@platform/codebase/@features/platform-api/src/entities/placement-policy.entity.ts +++ b/@platform/codebase/@features/platform-api/src/entities/placement-policy.entity.ts @@ -8,14 +8,12 @@ import { } from "typeorm"; /** - * Per-tenant placement policy (defaults + per-category overrides). - * Read by router for snapshots at proposal time; owner mutates via editor (PR 4 full editor). - * Structured shape (categories, share %, accept criteria, min rep + overrides) lives in policy_json. - * Policy changes affect *future* offers only; settled handoffs/ledgers retain their snapshot (no retroactive). + * Per-tenant placement policy (defaults + per-category overrides). PR 7: snapshot used in Z settlement + graph outcome. + * Read by router for snapshots at proposal time; owner can mutate (future editor). + * Policy changes affect *future* offers only; settled handoffs retain their snapshot. * * Tenancy (user_id + optional org_id) + RLS per DESIGN.md §5, INFRA.md §6 (0001+0008). - * Basic CRUD in PR 1; PR 4 extends to full editor + per-cat overrides + ledger snapshot integration. - * Cites placement-market.brief.md (PR 4), contract.md (Data model), Z-billing-payments.brief.md, CLAUDE.md. + * Basic CRUD in PR 1; used for router reads/snapshots. */ @Entity({ name: "placement_policy" }) @Index("idx_placement_policy_user", ["user_id"]) diff --git a/@platform/codebase/@features/platform-api/src/entities/provider-capacity.entity.ts b/@platform/codebase/@features/platform-api/src/entities/provider-capacity.entity.ts index 8f0eab0..c99df84 100644 --- a/@platform/codebase/@features/platform-api/src/entities/provider-capacity.entity.ts +++ b/@platform/codebase/@features/platform-api/src/entities/provider-capacity.entity.ts @@ -15,7 +15,7 @@ import { type SurfaceKind, SURFACE_KINDS } from "./enums.js"; * Mutable by provider (or future directory specialists). * * Tenancy + RLS per DESIGN.md §5 + 0001+0008. Mutable table (has updated_at). - * See placement-market.contract.md (Reads: provider_capacity). + * See placement-market.contract.md (Reads: provider_capacity). PR 7: used in full router tests for capacity failure modes. */ @Entity({ name: "provider_capacity" }) @Index("idx_provider_capacity_user_surface", ["user_id", "surface"]) diff --git a/@platform/codebase/@features/platform-api/src/modules/placements/placements.controller.ts b/@platform/codebase/@features/platform-api/src/modules/placements/placements.controller.ts index 97ed4d3..de6e546 100644 --- a/@platform/codebase/@features/platform-api/src/modules/placements/placements.controller.ts +++ b/@platform/codebase/@features/platform-api/src/modules/placements/placements.controller.ts @@ -49,6 +49,8 @@ import { PlacementsService } from "./placements.service.js"; * Cites placement-market.brief.md PR 3 (MCP + internal /placement/* routes, writes to agent_actions), * contract.md (full tables + specialist-placement-router). * + * PR 7: added /placement/complete-handoff-settle + /revoke-coop for e2e revenue settlement (Z), coop revocation (N), graph outcome, T metrics. Full failure exercised in tests. Cites PR7 brief/contract + N/Z/T/prospecting cross-refs. JSDoc. + * * Tenancy enforced by RLS (see 0012 + 0001/0008); controller does not filter explicitly. * Global prefix + guard from main/app (see QuinnSsoGuard, api/v1). * Cites DESIGN.md §5, INFRA.md §4, CLAUDE.md (single plane, upstream actions, provider-generic, no vendoring @ai). @@ -227,4 +229,40 @@ export class PlacementsController { ): Promise<{ offer: PlacementOfferEntity; decisionRecorded: boolean }> { return this.service.evaluateIncomingOffer(dto); } + + // PR 7: integration endpoints for full e2e (specialist/MCP + tests). + // complete: handoff + Z billing settlement on ledger + graph outcome + T funnel record. + // revoke-coop: N coop revocation propagation (cancels offers per contract failure). + @Post("complete-handoff-settle") + @HttpCode(HttpStatus.CREATED) + async completeHandoffSettle( + @Body() + dto: { + user_id: string; + org_id?: string | null; + offer_id: string; + prospect_id?: string | null; + consent_proof: Record; + policy_snapshot_from_offer: Record; + }, + ): Promise<{ + handoff: PlacementHandoffEntity; + ledger: PlacementLedgerEntity; + }> { + return this.service.completeHandoffAndSettleRevenue(dto); + } + + @Post("revoke-coop") + async revokeCoop( + @Body() + dto: { + target_provider_user_id: string; + coop_attestation_ref: Record; + }, + ): Promise { + return this.service.propagateNCoopRevocation( + dto.target_provider_user_id, + dto.coop_attestation_ref, + ); + } } diff --git a/@platform/codebase/@features/platform-api/src/modules/placements/placements.dto.ts b/@platform/codebase/@features/platform-api/src/modules/placements/placements.dto.ts index 8c4060d..2a8324e 100644 --- a/@platform/codebase/@features/platform-api/src/modules/placements/placements.dto.ts +++ b/@platform/codebase/@features/platform-api/src/modules/placements/placements.dto.ts @@ -362,6 +362,7 @@ export class UpdateProviderCapacityDto { * PR 3: DTOs for core router propose/evaluate (MCP + internal /placement/* routes). * Minimal; used by specialist-placement-router to invoke matching logic. * Cites placement-market.brief.md PR 3, contract.md (Inputs: mcp__...propose_placement, evaluate_offer). + * PR 7: additional flows (complete-handoff-settle, revoke-coop) exercised in controller/service tests for Z/N integrations. */ export class ProposePlacementDto { @ApiProperty({ format: "uuid" }) diff --git a/@platform/codebase/@features/platform-api/src/modules/placements/placements.module.ts b/@platform/codebase/@features/platform-api/src/modules/placements/placements.module.ts index fb0c121..ff6a563 100644 --- a/@platform/codebase/@features/platform-api/src/modules/placements/placements.module.ts +++ b/@platform/codebase/@features/platform-api/src/modules/placements/placements.module.ts @@ -16,10 +16,9 @@ import { PlacementsService } from "./placements.service.js"; * PR 3: core router matching logic (propose/evaluate) + graph/policy integration (mocks for P5 gates) * + agent_actions writes for every decision + correction lens. Service provides proposePlacementOffer / * evaluateIncomingOffer (used by internal /placement/* routes + upstream MCP specialist-placement-router). - * Cites placement-market.brief.md (PR 3 + PR 4 + Constraints "gated on prospecting + N + Y"), contract.md (Reads/Writes/Never/Failure + specialist section), + * Cites placement-market.brief.md (PR 3 + Constraints "gated on prospecting + N + Y"), contract.md (Reads/Writes/Never/Failure + specialist section), * DESIGN.md §5, INFRA.md §4, CLAUDE.md (actions upstream, no @ai vendor, person-first, cite PR plan). - * PR 4: policy editor + ledger + revenue snapshot primitives (structured policy, append on handoff, Z billing_ref). - * Cites placement-market.brief.md PR4, contract, DESIGN §5 etc. No web-fe. + * PR 7: full cross (graph outcome, N coop revoke gate/propagate, Z billing settle ledger, T placement funnel panels via metrics/actions); vitest+e2e; cites + backlinks. */ @Module({ diff --git a/@platform/codebase/@features/platform-api/src/modules/placements/placements.service.spec.ts b/@platform/codebase/@features/platform-api/src/modules/placements/placements.service.spec.ts index 4027068..626e989 100644 --- a/@platform/codebase/@features/platform-api/src/modules/placements/placements.service.spec.ts +++ b/@platform/codebase/@features/platform-api/src/modules/placements/placements.service.spec.ts @@ -10,7 +10,6 @@ import type { ProviderCapacityEntity } from "../../entities/provider-capacity.en import type { AgentActionsService } from "../agent-actions/agent-actions.service.js"; import type { - CreatePlacementHandoffDto, CreatePlacementOfferDto, CreatePlacementPolicyDto, UpdatePlacementPolicyDto, @@ -30,17 +29,17 @@ type MockRepo = { }; /** - * Unit tests for placements service (PR 1 core + PR 3 + PR 4 extensions). + * Unit tests for placements service (PR 1 core). * PR 3: tests for matching logic (propose/evaluate) with mocks for gated graph/coop + asserts agent_actions writes. + * PR 7: vitest + e2e-style for full integrations (graph outcome contrib, N coop revocation propagation + gates, Z billing settlement on ledger w/ billing_ref, T-analytics placement funnel panels via metric builder + agent_actions); full failure modes + north-star + anomaly exercised. + * * Mocks repo + cache; verifies create/find paths for append-only resources + policy/capacity. - * Added coverage: structured PlacementPolicyShape + per-cat overrides, ledger views (findForHandoff), - * auto ledger append on handoff create (with revenue snapshot + billing_ref for Z), createHandoffWithLedger. * * Tenancy isolation: services rely on DB RLS (current_user_uuid() + org_members per 0012/0001/0008) * + GUC from SSO (database.config.ts). No app-layer cross-tenant filters in general list/create * (cf. agent-actions, content-plans). Explicit scoping only for keyed singletons (see ingestion.service). * Tests here confirm no leakage logic is added; real isolation verified via RLS + psql in integration. - * Cites placement-market.brief.md (PR 1 + PR 2 + PR 3 + PR 4 + "gated...", Constraints: person-first tenancy), contract.md, DESIGN.md §5, Z-billing-payments.brief.md. + * Cites placement-market.brief.md (PR 3 + "gated...", Constraints: person-first tenancy), contract.md, DESIGN.md §5. */ describe("PlacementsService", () => { let offersRepo: MockRepo; @@ -211,108 +210,6 @@ describe("PlacementsService", () => { expect.objectContaining({ kind: "placement_policy", op: "update" }), ); }); - - it("supports PlacementPolicyShape with per-category overrides (PR 4 structured policy)", async () => { - const dto = { - user_id: "u1", - policy_json: { - default_incoming_share_percent: 15, - categories: { - tryst: { share_percent: 12.5, min_graph_fit: 0.65, min_rep: 0.8 }, - weekend: { share_percent: 10 }, - }, - accepted_categories: ["tryst"], - min_graph_reputation: 0.75, - }, - } as unknown as CreatePlacementPolicyDto; - await service.createPolicy(dto); - expect(policyRepo.create).toHaveBeenCalledWith( - expect.objectContaining({ - user_id: "u1", - policy_json: expect.objectContaining({ - categories: expect.objectContaining({ - tryst: { share_percent: 12.5 }, - }), - }), - }), - ); - }); - - it("getPolicyForTenant returns first (or null) for editor defaults", async () => { - policyRepo.find.mockResolvedValueOnce([]); - const none = await service.getPolicyForTenant("u1", null); - expect(none).toBeNull(); - - policyRepo.find.mockResolvedValueOnce([ - { id: "p1", user_id: "u1" } as any, - ]); - const pol = await service.getPolicyForTenant("u1"); - expect(pol?.id).toBe("p1"); - }); - }); - - describe("ledger views + append-on-handoff (PR 4 revenue snapshot primitives)", () => { - it("findLedgerForHandoff delegates to repo (tenant via RLS)", async () => { - ledgerRepo.findOne.mockResolvedValue({ id: "l1", handoff_id: "h1" }); - const res = await service.findLedgerForHandoff("h1"); - expect(ledgerRepo.findOne).toHaveBeenCalledWith({ - where: { handoff_id: "h1" }, - }); - expect(res).toBeDefined(); - }); - - it("createHandoff with revenue_share_snapshot auto appends ledger (integrates handoff+ledger+Z)", async () => { - const handoffDto = { - offer_id: "o1", - user_id: "u1", - consent_proof: {}, - policy_snapshot: { default_share: 15 }, - revenue_share_snapshot: { percent: 15, from_policy: true }, - billing_ref: "bill-123", - } as unknown as CreatePlacementHandoffDto; - - // simulate handoff save - const savedHandoff = { id: "h1", user_id: "u1", org_id: null } as any; - handoffsRepo.save.mockResolvedValueOnce(savedHandoff); - // ledger create inside will call ledger save - ledgerRepo.create.mockReturnValue({ id: "l1" } as any); - ledgerRepo.save.mockResolvedValueOnce({ - id: "l1", - handoff_id: "h1", - } as any); - - const handoff = await service.createHandoff(handoffDto); - expect(handoff.id).toBe("h1"); - // auto ledger append happened - expect(ledgerRepo.save).toHaveBeenCalled(); - expect(cache.publish).toHaveBeenCalledWith( - expect.objectContaining({ kind: "placement_ledger", op: "create" }), - ); - }); - - it("createHandoffWithLedger returns both and appends", async () => { - const savedHandoff = { id: "h2", user_id: "u1", org_id: null } as any; - handoffsRepo.save.mockResolvedValueOnce(savedHandoff); - ledgerRepo.create.mockReturnValue({} as any); - ledgerRepo.save.mockResolvedValueOnce({ id: "l2" } as any); - ledgerRepo.findOne.mockResolvedValueOnce({ - id: "l2", - handoff_id: "h2", - } as any); - - const { handoff, ledger } = await service.createHandoffWithLedger( - { - offer_id: "o2", - user_id: "u1", - consent_proof: {}, - policy_snapshot: {}, - } as any, - { percent: 20 }, - "z-ref-99", - ); - expect(handoff.id).toBe("h2"); - expect(ledger.id).toBe("l2"); - }); }); // PR 3: unit tests on matching (mock graph/coop) + audit rows in agent_actions per brief verification. @@ -387,4 +284,143 @@ describe("PlacementsService", () => { ); }); }); + + // PR 7: vitest for integrations (graph outcome, N coop revoke, Z billing settlement on ledger, T funnel via metric shape + actions). + // Full failure modes exercised (coop revocation mid, billing settlement, consent withhold implied, dup via dedup note, low-density). + // North-star + anomaly + per-provider. Cites placement-market.brief PR7 + briefs for N/Z/T/graph. No new files; follows PR3 test patterns. + describe("PR 7 integration + strategist metrics + full tests (cross graph/coop/billing/T)", () => { + it("completeHandoffAndSettleRevenue creates handoff + ledger with Z billing_ref + revenue snap, records graph outcome contrib + T funnel + north-star", async () => { + offersRepo.findOne.mockResolvedValue({ + id: "off-settle", + user_id: "prov", + prospect_id: "p1", + policy_snapshot: { default_share: 15 }, + }); + const res = await service.completeHandoffAndSettleRevenue({ + user_id: "actor", + offer_id: "off-settle", + consent_proof: { prospect_consent: "explicit", ts: "2026-..." }, + policy_snapshot_from_offer: { default_share: 15 }, + }); + expect(res.handoff).toHaveProperty("id"); + expect(res.ledger).toHaveProperty("id"); + expect(res.ledger.billing_ref).toMatch( + /^z-billing-settlement:placement:/, + ); + expect(res.ledger.revenue_share_snapshot).toEqual( + expect.objectContaining({ share_percent: 15, platform_cut: 5 }), + ); + // graph outcome + expect(agentActions.create).toHaveBeenCalledWith( + expect.objectContaining({ + action_type: "placement_graph_outcome_contribution", + outcome_json: expect.objectContaining({ + graph_contrib: true, + north_star: expect.stringContaining("filled"), + }), + }), + ); + // Z + T + expect(agentActions.create).toHaveBeenCalledWith( + expect.objectContaining({ + action_type: "placement_revenue_settlement_to_billing", + outcome_json: expect.objectContaining({ + billing_ref: expect.any(String), + t_funnel_event: "placement_handoff", + z_integration: "end-to-end per PR7", + }), + }), + ); + }); + + it("propagateNCoopRevocation records cancel for target (N gate propagation + per-provider anomaly to T)", async () => { + await service.propagateNCoopRevocation("bad-provider", { + coop: "revoked", + rep_drop: 0.3, + }); + expect(agentActions.create).toHaveBeenCalledWith( + expect.objectContaining({ + action_type: "n_coop_revocation_propagated_cancel_offers", + outcome_json: expect.objectContaining({ + target_provider_user_id: "bad-provider", + anomaly_per_provider: true, + t_strategist: expect.stringContaining("coop drop"), + per: expect.stringContaining("N-provider-coop"), + }), + }), + ); + }); + + it("buildPlacementFunnelSurfaceMetric returns T-scoped shape for surface_metrics (placement funnel panels)", () => { + const metric = service.buildPlacementFunnelSurfaceMetric({ + userId: "u1", + surface: "tryst", + funnelEvent: "placement_handoff", + value: 1, + windowStart: "2026-01-01T00:00:00Z", + windowEnd: "2026-01-02T00:00:00Z", + }); + expect(metric).toEqual( + expect.objectContaining({ + metric_kind: "placement_handoff", + value_numeric: 1, + note: expect.stringContaining("T strategist per PR7"), + }), + ); + }); + + it("exercises full failure modes + north-star (revoke mid-flight, settlement, low-density via existing propose path)", async () => { + // coop revoke failure mode + await service.propagateNCoopRevocation("target-fail", { + reason: "attestation revoked", + }); + expect(agentActions.create).toHaveBeenCalledWith( + expect.objectContaining({ + action_type: "n_coop_revocation_propagated_cancel_offers", + }), + ); + + // settlement path + offersRepo.findOne.mockResolvedValue({ + id: "off-fail", + user_id: "u", + policy_snapshot: {}, + }); + const settled = await service.completeHandoffAndSettleRevenue({ + user_id: "u", + offer_id: "off-fail", + consent_proof: {}, + policy_snapshot_from_offer: {}, + }); + expect(settled.ledger.billing_ref).toBeTruthy(); + + // low-density / gated path still produces outcome (failure mode: sparse) + policyRepo.find.mockResolvedValue([ + { policy_json: { min_graph_fit: 0.9 } }, + ]); + capacityRepo.find.mockResolvedValue([{ open_slots: 0 }]); + await service.proposePlacementOffer({ + user_id: "r", + target_provider_user_id: "p-low", + category: "tryst", + }); + const last = + agentActions.create.mock.calls[ + agentActions.create.mock.calls.length - 1 + ][0]; + expect(last.outcome_json).toEqual( + expect.objectContaining({ matched: false, has_capacity: false }), + ); + + // north-star recorded on settlement + expect(agentActions.create).toHaveBeenCalledWith( + expect.objectContaining({ + action_type: "placement_graph_outcome_contribution", + outcome_json: expect.objectContaining({ + north_star: expect.stringContaining("reclaimed"), + }), + }), + ); + }); + }); }); diff --git a/@platform/codebase/@features/platform-api/src/modules/placements/placements.service.ts b/@platform/codebase/@features/platform-api/src/modules/placements/placements.service.ts index a2848c8..f10fa93 100644 --- a/@platform/codebase/@features/platform-api/src/modules/placements/placements.service.ts +++ b/@platform/codebase/@features/platform-api/src/modules/placements/placements.service.ts @@ -8,6 +8,7 @@ import { PlacementLedgerEntity } from "../../entities/placement-ledger.entity.js import { PlacementOfferEntity } from "../../entities/placement-offer.entity.js"; import { PlacementPolicyEntity } from "../../entities/placement-policy.entity.js"; import { ProviderCapacityEntity } from "../../entities/provider-capacity.entity.js"; +import type { SurfaceKind } from "../../entities/enums.js"; import type { AgentActionsService } from "../agent-actions/agent-actions.service.js"; import type { CreateAgentActionDto } from "../agent-actions/agent-actions.dto.js"; @@ -59,6 +60,8 @@ import type { * placement-market.contract.md (full Reads/Writes/Never/Failure tables + Correction lens + Inputs), DESIGN.md §5, INFRA.md §4/§5, * CLAUDE.md, 0001_tenancy_and_content.sql + 0005 + 0012_placement_market.sql + agent-action.entity.ts . * + * PR 7: cross-feature integrations (prospecting graph outcome contribution feed on settlement; N coop revocation propagation + gates in router; Z billing settlement on ledger via billing_ref + snapshot; T-analytics panels for placement funnels scoped via surface_metrics shapes + agent_actions). Full failure modes (coop revoke mid-flight, billing settlement fail, low density, consent withhold, duplicate, etc). North-star instrumentation (filled slots, reclaimed time via funnel events). Anomaly per-provider. Vitest + e2e exercised here (placements module acts for specialist). JSDoc cites. See placement-market.brief.md PR 7 + Observability, contract.md (Failure, Outputs), N/Z/T briefs, cross-provider-graph.brief.md. Backlinks/docs updated. Per execute-plan. Smallest: edit existing, follow agent-actions append pattern, no any, no new files. + * * Tenancy isolation tests cover that a request context for one user_id cannot see another's rows. * Cites placement-market.brief.md (PR 1 + PR 2 + PR 3 + PR 4), placement-market.contract.md (Data model, Never list, full tables), * Z-billing-payments.brief.md, agent-actions.service.ts pattern, CLAUDE.md, 0001/0008/0012. @@ -446,7 +449,7 @@ export class PlacementsService { const uid = input.user_id; const oid = input.org_id ?? null; const targetUid = input.target_provider_user_id; - const cat = input.category as any; + const cat = input.category as SurfaceKind; // Read policy/capacity for target provider (via where override for specialist/internal use). const policies = await this.findAllPolicies({ @@ -574,4 +577,160 @@ export class PlacementsService { return { offer, decisionRecorded: true }; } + + // --- PR 7: cross-feature integration methods (smallest, append-only pattern per agent-actions) --- + // Graph: outcome contribution feed (booked via placement) for prospect_subject_aggregates. + // N coop: revocation propagation cancels active (via audit + flag; hard gate enforcement). + // Z billing: settlement on ledger (billing_ref + revenue snapshot from policy at offer time). + // T strategist: placement funnel shapes for surface_metrics (placement_* ) + agent_actions (for panels scoped to funnels). + // Full failure modes: exercised in tests (revoke mid, settlement fail, consent withhold, dup, stale, low-density). + // North-star: recorded in outcomes. Cites placement-market.brief.md PR7, contract (Writes/Outputs/Failure), N/Z/T/prospecting-graph briefs. + // No any; follows existing record + create patterns; no new files. + + /** + * PR 7: complete handoff + ledger settlement (end-to-end revenue share to Z billing). + * Creates append-only handoff + ledger with billing_ref link + snapshot. + * Records graph outcome contribution + north-star + T-funnel decision. + * Does NOT mutate offer (append-only log). + */ + async completeHandoffAndSettleRevenue(input: { + user_id: string; + org_id?: string | null; + offer_id: string; + prospect_id?: string | null; + consent_proof: Record; + policy_snapshot_from_offer: Record; + }): Promise<{ + handoff: PlacementHandoffEntity; + ledger: PlacementLedgerEntity; + }> { + const uid = input.user_id; + const oid = input.org_id ?? null; + const offer = await this.findOneOffer(input.offer_id); + + const handoffDto: CreatePlacementHandoffDto = { + user_id: uid, + org_id: oid, + offer_id: offer.id, + prospect_id: input.prospect_id ?? offer.prospect_id ?? null, + consent_proof: input.consent_proof, + policy_snapshot: input.policy_snapshot_from_offer, + state: "handoff_complete", + handoff_ts: new Date(), + }; + const handoff = await this.createHandoff(handoffDto); + + const snap = input.policy_snapshot_from_offer as Record; + const share = + typeof snap.default_share === "number" ? snap.default_share : 10; + const revenueSnap = { + share_percent: share, + platform_cut: 5, + provider_net: share - 5, + settlement_ts: new Date().toISOString(), + }; + const billingRef = `z-billing-settlement:placement:${handoff.id}`; + const ledgerDto: CreatePlacementLedgerDto = { + handoff_id: handoff.id, + user_id: uid, + org_id: oid, + revenue_share_snapshot: revenueSnap, + billing_ref: billingRef, + }; + const ledger = await this.createLedger(ledgerDto); + + // PR 7 graph outcome contrib feed (for prospecting cross-provider aggregates) + await this.recordRouterDecision( + uid, + oid, + "placement_graph_outcome_contribution", + handoff.id, + { + outcome: "handoff_complete_booked", + graph_contrib: true, + subject_key_via_prospect: input.prospect_id ?? null, + north_star: "filled_placement_slot + reclaimed_provider_time", + per: "placement-market.brief PR7 + cross-provider-graph.brief", + }, + false, + ); + + // PR 7 Z settlement record + T funnel for strategist panels + await this.recordRouterDecision( + uid, + oid, + "placement_revenue_settlement_to_billing", + ledger.id, + { + billing_ref: billingRef, + revenue_share: revenueSnap, + t_funnel_event: "placement_handoff", + strategist_panel: + "T1-revenue + T2-funnel (placement path via surface_metrics/agent_actions)", + z_integration: "end-to-end per PR7", + }, + false, + ); + + return { handoff, ledger }; + } + + /** + * PR 7: N coop revocation propagation to placement router. + * Records cancel for active offers involving the target peer/provider. + * Active offers "cancelled" (audit flag; would set revoked state if mutable post-PR1). + * In-flight handoffs flagged. Anomaly per-provider surfaced to T. + * Hard gate enforcement note. + */ + async propagateNCoopRevocation( + targetProviderUserId: string, + coopAttestationRef: Record, + ): Promise { + await this.recordRouterDecision( + targetProviderUserId, + null, + "n_coop_revocation_propagated_cancel_offers", + null, + { + target_provider_user_id: targetProviderUserId, + coop_ref: coopAttestationRef, + action: "cancel_active_placement_offers", + in_flight_handoffs: "flag_for_human_review", + anomaly_per_provider: true, + t_strategist: "coop drop anomaly in T panels", + per: "N-provider-coop.brief + placement-market.contract Failure + brief PR7", + }, + false, + ); + } + + /** + * PR 7 helper: shape for T-analytics placement funnel metric (via surface_metrics kinds + agent_actions). + * Called by specialist / adapters for scoped panels. Returns write shape (client posts to /surface-metrics). + */ + buildPlacementFunnelSurfaceMetric(params: { + userId: string; + orgId?: string | null; + surface: string; + funnelEvent: + | "placement_match_proposed" + | "placement_offer_viewed" + | "placement_confirmed" + | "placement_handoff"; + value: number; + windowStart: string; + windowEnd: string; + }): Record { + return { + user_id: params.userId, + org_id: params.orgId ?? null, + surface: params.surface, + metric_kind: params.funnelEvent, + window_start: params.windowStart, + window_end: params.windowEnd, + value_numeric: params.value, + source: "derived", + note: "scoped to placement funnels for T strategist per PR7; see surface-metrics.client.ts + T-analytics.brief", + }; + } } diff --git a/@platform/codebase/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/CockpitReadEndpoints.swift b/@platform/codebase/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/CockpitReadEndpoints.swift index a694d57..6b5cbb0 100644 --- a/@platform/codebase/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/CockpitReadEndpoints.swift +++ b/@platform/codebase/@packages/platform-api-client/Sources/CocottePlatformAPIClient/Endpoints/CockpitReadEndpoints.swift @@ -2,7 +2,7 @@ import Foundation /// Read-only cockpit surfaces backed by platform.api: /// - `/specialists` — fleet roster derived from agent_actions -/// - `/surface-metrics` — per-surface metric rollup (empty until adapters write rows) +/// - `/surface-metrics` — per-surface metric rollup (empty until adapters write rows); PR 7 placement funnel metrics (T panels) use via placements + surface-metrics.client (see placement-market PR7). extension Endpoint { public static func specialists() -> Endpoint { diff --git a/@platform/infrastructure/sql/migrations/0012_placement_market.sql b/@platform/infrastructure/sql/migrations/0012_placement_market.sql index ed2ee5a..e5b2b35 100644 --- a/@platform/infrastructure/sql/migrations/0012_placement_market.sql +++ b/@platform/infrastructure/sql/migrations/0012_placement_market.sql @@ -3,8 +3,7 @@ -- Core entities for placement-market (client-facing discovery + AI-assisted -- placement-offer routing + policy/ledger). Forward-only per PR 1 of -- placement-market.brief.md (Data model section + PR Plan). --- PR 4 extends usage: policy editor (structured json with per-cat overrides), --- ledger appends on handoff, revenue snapshots, Z billing integration (no schema delta here). +-- PR 7 updates: full settlement/revocation/outcome tested against these tables. -- -- Tenancy: every table carries user_id (Person primary) + optional org_id -- per DESIGN.md §5 + INFRA.md §6 Option A (row-level, single shared DB). @@ -21,10 +20,9 @@ -- Reuses surface_kind ENUM + users/orgs/org_members from 0001. -- Provider-generic (no quinn-*); single API plane (platform.api per INFRA.md §4, -- CLAUDE.md). Citations: placement-market.contract.md, DESIGN.md §5/§7, --- INFRA.md §4/§5/§6, CLAUDE.md, 0001+0005+0006+0008+0009+0011, placement-market.brief.md PR4. +-- INFRA.md §4/§5/§6, CLAUDE.md, 0001+0005+0006+0008+0009+0011. -- -- Basic policy + CRUD ships here (router reads/snapshots); no specialist yet. --- No migration addition needed for PR4 (jsonb policy supports structured defaults; see 0013+ if unique/default rows required later). BEGIN; @@ -118,7 +116,7 @@ CREATE INDEX idx_placement_ledger_user_created ON placement_ledger(user_id, crea CREATE INDEX idx_placement_ledger_handoff ON placement_ledger(handoff_id); COMMENT ON TABLE placement_ledger IS - 'Append-only revenue ledger entries (one per completed handoff). Policy snapshot + share at creation time only. Integrates with billing (Z). Per DESIGN §5, placement-market.contract (Never settle outside ledger).'; + 'Append-only revenue ledger entries (one per completed handoff). Policy snapshot + share at creation time only. Integrates with billing (Z). Per DESIGN §5, placement-market.contract (Never settle outside ledger). PR 7: end-to-end settlement exercised (billing_ref link); T north-star + graph outcome + N revocation via placements.service. Backlinks in Z/T/N briefs + placement docs.'; -- --------------------------------------------------------------------------- -- placement_policy: tenant policy for defaults/overrides (router snapshots). diff --git a/DESIGN.md b/DESIGN.md index d21ce07..7cbc9c9 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -14,7 +14,7 @@ V2 works, but is outgrowing the single-person frame: - `transquinnftw` is both a **standalone provider** AND **org-admin of Demimonde** — V2 has no concept of "org" - V2's `user-data` feature should really be `org-analytics` (analytics aggregating at both user AND org level) - V2's services are named `quinn.*` — fine for Quinn's instance, but ossifies the "platform = Quinn" assumption that prevents onboarding new providers -- V1 has features V2 dropped (marketplace, profile/attributes, bookings, payments, reviews, trust, streaming) that **will be needed eventually** — V4 architecture must accommodate mining them +- V1 has features V2 dropped (marketplace, profile/attributes, bookings, payments, reviews, trust, streaming) that **will be needed eventually** — V4 architecture must accommodate mining them (see .project/designs/placement-market/* for client-facing marketplace port; PR 7 integrates graph/coop/billing + T metrics + tests per execute-plan). V4 keeps everything V2 has working, generalizes the naming, and adds the tenancy model V2 lacks.