diff --git a/config/partners.example.toml b/config/partners.example.toml index 5e440b9f..71bda33d 100644 --- a/config/partners.example.toml +++ b/config/partners.example.toml @@ -11,7 +11,7 @@ # out id = "out-{CODE}-{n}" # Bare FANTASY402_ / partner-only FANTASY402_SPEN_ auto-upgrade to per-out on seed. # Secrets: {prefix}BEARER_TOKEN · CUSTOMER_ID · AGENT_ID · PASSWORD · DOMAIN · … -# Desk URL env (book-level): PARTNER_DOMAIN — must resolve via active SKINS[].hosts +# Desk URL env (book-level): DESK_DOMAIN (legacy PARTNER_DOMAIN) — must resolve via active SKINS[].hosts # (bare book-level DOMAIN env is retired). Prefer TOML url= on each out when known. # @see https://bun.com/docs/runtime/toml # @see docs/PARTNER-DOMAIN.md @@ -40,7 +40,7 @@ partner_code = "SPEN" provider = "fantasy402" env_prefix = "FANTASY402_SPEN_1_" currency = "USD" -# url must be a host on an *active* skin; omit → PARTNER_DOMAIN / Ultra default +# url must be a host on an *active* skin; omit → DESK_DOMAIN / Ultra default url = "https://fantasy402.com" # optional desk BookId — must agree with url host (host preferred) # book_id = "fantasy402" @@ -58,7 +58,7 @@ partner_code = "ASH" provider = "fantasy402" env_prefix = "FANTASY402_ASH_1_" currency = "USD" -# url must be a host on an *active* skin; omit → PARTNER_DOMAIN / Ultra default +# url must be a host on an *active* skin; omit → DESK_DOMAIN / Ultra default url = "https://fantasy402.com" # book_id = "fantasy402" working_balance = 2000 diff --git a/docs/PARTNER-DOMAIN.md b/docs/PARTNER-DOMAIN.md index cbe9efe7..d1a2c5e2 100644 --- a/docs/PARTNER-DOMAIN.md +++ b/docs/PARTNER-DOMAIN.md @@ -1,21 +1,37 @@ # Partner domain architecture -Five interconnected layers. **Seat-capital-shaped** naming; **Kalshi-bot** is -the local SSOT for registry + Fantasy Ultra until a separate seat-capital -service is the only writer. +Five interconnected **seat-ops** layers (financial partner → outs → finance). +Desk identity (host → book → skin → live product) lives in +[`src/domain/`](../src/domain/) — not under seat “partner”. -Machine status: `bun run partner:domain` · `bun run partner:domain -- --json` -Skin matrix: `bun run partner:skins` · Book matrix: `bun run partner:books` +**Kalshi-bot** is the local SSOT for registry + Fantasy Ultra until a separate +seat-capital service is the only writer. + +Seat-ops status: `bun run ops:status` · `bun run ops:status -- --json` +(legacy: `partner:domain`) +Skin matrix: `bun run domain:skins` · Book matrix: `bun run domain:books` +Host discover: `bun run domain:host-discover` Event inventory (Buckeye / Fantasy402 → plive+ezlive): `bun run partner:watch-events` Competitions (Plive league → canonical id): [`src/domain/competitions.ts`](../src/domain/competitions.ts) · `resolveCompetition` -Expansion map: `bun run partner:map` · +Expansion map: `bun run ops:map` · [`PARTNER-EXECUTION-EXPANSION.md`](PARTNER-EXECUTION-EXPANSION.md) Code map: -[`src/partner/domain.ts`](../src/partner/domain.ts) · inventory: +[`src/partner/architecture.ts`](../src/partner/architecture.ts) (seat-ops layers) · +desk matrix [`src/domain/`](../src/domain/) · inventory: [`PARTNER-FANTASY-ULTRA.md`](PARTNER-FANTASY-ULTRA.md) +### Naming split (desk vs seat) + +| Concept | Name | Example | +| ------- | ---- | ------- | +| Desk host URL env | **`DESK_DOMAIN`** (legacy `PARTNER_DOMAIN`) | `https://parlay21.com` | +| White-label family | `SkinId` | `ace`, `buckeye` | +| Desk brand under skin | `BookId` | `parlay21`, `fantasy402` | +| Seat financial owner | **Partner** (unchanged) | `SPEN` | +| Betting account | Out | `out-SPEN-1` | + --- ## Layers @@ -36,7 +52,7 @@ Expansion map: `bun run partner:map` · | **partial** | Scaffold / one path works; not full loop | | **planned** | Named in architecture; not implemented | -Run `bun run partner:domain` for the live component checklist. +Run `bun run ops:status` for the live component checklist. --- @@ -245,8 +261,10 @@ bun run serve | Command | Layer | | ----------------------------------------------------- | --------------------------------------------------------------------- | -| `partner:domain` | All (status) | -| `partner:map` | Telegram → authorization → provider expansion map (Mermaid) | +| `ops:status` (legacy `partner:domain`) | Seat-ops architecture checklist | +| `ops:map` (legacy `partner:map`) | Telegram → authorization → provider expansion map (Mermaid) | +| `domain:skins` / `domain:books` | Desk matrix (hosts → skin / book) | +| `domain:host-discover` | Suggest SkinId from host (never auto-maps) | | `partner:toml` | Partner + Accounts (Bun.TOML config seed/export) | | `partner:health` | Registry + env + risk + ledger freshness | | `partner:desk-smoke` | Per-out secret readiness + optional signed `login()` | @@ -363,14 +381,14 @@ Idempotent: re-run seed updates in place (SQLite `ON CONFLICT DO UPDATE`). 1. out prefix FANTASY402_SPEN_1_BEARER_TOKEN 2. partner prefix FANTASY402_SPEN_BEARER_TOKEN 3. book fallback FANTASY402_BEARER_TOKEN -4. desk URL PARTNER_DOMAIN → SKINS default +4. desk URL DESK_DOMAIN (legacy PARTNER_DOMAIN) → SKINS default (host → BookId via HOST_TO_BOOK + SkinId via HOST_TO_SKIN) ``` Canonical `env_prefix` is **per-out**: `{BOOK}_{CODE}_{N}_`. Bare `FANTASY402_` or partner-only `FANTASY402_SPEN_` auto-upgrade on materialize/seed. Keys (not USER/PASS): `BEARER_TOKEN` · `CUSTOMER_ID` · `AGENT_ID` · `PASSWORD` · `DOMAIN` -· `SKIN` · `CURRENCY`. Desk URL: **`PARTNER_DOMAIN`** (or per-out `*DOMAIN`) +· `SKIN` · `CURRENCY`. Desk URL: **`DESK_DOMAIN`** (legacy `PARTNER_DOMAIN`; or per-out `*DOMAIN`) must resolve via `SKINS[].hosts` → `BookId` + `SkinId`. Bare book-level DOMAIN env keys are retired (`RETIRED_BARE_BOOK_DOMAIN_ENVS`). Env prefix `{BOOK}_` is the adapter env brand (`FANTASY402`), not the desk `BookId` catalog. API base diff --git a/docs/PARTNER-FANTASY-ULTRA.md b/docs/PARTNER-FANTASY-ULTRA.md index 5c779f5e..cf0a3684 100644 --- a/docs/PARTNER-FANTASY-ULTRA.md +++ b/docs/PARTNER-FANTASY-ULTRA.md @@ -270,7 +270,7 @@ export FANTASY402_AGENT_ID='…' export FANTASY402_PASSWORD='…' # optional — example host; omit to use SKINS-derived Ultra-mapper default # (requireDefaultUrlForUltraMapper / defaultUrlForSkin) -export PARTNER_DOMAIN='https://BOOK.example' # must be a SKINS[].hosts URL → SkinId +export DESK_DOMAIN='https://BOOK.example' # must be a SKINS[].hosts URL → SkinId (legacy: PARTNER_DOMAIN) export FANTASY402_SKIN=2 export FANTASY402_CURRENCY=USD ``` @@ -430,7 +430,7 @@ Code: `PandoraSocket` · `coefficients.ts` · `coefficient-store.ts` · Partner → Communication → Accounts/Outs → Assets → Finance. **Full map + maturity:** [`PARTNER-DOMAIN.md`](PARTNER-DOMAIN.md) · -`bun run partner:domain` +`bun run ops:status` ## Liquidity sources map (partners / outs / providers) @@ -452,16 +452,16 @@ Works for **any** host. Mapped desks come from `SKINS[].hosts` (not a hard-coded book list). ```bash -bun run partner:host-discover -- --url=https://BOOK.example +bun run domain:host-discover -- --url=https://BOOK.example # → skin unknown|suggested, stores public HTML/asset URLs under docs/artifacts/host-discover/-urls.json -bun run partner:host-discover -- --url=https://BOOK.example --har=./session.har +bun run domain:host-discover -- --url=https://BOOK.example --har=./session.har # → merges session URLs into inventory only (no Ultra→skin scoring) -bun run partner:host-discover -- --all +bun run domain:host-discover -- --all # → every apex host in SKINS[].hosts -bun run partner:host-discover -- --url=https://BOOK.example --compare --json +bun run domain:host-discover -- --url=https://BOOK.example --compare --json # → optional target + all mapped apex hosts ``` @@ -480,7 +480,7 @@ bun run partner:host-discover -- --url=https://BOOK.example --compare --json - Mapped host → definitive score 1.0 (`already_mapped`) - URL inventory: `docs/artifacts/host-discover/-urls.json` - Code: `src/partner/host-discover.ts` · `src/partner/host-weighted-score.ts` · - `tools/partner-host-discover.ts` + `tools/host-discover-cli.ts` ### Out identity boundary (host → skin → live products → adapter) diff --git a/docs/PROTONPASS.md b/docs/PROTONPASS.md index c10301ba..d670cb52 100644 --- a/docs/PROTONPASS.md +++ b/docs/PROTONPASS.md @@ -136,7 +136,7 @@ username/password/url — no `--field`. Multi-field desk secrets use | `agentID` | Text | `FANTASY402_AGENT_ID` | | `password` | Hidden | `FANTASY402_PASSWORD` | | `bearerToken` | Hidden | `FANTASY402_BEARER_TOKEN` (browser JWT; short-lived) | -| `domain` | Text | `PARTNER_DOMAIN` (optional; default from `SKINS[].hosts` via `resolveDeskDomainFromEnv` → SkinId) | +| `domain` | Text | `DESK_DOMAIN` (legacy `PARTNER_DOMAIN`; optional; default from `SKINS[].hosts` via `resolveDeskDomainFromEnv` → SkinId) | | `skin` | Text | `FANTASY402_SKIN` (optional; default `2`) | | `currency` | Text | `FANTASY402_CURRENCY` (optional; default `USD`) | diff --git a/package.json b/package.json index 1ea9d016..a4079aec 100644 --- a/package.json +++ b/package.json @@ -67,11 +67,16 @@ "partner:registry": "bun tools/partner-registry.ts", "partner:capacity": "bun tools/partner-registry.ts", "partner:sports": "bun tools/partner-sports.ts", - "partner:domain": "bun tools/partner-domain.ts", - "partner:skins": "bun tools/partner-domain.ts --skins", - "partner:books": "bun tools/partner-domain.ts --books", - "partner:host-discover": "bun tools/partner-host-discover.ts", - "partner:map": "bun tools/partner-domain.ts --map", + "domain:skins": "bun tools/domain-cli.ts --skins", + "domain:books": "bun tools/domain-cli.ts --books", + "domain:host-discover": "bun tools/host-discover-cli.ts", + "ops:status": "bun tools/ops-cli.ts", + "ops:map": "bun tools/ops-cli.ts --map", + "partner:domain": "bun tools/ops-cli.ts", + "partner:skins": "bun tools/domain-cli.ts --skins", + "partner:books": "bun tools/domain-cli.ts --books", + "partner:host-discover": "bun tools/host-discover-cli.ts", + "partner:map": "bun tools/ops-cli.ts --map", "partner:profile": "bun tools/partner-profile.ts", "partner:avatars": "bun tools/partner-profile.ts --png", "partner:toml": "bun tools/partner-toml.ts", diff --git a/src/domain/README.md b/src/domain/README.md index f116039d..e0a28ebb 100644 --- a/src/domain/README.md +++ b/src/domain/README.md @@ -95,23 +95,27 @@ hand-edit product or gap columns; change [`skins.ts`](skins.ts) instead. **Invariant:** `active: true` ⇒ `hosts.length ≥ 1` (`assertActiveSkinsHaveHosts`). **Fingerprint gate:** active skins need fingerprints or membership in `FINGERPRINT_PENDING_SKINS` -(`assertFingerprintCoverage`). Desk URL env: `PARTNER_DOMAIN` (or per-out -`*DOMAIN`) must be a host in an active skin. Bare book-level DOMAIN env keys are -retired (`RETIRED_BARE_BOOK_DOMAIN_ENVS`). `fantasy402` alias → skin `buckeye` +(`assertFingerprintCoverage`). Desk URL env: **`DESK_DOMAIN`** (legacy +`PARTNER_DOMAIN` dual-read; or per-out `*DOMAIN`) must be a host in an active +skin. Bare book-level DOMAIN env keys are retired +(`RETIRED_BARE_BOOK_DOMAIN_ENVS`). `fantasy402` alias → skin `buckeye` (mapper token / BookId, not SkinId). Host → `BookId` → `SkinId` (`getBookByHost` / `resolveBookId` in [`books.ts`](books.ts)). ```bash -bun run partner:skins -bun run partner:skins -- --json -bun run partner:books -bun run partner:books -- --json +bun run domain:skins +bun run domain:skins -- --json +bun run domain:books +bun run domain:books -- --json +# seat-ops (not desk): bun run ops:status +# legacy aliases: partner:skins · partner:books ``` ## Unknown host ```bash -bun run partner:host-discover -- --url=https://example.com +bun run domain:host-discover -- --url=https://example.com +# legacy: partner:host-discover ``` Suggests skin from weighted fingerprints (`SKINS[].fingerprints` + diff --git a/src/domain/books.ts b/src/domain/books.ts index 9d7da224..ba7cce81 100644 --- a/src/domain/books.ts +++ b/src/domain/books.ts @@ -27,6 +27,7 @@ export type BookRecord = { label: string; }; +/** Interior brand only — not a wire parser. Prefer resolveBookId / isBookId at boundaries. */ function asBookId(raw: string): BookId { const t = raw.trim().toLowerCase(); if (!t) throw new Error('BookId: empty'); @@ -191,7 +192,7 @@ export type BookMatrixRow = { hosts: readonly string[]; }; -/** Machine-readable book matrix for `bun run partner:books`. */ +/** Machine-readable book matrix for `bun run domain:books`. */ export function buildBookMatrixRows(): BookMatrixRow[] { return BOOKS.map(b => ({ bookId: b.id, diff --git a/src/domain/index.ts b/src/domain/index.ts index c8e1cebd..5c6fbef2 100644 --- a/src/domain/index.ts +++ b/src/domain/index.ts @@ -62,12 +62,14 @@ export { listSkins, normalizeHost, normalizeSkinName, + DESK_DOMAIN_ENV, PARTNER_DOMAIN_ENV, RETIRED_BARE_BOOK_DOMAIN_ENVS, isRetiredBareBookDomainEnv, assertActiveSkinsHaveHosts, listActiveSkins, requireDefaultUrlForUltraMapper, + deskDomainFromEnvMap, resolveDeskDomainFromEnv, resolveSkinId, skinOfferedCatalogNames, diff --git a/src/domain/live-product-endpoints.ts b/src/domain/live-product-endpoints.ts index ff2e3922..c4fb203d 100644 --- a/src/domain/live-product-endpoints.ts +++ b/src/domain/live-product-endpoints.ts @@ -35,7 +35,7 @@ export const STATSCORE_BOOKED_EVENTS = { } as const; /** - * Desk-relative API paths for the Fantasy402 Ultra mapper (joined to PARTNER_DOMAIN / SKINS host). + * Desk-relative API paths for the Fantasy402 Ultra mapper (joined to DESK_DOMAIN / SKINS host). * Not absolute URLs — host comes from skins / env. */ export const ULTRA_DESK_API_PATHS = { diff --git a/src/domain/skins.ts b/src/domain/skins.ts index 19f4eeb5..af872d74 100644 --- a/src/domain/skins.ts +++ b/src/domain/skins.ts @@ -34,7 +34,7 @@ export type SkinRecord = { displayName: string; description: string; /** - * Active desks must declare ≥1 host (HOST_TO_SKIN / PARTNER_DOMAIN / discover). + * Active desks must declare ≥1 host (HOST_TO_SKIN / DESK_DOMAIN / discover). * Inactive = placeholder until hosts + offerings are proven. */ active: boolean; @@ -373,12 +373,20 @@ export function requireDefaultUrlForUltraMapper(): string { return url; } -/** Brand-neutral desk URL env — resolved against SKINS hosts / SkinId mapping. */ +/** + * Brand-neutral desk **host** URL env — resolved against SKINS hosts / SkinId. + * Prefer this over seat-partner naming; desk identity is host → book → skin. + */ +export const DESK_DOMAIN_ENV = 'DESK_DOMAIN'; + +/** + * @deprecated Use {@link DESK_DOMAIN_ENV} (`DESK_DOMAIN`). Still dual-read for one release. + */ export const PARTNER_DOMAIN_ENV = 'PARTNER_DOMAIN'; /** * Retired bare-book desk URL env keys — never read. - * Use PARTNER_DOMAIN or per-out/partner `{PREFIX}DOMAIN` instead. + * Use DESK_DOMAIN (or legacy PARTNER_DOMAIN) or per-out `{PREFIX}DOMAIN` instead. */ export const RETIRED_BARE_BOOK_DOMAIN_ENVS = ['FANTASY402_DOMAIN'] as const; @@ -386,15 +394,29 @@ export function isRetiredBareBookDomainEnv(key: string): boolean { return (RETIRED_BARE_BOOK_DOMAIN_ENVS as readonly string[]).includes(key); } +/** + * Preferred desk URL from env map only (no Ultra default). + * `DESK_DOMAIN` wins over legacy `PARTNER_DOMAIN`. + */ +export function deskDomainFromEnvMap( + envMap: Record = typeof process !== 'undefined' ? process.env : {} +): string | undefined { + const preferred = envMap[DESK_DOMAIN_ENV]?.trim(); + if (preferred) return preferred; + const legacy = envMap[PARTNER_DOMAIN_ENV]?.trim(); + if (legacy) return legacy; + return undefined; +} + /** * Desk URL from env (brand-neutral). - * PARTNER_DOMAIN → SKINS Ultra-mapper default (hosts → SkinId) + * DESK_DOMAIN → legacy PARTNER_DOMAIN → SKINS Ultra-mapper default (hosts → SkinId) * Bare-book DOMAIN env keys in RETIRED_BARE_BOOK_DOMAIN_ENVS are ignored. */ export function resolveDeskDomainFromEnv( envMap: Record = typeof process !== 'undefined' ? process.env : {} ): string { - return envMap[PARTNER_DOMAIN_ENV]?.trim() || requireDefaultUrlForUltraMapper(); + return deskDomainFromEnvMap(envMap) || requireDefaultUrlForUltraMapper(); } export function skinOffersLiveProduct(skin: string, product: string): boolean { diff --git a/src/institutions/event-store/open-db.ts b/src/institutions/event-store/open-db.ts index 0fb04951..2014e727 100644 --- a/src/institutions/event-store/open-db.ts +++ b/src/institutions/event-store/open-db.ts @@ -166,7 +166,7 @@ export function applyEventStoreSchema(db: Database): void { ); db.run(`CREATE INDEX IF NOT EXISTS idx_skin_events_book ON skin_events (book_id)`); db.run(`CREATE INDEX IF NOT EXISTS idx_skin_events_last_updated ON skin_events (last_updated)`); - db.run(`CREATE INDEX IF NOT EXISTS idx_skin_events_competition ON skin_events (competition_id)`); + // idx_skin_events_competition requires competition_id — create after migrateEventStoreColumns // Partner financial registry (outs) — secrets stay in env, not here db.run(`CREATE TABLE IF NOT EXISTS partners ( id TEXT PRIMARY KEY, @@ -213,6 +213,11 @@ export function applyEventStoreSchema(db: Database): void { migrateSkinEventsInventoryIdentity(db); migrateSkinEventsBookInventoryUnique(db); migrateSkinEventsCompetitionIds(db); + if (tableHasColumn(db, 'skin_events', 'competition_id')) { + db.run( + `CREATE INDEX IF NOT EXISTS idx_skin_events_competition ON skin_events (competition_id)` + ); + } migrateSourceEventSelectors(db); abandonLegacyKalshiInventoryRuns(db); abandonUnpinnedSourceInventoryRuns(db); diff --git a/src/partner/account-profile.ts b/src/partner/account-profile.ts index 2ac16d62..5f3b92bd 100644 --- a/src/partner/account-profile.ts +++ b/src/partner/account-profile.ts @@ -147,7 +147,7 @@ export function fantasyDeskEnvPresence( * * Required (any level of the fallback chain): * *BEARER_TOKEN *CUSTOMER_ID *AGENT_ID *PASSWORD - * Optional: PARTNER_DOMAIN (or per-out/partner *DOMAIN) → host → SkinId; *SKIN *CURRENCY + * Optional: DESK_DOMAIN (legacy PARTNER_DOMAIN) or per-out/partner *DOMAIN → host → SkinId; *SKIN *CURRENCY * Account id: FANTASY402_ACCOUNT_ID (default partner-dummy) */ export function loadFantasy402ProfileFromEnv( diff --git a/src/partner/architecture.ts b/src/partner/architecture.ts new file mode 100644 index 00000000..2c646193 --- /dev/null +++ b/src/partner/architecture.ts @@ -0,0 +1,470 @@ +/** + * Seat-ops architecture SSOT (five layers). + * + * Partner (seat) → Communication → Accounts/Outs → Assets → Finance + * + * **Not** the desk matrix (`src/domain/` = skins / hosts / books / live products). + * This module is the machine-readable map of **what exists in Kalshi-bot** vs + * aspirational seat-capital / Telegram onboarding. Keep status honest. + * + * @see docs/PARTNER-DOMAIN.md + * @see src/domain/README.md + */ + +export type OpsLayerId = 'partner' | 'communication' | 'accounts' | 'assets' | 'finance'; + +export type OpsMaturity = 'built' | 'partial' | 'planned'; + +export type OpsComponent = { + id: string; + name: string; + maturity: OpsMaturity; + /** Code / table / CLI path */ + where: string; + notes?: string; +}; + +export type OpsLayer = { + id: OpsLayerId; + name: string; + purpose: string; + components: OpsComponent[]; +}; + +/** @deprecated Use OpsLayerId */ +export type DomainLayerId = OpsLayerId; +/** @deprecated Use OpsMaturity */ +export type DomainMaturity = OpsMaturity; +/** @deprecated Use OpsComponent */ +export type DomainComponent = OpsComponent; +/** @deprecated Use OpsLayer */ +export type DomainLayer = OpsLayer; + +/** Naming conventions (do not invent alternate forms). */ +export const PARTNER_NAMING = { + partnerCode: 'UPPER short code', + partnerCodeExample: 'SPEN', + outId: 'out-{partnerCode}-{seq}', + outIdExample: 'out-SPEN-1', + bookId: 'desk brand under skin (host-derived)', + bookIdExample: 'fantasy402', + skinId: 'white-label family', + skinIdExample: 'buckeye', + vaultId: 'vault-{outId}', + vaultIdExample: 'vault-out-SPEN-1', + liquidityKey: '{outId}@{skin}', + liquidityKeyExample: 'out-SPEN-1@ezlive', + avatarFile: '{partnerCode}.svg|png', + avatarExample: 'SPEN.png', + envPrefix: '{BOOK}_{CODE}_{N}_ e.g. FANTASY402_SPEN_1_', +} as const; + +/** + * Canonical five-layer map for agents and `ops:status` / `partner:domain`. + * Update maturity when shipping — never mark planned as built. + */ +export const OPS_LAYERS: readonly OpsLayer[] = [ + { + id: 'partner', + name: 'Partner', + purpose: 'Financial entity that owns outs and receives split/commission.', + components: [ + { + id: 'partners-table', + name: 'partners table', + maturity: 'built', + where: 'event-store · partners (id, name, active, profit_split, commission_rate)', + }, + { + id: 'partners-toml', + name: 'partners.toml (Bun.TOML)', + maturity: 'built', + where: 'config/partners.example.toml · partner:toml · src/partner/toml-config.ts', + notes: 'Non-secret SSOT on disk; seed registry without FANTASY402_SKINS_JSON', + }, + { + id: 'partner-entity', + name: 'PartnerEntity type', + maturity: 'built', + where: 'src/partner/registry.ts', + }, + { + id: 'partner-code', + name: 'partnerCode on out meta', + maturity: 'partial', + where: 'betting_accounts.meta_json.partnerCode', + notes: 'Set on seed (FANTASY402_PARTNER_CODE); not a dedicated partners.code column yet', + }, + { + id: 'partner-color-avatar', + name: 'Deterministic color + avatar (Bun.color)', + maturity: 'built', + where: 'src/partner/visuals.ts · partner:profile [--png]', + notes: 'HSL→hex/rgba/ansi-16m + SVG/PNG via Bun.Image; contrast text', + }, + { + id: 'glossary-partner-ops', + name: 'Partner-ops glossary ids', + maturity: 'built', + where: 'src/institutions/glossary.ts · partners:validate', + }, + ], + }, + { + id: 'communication', + name: 'Communication', + purpose: 'Partner-facing chat, alerts, bot commands, digests.', + components: [ + { + id: 'telegram-bot', + name: 'Telegram long-poll bot', + maturity: 'partial', + where: 'src/telegram/bot.ts', + notes: 'Calibration digest/dashboard plus permissioned /approve and /revoke_out routing', + }, + { + id: 'telegram-authorization-flow', + name: 'Telegram authorization + durable receipt outbox', + maturity: 'built', + where: 'src/telegram/authorization-*.ts · src/partner/authorization/', + notes: + 'Numeric chat/topic/user binding, hash-verified grants, revocation evidence, retries', + }, + { + id: 'telegram-subscribers', + name: 'Subscriber list', + maturity: 'partial', + where: 'src/telegram/subscribers.ts · research/telegram-subscribers.json', + }, + { + id: 'partner-telegram-link', + name: 'partners.telegram_chat_id / topicId', + maturity: 'partial', + where: 'account_authorization_requests · account_authorizations', + notes: + 'Authorization provenance is bound; partner-level channel preferences remain planned', + }, + { + id: 'inventory-telegram', + name: 'New-event Telegram notify', + maturity: 'partial', + where: 'tools/watch-fantasy-events.ts', + }, + { + id: 'execution-operations-workers', + name: 'Independent execution workers + breaker receipts', + maturity: 'built', + where: + 'partner:reconcile-kalshi · partner:sync-kalshi-lifecycle · partner:deliver-receipts · partner:execution:register', + notes: + 'Leased reconciliation and receipts plus account lifecycle ingestion run independently of Telegram polling', + }, + ], + }, + { + id: 'accounts', + name: 'Accounts / Outs', + purpose: 'Betting accounts (outs) with multi-skin limits and book adapter.', + components: [ + { + id: 'betting-accounts', + name: 'betting_accounts table', + maturity: 'built', + where: 'event-store · out id, provider, max_stake, meta_json', + }, + { + id: 'out-skin-matrix', + name: 'Out × skin capacity', + maturity: 'built', + where: 'src/partner/skins.ts · partner:capacity', + }, + { + id: 'fantasy-adapter', + name: 'Fantasy402 Ultra adapter', + maturity: 'partial', + where: 'src/partner/fantasy-ultra/', + notes: 'Login, inventory, Pandora coeffs; placeOrder live only with HAR map URL', + }, + { + id: 'sports-inventory', + name: 'Sports + leagues inventory', + maturity: 'built', + where: 'partner:sports · 30 stream buckets · 4 primary API ids', + }, + { + id: 'concentration-router', + name: 'selectAccountForProposal', + maturity: 'partial', + where: 'listEligibleOutSkinPairs · concentrationByOut', + notes: 'Helpers only — no full proposal router CLI yet', + }, + { + id: 'authorized-execution-wrapper', + name: 'Transactional authorized execution wrapper', + maturity: 'built', + where: 'src/partner/execution/', + notes: + 'Immediate reservation transaction, gate recheck, idempotent dispatch, ambiguous-outcome reconciliation, and durable receipts', + }, + { + id: 'provider-execution-bindings', + name: 'Live provider execution bindings', + maturity: 'built', + where: + 'src/partner/execution/kalshi*.ts · src/bot/kalshi-client.ts · src/research/serve.ts', + notes: + 'Kalshi V2 mapper, out-scoped client, authorized order/cancel routes, complete reconciliation evidence, provider lifecycle accounting, and demo proof tooling; production remains separately armed only after real soak review', + }, + ], + }, + { + id: 'assets', + name: 'Assets', + purpose: 'Credentials, vault pointers, visual identity (no secrets in DB).', + components: [ + { + id: 'env-prefix', + name: 'env_prefix on out', + maturity: 'built', + where: 'betting_accounts.env_prefix', + }, + { + id: 'vault-meta', + name: 'meta.vaultId', + maturity: 'partial', + where: 'meta_json.vaultId on seed', + }, + { + id: 'proton-provision', + name: 'Proton Pass provision', + maturity: 'partial', + where: 'partner:vault:provision · .env.protonpass', + notes: 'Custom item Fantasy402; per-out vault optional', + }, + { + id: 'credentials-boundary', + name: 'Secrets never in SQLite', + maturity: 'built', + where: 'registry seed + account-profile env/Pass only', + }, + ], + }, + { + id: 'finance', + name: 'Finance', + purpose: 'Ledger, P&L, splits, partner reports.', + components: [ + { + id: 'partner-finance-cron', + name: 'Desk finance cron (registry-driven)', + maturity: 'built', + where: 'partner:finance-cron · PARTNER_FINANCE_CRON=1 in cron-main', + notes: 'Capacity + env + inventory + ticket open/settled rollup; net P&L needs list URL', + }, + { + id: 'partner-ledger', + name: 'partner_ledger', + maturity: 'partial', + where: 'src/partner/ledger.ts · desk_snapshot · odds_book · ticket', + notes: 'No remote settlement list API — ticket via partner:ingest-tickets (betGroups)', + }, + { + id: 'authorized-execution-journal', + name: 'Immutable authorized-execution journal + projections', + maturity: 'built', + where: 'src/partner/execution/execution-journal.ts · provider-lifecycle-journal.ts', + notes: + 'Integer cash, exposure, realized P&L, fees, and partner split by partner/out/skin; provider drift remains fail-closed', + }, + { + id: 'ledger-types', + name: 'Generic ledger types', + maturity: 'partial', + where: 'src/institutions/ledger-types.ts', + notes: 'Institution types — not partner_ledger integration', + }, + { + id: 'accounting-glossary', + name: 'Accounting / event glossary', + maturity: 'built', + where: 'glossary accounting.* · event.partner.* · event.out.*', + }, + { + id: 'pnl-cron-telegram', + name: 'Daily desk + ticket totals → Telegram', + maturity: 'partial', + where: 'partner:finance-cron --notify · TELEGRAM_TOPIC_ID_{CODE}', + notes: 'Ticket risk/toWin when ingested; settled net P&L still planned', + }, + ], + }, +] as const; + +export type OpsStatusReport = { + generatedAt: string; + naming: typeof PARTNER_NAMING; + layers: Array<{ + id: OpsLayerId; + name: string; + purpose: string; + built: number; + partial: number; + planned: number; + components: OpsComponent[]; + }>; + totals: { built: number; partial: number; planned: number; components: number }; + orchestration: { + ssot: string; + clis: string[]; + missingForBotLoop: string[]; + }; +}; + +/** @deprecated Use OpsStatusReport */ +export type DomainStatusReport = OpsStatusReport; + +export function buildOpsStatusReport(nowMs = Date.now()): OpsStatusReport { + const layers = OPS_LAYERS.map(layer => { + let built = 0; + let partial = 0; + let planned = 0; + for (const c of layer.components) { + if (c.maturity === 'built') built++; + else if (c.maturity === 'partial') partial++; + else planned++; + } + return { + id: layer.id, + name: layer.name, + purpose: layer.purpose, + built, + partial, + planned, + components: [...layer.components], + }; + }); + + const totals = layers.reduce( + (acc, l) => { + acc.built += l.built; + acc.partial += l.partial; + acc.planned += l.planned; + acc.components += l.components.length; + return acc; + }, + { built: 0, partial: 0, planned: 0, components: 0 } + ); + + return { + generatedAt: new Date(nowMs).toISOString(), + naming: PARTNER_NAMING, + layers, + totals, + orchestration: { + ssot: 'event-store SQLite (partners, betting_accounts, skin_events, account_authorizations, exposure_reservations) + Proton Pass + env', + clis: [ + 'ops:status', + 'ops:map', + 'domain:skins', + 'domain:books', + 'domain:host-discover', + 'partner:capacity', + 'partner:profile', + 'partner:toml', + 'partner:health', + 'partner:finance-cron', + 'partner:reconcile-kalshi', + 'partner:sync-kalshi-lifecycle', + 'partner:deliver-receipts', + 'partner:execution:preview/register/remove', + 'partner:execution:demo-collect', + 'partner:execution:demo-graduation', + 'partner:sports', + 'partner:registry', + 'partner:sync', + 'partner:vault:provision', + 'partner:pandora-probe', + 'partner:webview-ws-capture', + ], + missingForBotLoop: [ + 'partners.telegram_chat_id + topic preferences', + 'Telegram /capacity /add command router', + 'authorized execution journal projection → partner report surface', + 'legacy Fantasy402 partner_ledger settlement list adapter', + ], + }, + }; +} + +/** @deprecated Use {@link buildOpsStatusReport} */ +export function buildDomainStatusReport(nowMs = Date.now()): OpsStatusReport { + return buildOpsStatusReport(nowMs); +} + +export function formatOpsStatusText(report: OpsStatusReport): string { + const lines: string[] = []; + lines.push( + `seat ops built=${report.totals.built} partial=${report.totals.partial} planned=${report.totals.planned} (${report.totals.components} components)` + ); + for (const layer of report.layers) { + lines.push(`\n## ${layer.name} [${layer.built}✓ ${layer.partial}~ ${layer.planned}…]`); + lines.push(` ${layer.purpose}`); + for (const c of layer.components) { + const mark = c.maturity === 'built' ? '✓' : c.maturity === 'partial' ? '~' : '·'; + lines.push(` ${mark} ${c.name}`); + lines.push(` ${c.where}`); + if (c.notes) lines.push(` note: ${c.notes}`); + } + } + lines.push('\n## Orchestration gaps'); + for (const m of report.orchestration.missingForBotLoop) { + lines.push(` · ${m}`); + } + lines.push('\n## Naming'); + lines.push(` out: ${PARTNER_NAMING.outIdExample}`); + lines.push(` vault: ${PARTNER_NAMING.vaultIdExample}`); + lines.push(` liquidity: ${PARTNER_NAMING.liquidityKeyExample}`); + return lines.join('\n'); +} + +/** @deprecated Use {@link formatOpsStatusText} */ +export function formatDomainStatusText(report: OpsStatusReport): string { + return formatOpsStatusText(report); +} + +/** + * Reusable architecture map for the permissioned partner execution boundary. + * Dashed edges are expansion contracts, never claims of live provider wiring. + */ +export function formatPartnerExpansionMermaid(): string { + return `flowchart LR + PARTNER[Partner representative] + TELEGRAM[Telegram group/topic] + REQUEST[Authorization request + policy hash] + GRANT[Active SQLite grant] + HTTP[Authenticated compliance boundary] + GATE[Authorization + risk + stake gate] + KALSHI[Kalshi V2 execution] + LIFECYCLE[Reconciliation + lifecycle] + JOURNAL[Immutable journal] + RECEIPT[Durable receipt outbox] + POLYDATA[Polymarket Gamma market data] + REGINTEL[Regulatory line-move intelligence] + POLYEXEC["Polymarket execution adapter
not implemented"] + FANTASY["Fantasy402 execution
not authorized/wired"] + + PARTNER --> TELEGRAM + TELEGRAM --> REQUEST + REQUEST --> GRANT + GRANT --> GATE + HTTP --> GATE + GATE --> KALSHI + KALSHI --> LIFECYCLE + LIFECYCLE --> JOURNAL + JOURNAL --> RECEIPT + RECEIPT --> TELEGRAM + POLYDATA --> REGINTEL + REGINTEL -. intelligence only .-> HTTP + GATE -. future provider-parity contract .-> POLYEXEC + GATE -. blocked pending idempotency contract .-> FANTASY`; +} diff --git a/src/partner/dashboard-data.ts b/src/partner/dashboard-data.ts index df31657b..9ba4c7f7 100644 --- a/src/partner/dashboard-data.ts +++ b/src/partner/dashboard-data.ts @@ -3,7 +3,7 @@ */ // @see https://bun.com/docs/runtime/sqlite import type { Database } from "bun:sqlite"; -import { buildDomainStatusReport } from "./domain.ts"; +import { buildOpsStatusReport } from "./architecture.ts"; import { computeProviderCapacity, ensurePartnerRegistrySchema, @@ -206,7 +206,7 @@ export async function buildPartnerDashboardSnapshot( const accounts = listActiveBettingAccounts(db); const capacity = computeProviderCapacity(accounts); const env = checkPartnersEnvPresence(accounts); - const domain = buildDomainStatusReport(); + const domain = buildOpsStatusReport(); const ledgerFreshness = listLedgerFreshness(db); const risk = evaluateRiskHealth(db, accounts); const riskOk = riskOkUnderThreshold(risk, threshold); diff --git a/src/partner/domain.ts b/src/partner/domain.ts index 1298cf27..181ecfac 100644 --- a/src/partner/domain.ts +++ b/src/partner/domain.ts @@ -1,442 +1,28 @@ /** - * Partner domain architecture SSOT (five layers). + * @deprecated Import from `./architecture.ts`. * - * Partner → Communication → Accounts/Outs → Assets → Finance + * Seat-ops architecture (not desk matrix `src/domain/`). + * Remove this shim after callers migrate (target: next release). * - * This module is the machine-readable map of **what exists in Kalshi-bot** vs - * aspirational seat-capital / Telegram onboarding. Keep status honest. - * - * @see docs/PARTNER-DOMAIN.md - */ - -export type DomainLayerId = 'partner' | 'communication' | 'accounts' | 'assets' | 'finance'; - -export type DomainMaturity = 'built' | 'partial' | 'planned'; - -export type DomainComponent = { - id: string; - name: string; - maturity: DomainMaturity; - /** Code / table / CLI path */ - where: string; - notes?: string; -}; - -export type DomainLayer = { - id: DomainLayerId; - name: string; - purpose: string; - components: DomainComponent[]; -}; - -/** Naming conventions (do not invent alternate forms). */ -export const PARTNER_NAMING = { - partnerCode: 'UPPER short code', - partnerCodeExample: 'SPEN', - outId: 'out-{partnerCode}-{seq}', - outIdExample: 'out-SPEN-1', - bookId: 'desk brand under skin (host-derived)', - bookIdExample: 'fantasy402', - skinId: 'white-label family', - skinIdExample: 'buckeye', - vaultId: 'vault-{outId}', - vaultIdExample: 'vault-out-SPEN-1', - liquidityKey: '{outId}@{skin}', - liquidityKeyExample: 'out-SPEN-1@ezlive', - avatarFile: '{partnerCode}.svg|png', - avatarExample: 'SPEN.png', - envPrefix: '{BOOK}_{CODE}_{N}_ e.g. FANTASY402_SPEN_1_', -} as const; - -/** - * Canonical five-layer map for agents and `partner:domain`. - * Update maturity when shipping — never mark planned as built. - */ -export const PARTNER_DOMAIN_LAYERS: readonly DomainLayer[] = [ - { - id: 'partner', - name: 'Partner', - purpose: 'Financial entity that owns outs and receives split/commission.', - components: [ - { - id: 'partners-table', - name: 'partners table', - maturity: 'built', - where: 'event-store · partners (id, name, active, profit_split, commission_rate)', - }, - { - id: 'partners-toml', - name: 'partners.toml (Bun.TOML)', - maturity: 'built', - where: 'config/partners.example.toml · partner:toml · src/partner/toml-config.ts', - notes: 'Non-secret SSOT on disk; seed registry without FANTASY402_SKINS_JSON', - }, - { - id: 'partner-entity', - name: 'PartnerEntity type', - maturity: 'built', - where: 'src/partner/registry.ts', - }, - { - id: 'partner-code', - name: 'partnerCode on out meta', - maturity: 'partial', - where: 'betting_accounts.meta_json.partnerCode', - notes: 'Set on seed (FANTASY402_PARTNER_CODE); not a dedicated partners.code column yet', - }, - { - id: 'partner-color-avatar', - name: 'Deterministic color + avatar (Bun.color)', - maturity: 'built', - where: 'src/partner/visuals.ts · partner:profile [--png]', - notes: 'HSL→hex/rgba/ansi-16m + SVG/PNG via Bun.Image; contrast text', - }, - { - id: 'glossary-partner-ops', - name: 'Partner-ops glossary ids', - maturity: 'built', - where: 'src/institutions/glossary.ts · partners:validate', - }, - ], - }, - { - id: 'communication', - name: 'Communication', - purpose: 'Partner-facing chat, alerts, bot commands, digests.', - components: [ - { - id: 'telegram-bot', - name: 'Telegram long-poll bot', - maturity: 'partial', - where: 'src/telegram/bot.ts', - notes: 'Calibration digest/dashboard plus permissioned /approve and /revoke_out routing', - }, - { - id: 'telegram-authorization-flow', - name: 'Telegram authorization + durable receipt outbox', - maturity: 'built', - where: 'src/telegram/authorization-*.ts · src/partner/authorization/', - notes: - 'Numeric chat/topic/user binding, hash-verified grants, revocation evidence, retries', - }, - { - id: 'telegram-subscribers', - name: 'Subscriber list', - maturity: 'partial', - where: 'src/telegram/subscribers.ts · research/telegram-subscribers.json', - }, - { - id: 'partner-telegram-link', - name: 'partners.telegram_chat_id / topicId', - maturity: 'partial', - where: 'account_authorization_requests · account_authorizations', - notes: - 'Authorization provenance is bound; partner-level channel preferences remain planned', - }, - { - id: 'inventory-telegram', - name: 'New-event Telegram notify', - maturity: 'partial', - where: 'tools/watch-fantasy-events.ts', - }, - { - id: 'execution-operations-workers', - name: 'Independent execution workers + breaker receipts', - maturity: 'built', - where: - 'partner:reconcile-kalshi · partner:sync-kalshi-lifecycle · partner:deliver-receipts · partner:execution:register', - notes: - 'Leased reconciliation and receipts plus account lifecycle ingestion run independently of Telegram polling', - }, - ], - }, - { - id: 'accounts', - name: 'Accounts / Outs', - purpose: 'Betting accounts (outs) with multi-skin limits and book adapter.', - components: [ - { - id: 'betting-accounts', - name: 'betting_accounts table', - maturity: 'built', - where: 'event-store · out id, provider, max_stake, meta_json', - }, - { - id: 'out-skin-matrix', - name: 'Out × skin capacity', - maturity: 'built', - where: 'src/partner/skins.ts · partner:capacity', - }, - { - id: 'fantasy-adapter', - name: 'Fantasy402 Ultra adapter', - maturity: 'partial', - where: 'src/partner/fantasy-ultra/', - notes: 'Login, inventory, Pandora coeffs; placeOrder live only with HAR map URL', - }, - { - id: 'sports-inventory', - name: 'Sports + leagues inventory', - maturity: 'built', - where: 'partner:sports · 30 stream buckets · 4 primary API ids', - }, - { - id: 'concentration-router', - name: 'selectAccountForProposal', - maturity: 'partial', - where: 'listEligibleOutSkinPairs · concentrationByOut', - notes: 'Helpers only — no full proposal router CLI yet', - }, - { - id: 'authorized-execution-wrapper', - name: 'Transactional authorized execution wrapper', - maturity: 'built', - where: 'src/partner/execution/', - notes: - 'Immediate reservation transaction, gate recheck, idempotent dispatch, ambiguous-outcome reconciliation, and durable receipts', - }, - { - id: 'provider-execution-bindings', - name: 'Live provider execution bindings', - maturity: 'built', - where: - 'src/partner/execution/kalshi*.ts · src/bot/kalshi-client.ts · src/research/serve.ts', - notes: - 'Kalshi V2 mapper, out-scoped client, authorized order/cancel routes, complete reconciliation evidence, provider lifecycle accounting, and demo proof tooling; production remains separately armed only after real soak review', - }, - ], - }, - { - id: 'assets', - name: 'Assets', - purpose: 'Credentials, vault pointers, visual identity (no secrets in DB).', - components: [ - { - id: 'env-prefix', - name: 'env_prefix on out', - maturity: 'built', - where: 'betting_accounts.env_prefix', - }, - { - id: 'vault-meta', - name: 'meta.vaultId', - maturity: 'partial', - where: 'meta_json.vaultId on seed', - }, - { - id: 'proton-provision', - name: 'Proton Pass provision', - maturity: 'partial', - where: 'partner:vault:provision · .env.protonpass', - notes: 'Custom item Fantasy402; per-out vault optional', - }, - { - id: 'credentials-boundary', - name: 'Secrets never in SQLite', - maturity: 'built', - where: 'registry seed + account-profile env/Pass only', - }, - ], - }, - { - id: 'finance', - name: 'Finance', - purpose: 'Ledger, P&L, splits, partner reports.', - components: [ - { - id: 'partner-finance-cron', - name: 'Desk finance cron (registry-driven)', - maturity: 'built', - where: 'partner:finance-cron · PARTNER_FINANCE_CRON=1 in cron-main', - notes: 'Capacity + env + inventory + ticket open/settled rollup; net P&L needs list URL', - }, - { - id: 'partner-ledger', - name: 'partner_ledger', - maturity: 'partial', - where: 'src/partner/ledger.ts · desk_snapshot · odds_book · ticket', - notes: 'No remote settlement list API — ticket via partner:ingest-tickets (betGroups)', - }, - { - id: 'authorized-execution-journal', - name: 'Immutable authorized-execution journal + projections', - maturity: 'built', - where: 'src/partner/execution/execution-journal.ts · provider-lifecycle-journal.ts', - notes: - 'Integer cash, exposure, realized P&L, fees, and partner split by partner/out/skin; provider drift remains fail-closed', - }, - { - id: 'ledger-types', - name: 'Generic ledger types', - maturity: 'partial', - where: 'src/institutions/ledger-types.ts', - notes: 'Institution types — not partner_ledger integration', - }, - { - id: 'accounting-glossary', - name: 'Accounting / event glossary', - maturity: 'built', - where: 'glossary accounting.* · event.partner.* · event.out.*', - }, - { - id: 'pnl-cron-telegram', - name: 'Daily desk + ticket totals → Telegram', - maturity: 'partial', - where: 'partner:finance-cron --notify · TELEGRAM_TOPIC_ID_{CODE}', - notes: 'Ticket risk/toWin when ingested; settled net P&L still planned', - }, - ], - }, -] as const; - -export type DomainStatusReport = { - generatedAt: string; - naming: typeof PARTNER_NAMING; - layers: Array<{ - id: DomainLayerId; - name: string; - purpose: string; - built: number; - partial: number; - planned: number; - components: DomainComponent[]; - }>; - totals: { built: number; partial: number; planned: number; components: number }; - orchestration: { - ssot: string; - clis: string[]; - missingForBotLoop: string[]; - }; -}; - -export function buildDomainStatusReport(nowMs = Date.now()): DomainStatusReport { - const layers = PARTNER_DOMAIN_LAYERS.map(layer => { - let built = 0; - let partial = 0; - let planned = 0; - for (const c of layer.components) { - if (c.maturity === 'built') built++; - else if (c.maturity === 'partial') partial++; - else planned++; - } - return { - id: layer.id, - name: layer.name, - purpose: layer.purpose, - built, - partial, - planned, - components: [...layer.components], - }; - }); - - const totals = layers.reduce( - (acc, l) => { - acc.built += l.built; - acc.partial += l.partial; - acc.planned += l.planned; - acc.components += l.components.length; - return acc; - }, - { built: 0, partial: 0, planned: 0, components: 0 } - ); - - return { - generatedAt: new Date(nowMs).toISOString(), - naming: PARTNER_NAMING, - layers, - totals, - orchestration: { - ssot: 'event-store SQLite (partners, betting_accounts, skin_events, account_authorizations, exposure_reservations) + Proton Pass + env', - clis: [ - 'partner:domain', - 'partner:capacity', - 'partner:profile', - 'partner:toml', - 'partner:health', - 'partner:finance-cron', - 'partner:reconcile-kalshi', - 'partner:sync-kalshi-lifecycle', - 'partner:deliver-receipts', - 'partner:execution:preview/register/remove', - 'partner:execution:demo-collect', - 'partner:execution:demo-graduation', - 'partner:sports', - 'partner:registry', - 'partner:sync', - 'partner:vault:provision', - 'partner:pandora-probe', - 'partner:webview-ws-capture', - ], - missingForBotLoop: [ - 'partners.telegram_chat_id + topic preferences', - 'Telegram /capacity /add command router', - 'authorized execution journal projection → partner report surface', - 'legacy Fantasy402 partner_ledger settlement list adapter', - ], - }, - }; -} - -export function formatDomainStatusText(report: DomainStatusReport): string { - const lines: string[] = []; - lines.push( - `partner domain built=${report.totals.built} partial=${report.totals.partial} planned=${report.totals.planned} (${report.totals.components} components)` - ); - for (const layer of report.layers) { - lines.push(`\n## ${layer.name} [${layer.built}✓ ${layer.partial}~ ${layer.planned}…]`); - lines.push(` ${layer.purpose}`); - for (const c of layer.components) { - const mark = c.maturity === 'built' ? '✓' : c.maturity === 'partial' ? '~' : '·'; - lines.push(` ${mark} ${c.name}`); - lines.push(` ${c.where}`); - if (c.notes) lines.push(` note: ${c.notes}`); - } - } - lines.push('\n## Orchestration gaps'); - for (const m of report.orchestration.missingForBotLoop) { - lines.push(` · ${m}`); - } - lines.push('\n## Naming'); - lines.push(` out: ${PARTNER_NAMING.outIdExample}`); - lines.push(` vault: ${PARTNER_NAMING.vaultIdExample}`); - lines.push(` liquidity: ${PARTNER_NAMING.liquidityKeyExample}`); - return lines.join('\n'); -} - -/** - * Reusable architecture map for the permissioned partner execution boundary. - * Dashed edges are expansion contracts, never claims of live provider wiring. + * `PARTNER_DOMAIN_LAYERS` is **only** re-exported here — not from partner/index.ts. */ -export function formatPartnerExpansionMermaid(): string { - return `flowchart LR - PARTNER[Partner representative] - TELEGRAM[Telegram group/topic] - REQUEST[Authorization request + policy hash] - GRANT[Active SQLite grant] - HTTP[Authenticated compliance boundary] - GATE[Authorization + risk + stake gate] - KALSHI[Kalshi V2 execution] - LIFECYCLE[Reconciliation + lifecycle] - JOURNAL[Immutable journal] - RECEIPT[Durable receipt outbox] - POLYDATA[Polymarket Gamma market data] - REGINTEL[Regulatory line-move intelligence] - POLYEXEC["Polymarket execution adapter
not implemented"] - FANTASY["Fantasy402 execution
not authorized/wired"] - - PARTNER --> TELEGRAM - TELEGRAM --> REQUEST - REQUEST --> GRANT - GRANT --> GATE - HTTP --> GATE - GATE --> KALSHI - KALSHI --> LIFECYCLE - LIFECYCLE --> JOURNAL - JOURNAL --> RECEIPT - RECEIPT --> TELEGRAM - POLYDATA --> REGINTEL - REGINTEL -. intelligence only .-> HTTP - GATE -. future provider-parity contract .-> POLYEXEC - GATE -. blocked pending idempotency contract .-> FANTASY`; -} +export { + OPS_LAYERS, + OPS_LAYERS as PARTNER_DOMAIN_LAYERS, + PARTNER_NAMING, + buildOpsStatusReport, + buildDomainStatusReport, + formatOpsStatusText, + formatDomainStatusText, + formatPartnerExpansionMermaid, + type OpsComponent, + type OpsLayer, + type OpsLayerId, + type OpsMaturity, + type OpsStatusReport, + type DomainComponent, + type DomainLayer, + type DomainLayerId, + type DomainMaturity, + type DomainStatusReport, +} from './architecture.ts'; diff --git a/src/partner/index.ts b/src/partner/index.ts index bd1c07b7..348d6ace 100644 --- a/src/partner/index.ts +++ b/src/partner/index.ts @@ -196,8 +196,10 @@ export { LIVE_PRODUCT_IDS, LIVE_PRODUCTS, LIVE_PRODUCT_SPORT_BINDINGS, + DESK_DOMAIN_ENV, PARTNER_DOMAIN_ENV, RETIRED_BARE_BOOK_DOMAIN_ENVS, + deskDomainFromEnvMap, SKIN_IDS, SKIN_SPORT_BINDINGS, SKINS, @@ -284,16 +286,16 @@ export { } from './sports-inventory.ts'; export { - PARTNER_DOMAIN_LAYERS, + OPS_LAYERS, PARTNER_NAMING, - buildDomainStatusReport, - formatDomainStatusText, - type DomainComponent, - type DomainLayer, - type DomainLayerId, - type DomainMaturity, - type DomainStatusReport, -} from './domain.ts'; + buildOpsStatusReport, + formatOpsStatusText, + type OpsComponent, + type OpsLayer, + type OpsLayerId, + type OpsMaturity, + type OpsStatusReport, +} from './architecture.ts'; export { classifyTicketStatus, diff --git a/src/partner/toml-config.ts b/src/partner/toml-config.ts index 15a827d7..15ee1235 100644 --- a/src/partner/toml-config.ts +++ b/src/partner/toml-config.ts @@ -16,7 +16,7 @@ * 1. out prefix e.g. FANTASY402_SPEN_1_BEARER_TOKEN * 2. partner prefix e.g. FANTASY402_SPEN_BEARER_TOKEN * 3. book fallback e.g. FANTASY402_BEARER_TOKEN - * 4. desk URL PARTNER_DOMAIN → SKINS Ultra-mapper default (host → SkinId) + * 4. desk URL DESK_DOMAIN (legacy PARTNER_DOMAIN) → SKINS Ultra default (host → SkinId) * * Canonical env_prefix: `{BOOK}_{CODE}_{N}_` from out id `out-SPEN-1`. * @@ -27,7 +27,7 @@ import type { Database } from 'bun:sqlite'; import { z } from 'zod'; import { - PARTNER_DOMAIN_ENV, + deskDomainFromEnvMap, getBookByHost, getSkin, getSkinByHost, @@ -46,7 +46,12 @@ import { buildSkinsMeta, type OutSkinLimit } from './skins.ts'; import { getPartnerVisual } from './visuals.ts'; import { tomlStringify } from './toml-stringify.ts'; -export { PARTNER_DOMAIN_ENV, resolveDeskDomainFromEnv } from '../domain/index.ts'; +export { + DESK_DOMAIN_ENV, + PARTNER_DOMAIN_ENV, + deskDomainFromEnvMap, + resolveDeskDomainFromEnv, +} from '../domain/index.ts'; export const DEFAULT_PARTNERS_TOML = 'config/partners.toml'; export const EXAMPLE_PARTNERS_TOML = 'config/partners.example.toml'; @@ -380,7 +385,7 @@ export function formatPartnerAssetIssues(issues: PartnerAssetIssue[]): string { * out prefix → partner prefix → book fallback * * DOMAIN is host→SkinId territory: per-out/partner `*DOMAIN` still wins; - * book-level uses PARTNER_DOMAIN only (retired bare-book DOMAIN envs ignored). + * book-level uses DESK_DOMAIN (legacy PARTNER_DOMAIN) only (retired bare-book DOMAIN envs ignored). */ export function resolvePartnerEnv( envPrefix: string | null | undefined, @@ -397,14 +402,14 @@ export function resolvePartnerEnv( for (const step of chain) { const envKey = `${step.prefix}${key}`; // Bare book prefix is often labeled source "out" when callers pass FANTASY402_ - // itself — still never read retired bare-book DOMAIN; only PARTNER_DOMAIN. + // itself — still never read retired bare-book DOMAIN; only DESK_DOMAIN / legacy PARTNER_DOMAIN. const bookLevelDomain = key === 'DOMAIN' && (step.source === 'book_fallback' || isBareBookEnvPrefix(step.prefix, provider) || isRetiredBareBookDomainEnv(envKey)); if (bookLevelDomain) { - const preferred = envMap[PARTNER_DOMAIN_ENV]?.trim(); + const preferred = deskDomainFromEnvMap(envMap); if (preferred) { values.DOMAIN = preferred; source.DOMAIN = 'book_fallback'; @@ -420,7 +425,7 @@ export function resolvePartnerEnv( } } if (key === 'DOMAIN' && !values.DOMAIN) { - const preferred = envMap[PARTNER_DOMAIN_ENV]?.trim(); + const preferred = deskDomainFromEnvMap(envMap); if (preferred) { values.DOMAIN = preferred; source.DOMAIN = 'book_fallback'; diff --git a/tests/domain/sports-skins-books.test.ts b/tests/domain/sports-skins-books.test.ts index cce333e2..4abfd79f 100644 --- a/tests/domain/sports-skins-books.test.ts +++ b/tests/domain/sports-skins-books.test.ts @@ -12,6 +12,7 @@ import { formatSkinMatrixMarkdownTable, getBookByHost, listBookIdsForSkin, + DESK_DOMAIN_ENV, PARTNER_DOMAIN_ENV, RETIRED_BARE_BOOK_DOMAIN_ENVS, assertActiveSkinsHaveHosts, @@ -80,12 +81,15 @@ describe('domain sports / skins / live products', () => { }); test('BookId is desk brand under skin (not SkinId)', () => { - expect(resolveBookId('fantasy402')).toBe('fantasy402'); + expect(resolveBookId('fantasy402')).toBe(resolveBookId('fantasy402')); + expect(String(resolveBookId('fantasy402'))).toBe('fantasy402'); expect(resolveSkinId('fantasy402')).toBe('buckeye'); expect(skinIdForBook('fantasy402')).toBe('buckeye'); - expect(getBookByHost('https://www.fantasy402.com/login')).toBe('fantasy402'); - expect(getBookByHost('classic.lvaction.com')).toBe('classic.lvaction.com'); - expect(listBookIdsForSkin('buckeye').sort()).toEqual(['betwest', 'fantasy402', 'hulkwager']); + expect(String(getBookByHost('https://www.fantasy402.com/login'))).toBe('fantasy402'); + expect(String(getBookByHost('classic.lvaction.com'))).toBe('classic.lvaction.com'); + expect(listBookIdsForSkin('buckeye').map(String).sort()).toEqual( + ['betwest', 'fantasy402', 'hulkwager'].sort() + ); expect(bookOffersLiveProduct('fantasy402', 'ezlive')).toBe(true); expect(bookOffersLiveProduct('parlay21', 'maglive')).toBe(true); expect(bookOffersLiveProduct('buckeye', 'ezlive')).toBe(false); // SkinId ≠ BookId @@ -129,7 +133,7 @@ describe('domain sports / skins / live products', () => { expect(requireDefaultUrlForUltraMapper()).toBe(expected); }); - test('resolveDeskDomainFromEnv uses PARTNER_DOMAIN then SKINS default', () => { + test('resolveDeskDomainFromEnv uses DESK_DOMAIN (legacy PARTNER_DOMAIN) then SKINS default', () => { const fallback = requireDefaultUrlForUltraMapper(); const retired = Object.fromEntries( RETIRED_BARE_BOOK_DOMAIN_ENVS.map(k => [k, 'https://ignored.example']) @@ -139,7 +143,14 @@ describe('domain sports / skins / live products', () => { expect( resolveDeskDomainFromEnv({ ...retired, - [PARTNER_DOMAIN_ENV]: 'https://preferred.example', + [PARTNER_DOMAIN_ENV]: 'https://legacy.example', + }) + ).toBe('https://legacy.example'); + expect( + resolveDeskDomainFromEnv({ + ...retired, + [PARTNER_DOMAIN_ENV]: 'https://legacy.example', + [DESK_DOMAIN_ENV]: 'https://preferred.example', }) ).toBe('https://preferred.example'); }); diff --git a/tests/partner/domain.test.ts b/tests/partner/domain.test.ts index 7aa31b9c..9cf78519 100644 --- a/tests/partner/domain.test.ts +++ b/tests/partner/domain.test.ts @@ -1,32 +1,35 @@ // @see https://bun.com/docs/test/index#run-tests import { describe, expect, test } from 'bun:test'; import { - PARTNER_DOMAIN_LAYERS, + OPS_LAYERS, PARTNER_NAMING, - buildDomainStatusReport, - formatDomainStatusText, + buildOpsStatusReport, + formatOpsStatusText, formatPartnerExpansionMermaid, -} from '../../src/partner/domain.ts'; +} from '../../src/partner/architecture.ts'; -describe('partner domain architecture', () => { +describe('seat-ops architecture (not desk domain matrix)', () => { test('five layers with honest maturity counts', () => { - expect(PARTNER_DOMAIN_LAYERS.map(l => l.id)).toEqual([ + expect(OPS_LAYERS.map(l => l.id)).toEqual([ 'partner', 'communication', 'accounts', 'assets', 'finance', ]); - const report = buildDomainStatusReport(); + const report = buildOpsStatusReport(); expect(report.totals.components).toBeGreaterThan(10); expect(report.totals.built).toBeGreaterThan(0); expect(report.totals.partial).toBeGreaterThan(0); expect(report.totals.planned).toBe(0); expect(report.orchestration.missingForBotLoop.length).toBeGreaterThan(0); + expect(report.orchestration.clis).toContain('ops:status'); + expect(report.orchestration.clis).toContain('domain:skins'); + expect(report.orchestration.clis).not.toContain('domain:status'); expect(PARTNER_NAMING.outIdExample).toBe('out-SPEN-1'); expect(PARTNER_NAMING.bookIdExample).toBe('fantasy402'); expect(PARTNER_NAMING.skinIdExample).toBe('buckeye'); - expect(formatDomainStatusText(report)).toContain('partner domain'); + expect(formatOpsStatusText(report)).toContain('seat ops'); }); test('expansion map distinguishes built execution from intelligence and unwired providers', () => { diff --git a/tests/partner/execution/kalshi-live.test.ts b/tests/partner/execution/kalshi-live.test.ts index 6b9e1301..f61656e7 100644 --- a/tests/partner/execution/kalshi-live.test.ts +++ b/tests/partner/execution/kalshi-live.test.ts @@ -248,20 +248,23 @@ function setup(variant?: "inactive" | "partner" | "provider"): Database { commissionRate: null, notes: null, }, NOW_MS); + // fantasy402 requires host→skin; kalshi may omit url (requireHost only when url or fantasy402) + const isFantasy = variant === "provider"; upsertBettingAccount(db, { id: "out-SPORTS-1", partnerId: "partner-sports", - provider: variant === "provider" ? "fantasy402" : "kalshi", - url: "", + provider: isFantasy ? "fantasy402" : "kalshi", + url: isFantasy ? "https://www.fantasy402.com" : "", status: variant === "inactive" ? "inactive" : "active", - envPrefix: "KALSHI_SPORTS_1_", + envPrefix: isFantasy ? "FANTASY402_SPORTS_1_" : "KALSHI_SPORTS_1_", maxStake: 5, maxWin: 20, currency: "USD", skin: null, metaJson: JSON.stringify({ partnerCode: "SPORTS", - skins: [{ name: "main", perBetMax: 5, maxWin: 20, active: true }], + skins: [{ name: isFantasy ? "ezlive" : "main", perBetMax: 5, maxWin: 20, active: true }], + ...(isFantasy ? { skinId: "buckeye", bookId: "fantasy402" } : {}), }), }, NOW_MS); const policy: AuthorizationPolicy = { diff --git a/tests/partner/out-identity.test.ts b/tests/partner/out-identity.test.ts index ccea0776..5c297c00 100644 --- a/tests/partner/out-identity.test.ts +++ b/tests/partner/out-identity.test.ts @@ -29,7 +29,7 @@ describe('out-identity boundary', () => { }), }); expect(identity?.skinId).toBe('buckeye'); - expect(identity?.bookId).toBe('fantasy402'); + expect(String(identity?.bookId)).toBe('fantasy402'); expect(identity?.adapter.adapterId).toBe('fantasy-ultra'); expect(identity?.adapter.mapperKind).toBe('fantasy402'); expect(identity?.adapter.bookEnvToken).toBe('FANTASY402'); @@ -86,7 +86,7 @@ describe('out-identity boundary', () => { }), }); expect(identity?.skinId).toBe('ace'); - expect(identity?.bookId).toBe('parlay21'); + expect(String(identity?.bookId)).toBe('parlay21'); expect(identity?.adapter.adapterId).toBe('unmapped'); expect(identity?.adapter.mapperKind).toBe('unmapped'); }); diff --git a/tests/partner/registry.test.ts b/tests/partner/registry.test.ts index a6a210e5..d39acbfc 100644 --- a/tests/partner/registry.test.ts +++ b/tests/partner/registry.test.ts @@ -222,7 +222,7 @@ describe('partner registry', () => { ]), }); expect(seeded?.skinId).toBe('buckeye'); - expect(seeded?.bookId).toBe('fantasy402'); + expect(String(seeded?.bookId)).toBe('fantasy402'); expect(seeded?.mapper).toBe('fantasy402'); expect(seeded?.adapterId).toBe('fantasy-ultra'); expect(seeded?.metaJson).toContain('"skinId":"buckeye"'); @@ -230,7 +230,7 @@ describe('partner registry', () => { expect(seeded?.metaJson).toContain('"liveProducts"'); expect(seeded?.metaJson).toContain('"defaultLiveProduct"'); const byId = getBettingAccountById(db, seeded!.id); - expect(byId?.bookId).toBe('fantasy402'); + expect(String(byId?.bookId)).toBe('fantasy402'); expect(byId?.skinId).toBe('buckeye'); }); diff --git a/tests/partner/skin-events-store.test.ts b/tests/partner/skin-events-store.test.ts index 0bbcef71..2be4dfea 100644 --- a/tests/partner/skin-events-store.test.ts +++ b/tests/partner/skin-events-store.test.ts @@ -62,7 +62,7 @@ describe('skin_events store', () => { expect(liveProductsCoveredByInventory('buckeye').sort()).toEqual(['ezlive', 'plive']); const id = buckeyeInventoryIdentity(); expect(id.skinId).toBe('buckeye'); - expect(id.bookId).toBe('fantasy402'); + expect(String(id.bookId)).toBe('fantasy402'); expect(id.inventoryLiveProduct).toBe('plive'); }); @@ -89,7 +89,7 @@ describe('skin_events store', () => { expect(r1.updated.length).toBe(0); expect(listSkinInventoryIds(db, 'fantasy402').size).toBe(2); expect(r1.inserted[0]?.skinId).toBe('buckeye'); - expect(r1.inserted[0]?.bookId).toBe('fantasy402'); + expect(String(r1.inserted[0]?.bookId)).toBe('fantasy402'); expect(r1.inserted[0]?.inventoryLiveProduct).toBe('plive'); expect(r1.inserted[0]?.sport).toBe('table_tennis'); expect(r1.inserted[0]?.competitionId).toBe('table_tennis.setka_cup'); @@ -260,7 +260,7 @@ describe('skin_events store', () => { new Response(JSON.stringify(wire), { status: 200, headers: { 'content-type': 'application/json' }, - })) as typeof fetch, + })) as unknown as typeof fetch, }); expect(events.length).toBe(1); expect(events[0]?.inventoryId).toBe('55'); diff --git a/tests/partner/toml-config.test.ts b/tests/partner/toml-config.test.ts index 7297073f..827d3790 100644 --- a/tests/partner/toml-config.test.ts +++ b/tests/partner/toml-config.test.ts @@ -8,6 +8,7 @@ import { import { openEventStore } from '../../src/institutions/event-store/open-db.ts'; import { computeProviderCapacity, listActiveBettingAccounts } from '../../src/partner/registry.ts'; import { + DESK_DOMAIN_ENV, PARTNER_DOMAIN_ENV, canonicalOutEnvPrefix, canonicalPartnerEnvPrefix, @@ -62,7 +63,7 @@ describe('partners TOML (Bun.TOML)', () => { expect(accounts[0]?.metaJson).not.toContain('password'); // Ultra default url → fantasy402 book + buckeye skin stamped at materialize expect(accounts[0]?.skinId).toBe('buckeye'); - expect(accounts[0]?.bookId).toBe('fantasy402'); + expect(String(accounts[0]?.bookId)).toBe('fantasy402'); expect(accounts[0]?.metaJson).toContain('"skinId":"buckeye"'); expect(accounts[0]?.metaJson).toContain('"bookId":"fantasy402"'); }); @@ -82,7 +83,7 @@ book_id = "fantasy402" skins = [{ name = "ezlive", per_bet_max = 500, max_win = 2500, active = true }] `); const ok = materializePartnersToml(withUrl); - expect(ok.accounts[0]?.bookId).toBe('fantasy402'); + expect(String(ok.accounts[0]?.bookId)).toBe('fantasy402'); expect(ok.accounts[0]?.skinId).toBe('buckeye'); const conflict = parsePartnersToml(` @@ -209,7 +210,7 @@ skins = [{ name = "ezlive", per_bet_max = "not-a-number" }] expect(base.source.CUSTOMER_ID).toBe('out'); }); - test('resolvePartnerEnv: retired bare-book DOMAIN ignored; PARTNER_DOMAIN / per-out DOMAIN win', () => { + test('resolvePartnerEnv: retired bare-book DOMAIN ignored; DESK_DOMAIN / per-out DOMAIN win', () => { const retired = Object.fromEntries( RETIRED_BARE_BOOK_DOMAIN_ENVS.map(k => [k, 'https://evil.example']) ); @@ -225,17 +226,25 @@ skins = [{ name = "ezlive", per_bet_max = "not-a-number" }] const aceDesk = defaultUrlForSkin('ace')!; const lonestarDesk = urlForHost('lonestarwagering.com'); - const withPartnerDomain = resolvePartnerEnv('FANTASY402_', { + const withLegacyDomain = resolvePartnerEnv('FANTASY402_', { ...ignored, [PARTNER_DOMAIN_ENV]: aceDesk, }); - expect(withPartnerDomain.values.DOMAIN).toBe(aceDesk); - expect(withPartnerDomain.source.DOMAIN).toBe('book_fallback'); + expect(withLegacyDomain.values.DOMAIN).toBe(aceDesk); + expect(withLegacyDomain.source.DOMAIN).toBe('book_fallback'); + + const withDeskDomain = resolvePartnerEnv('FANTASY402_', { + ...ignored, + [PARTNER_DOMAIN_ENV]: 'https://legacy-should-lose.example', + [DESK_DOMAIN_ENV]: aceDesk, + }); + expect(withDeskDomain.values.DOMAIN).toBe(aceDesk); + expect(withDeskDomain.source.DOMAIN).toBe('book_fallback'); const perOut = resolvePartnerEnv('FANTASY402_SPEN_1_', { ...ignored, FANTASY402_SPEN_1_DOMAIN: lonestarDesk, - [PARTNER_DOMAIN_ENV]: aceDesk, + [DESK_DOMAIN_ENV]: aceDesk, }); expect(perOut.values.DOMAIN).toBe(lonestarDesk); expect(perOut.source.DOMAIN).toBe('out'); diff --git a/tests/partner/widget-odds.test.ts b/tests/partner/widget-odds.test.ts index 34fc8bb8..d9675067 100644 --- a/tests/partner/widget-odds.test.ts +++ b/tests/partner/widget-odds.test.ts @@ -35,7 +35,8 @@ describe("fantasy widget sport map", () => { test("seed provider_sport_mappings", () => { const db = openEventStore({ dbPath: ":memory:" }); const n = seedFantasySportMappings(db); - expect(n).toBe(FANTASY_SPORT_MAPPINGS.length); + // plive + ezlive + legacy fantasy402 dual-write (see sports-skins-books dual-write test) + expect(n).toBeGreaterThanOrEqual(FANTASY_SPORT_MAPPINGS.length * 3); const row = db .query( `SELECT api_sport_id AS api, widget_sport_id AS widget FROM provider_sport_mappings diff --git a/tests/research/trading-order.test.ts b/tests/research/trading-order.test.ts index 1524139f..30af4e40 100644 --- a/tests/research/trading-order.test.ts +++ b/tests/research/trading-order.test.ts @@ -326,7 +326,7 @@ function fantasyDatabase(): Database { id: "out-SPORTS-1", partnerId: "partner-sports", provider: "fantasy402", - url: "", + url: "https://www.fantasy402.com", status: "active", envPrefix: "FANTASY402_SPORTS_1_", maxStake: 5, @@ -335,7 +335,9 @@ function fantasyDatabase(): Database { skin: null, metaJson: JSON.stringify({ partnerCode: "SPORTS", - skins: [{ name: "main", perBetMax: 5, maxWin: 20, active: true }], + skinId: "buckeye", + bookId: "fantasy402", + skins: [{ name: "ezlive", perBetMax: 5, maxWin: 20, active: true }], }), }); return db; diff --git a/tools/domain-cli.ts b/tools/domain-cli.ts new file mode 100644 index 00000000..9ba293f6 --- /dev/null +++ b/tools/domain-cli.ts @@ -0,0 +1,81 @@ +#!/usr/bin/env bun +/** + * Desk domain matrix CLIs only (host → book → skin). + * + * bun run domain:skins + * bun run domain:skins -- --json + * bun run domain:books + * bun run domain:books -- --json + * + * Seat-ops status / expansion map: bun run ops:status · bun run ops:map + * Host discover: bun run domain:host-discover + * Legacy: partner:skins · partner:books + * + * @see src/domain/README.md + * @see src/domain/skin-matrix.ts + * @see src/domain/books.ts + */ +import { + buildBookMatrixRows, + buildSkinMatrixRows, + formatBooksMatrixText, + formatSkinMatrixText, +} from '../src/domain/index.ts'; + +const json = process.argv.includes('--json'); +const skins = process.argv.includes('--skins') || process.argv.includes('--skin-matrix'); +const books = process.argv.includes('--books') || process.argv.includes('--book-matrix'); + +if (skins && books) throw new TypeError('Choose one of --skins or --books'); +if (!skins && !books) { + throw new TypeError( + 'Desk matrix CLI requires --skins or --books. For seat-ops use: bun run ops:status' + ); +} + +if (skins) { + const rows = buildSkinMatrixRows(); + if (json) { + console.log( + JSON.stringify( + { + skins: rows, + count: rows.length, + withGaps: rows.filter(r => r.gaps.length > 0).length, + fingerprintPending: rows.filter(r => r.fingerprintPending).map(r => r.skinId), + }, + null, + 2 + ) + ); + } else { + console.log(formatSkinMatrixText(rows)); + console.log('\n · SSOT: src/domain/skins.ts · docs: src/domain/README.md'); + } + process.exit(0); +} + +// books +const rows = buildBookMatrixRows(); +if (json) { + const bySkin: Record = {}; + for (const r of rows) { + const list = bySkin[r.skinId] ?? []; + list.push(r.bookId); + bySkin[r.skinId] = list; + } + console.log( + JSON.stringify( + { + books: rows, + count: rows.length, + bySkin, + }, + null, + 2 + ) + ); +} else { + console.log(formatBooksMatrixText(rows)); + console.log('\n · SSOT: src/domain/books.ts · docs: src/domain/README.md'); +} diff --git a/tools/partner-host-discover.ts b/tools/host-discover-cli.ts similarity index 92% rename from tools/partner-host-discover.ts rename to tools/host-discover-cli.ts index 6407fad0..4efb1b68 100644 --- a/tools/partner-host-discover.ts +++ b/tools/host-discover-cli.ts @@ -2,11 +2,13 @@ /** * Discover likely SkinId / adapter for any host (suggest only). * - * bun run partner:host-discover -- --url=https://BOOK.example - * bun run partner:host-discover -- --url=https://BOOK.example --weigh --json - * bun run partner:host-discover -- --url=https://BOOK.example --har=./session.har - * bun run partner:host-discover -- --all - * bun run partner:host-discover -- --url=https://BOOK.example --compare + * bun run domain:host-discover -- --url=https://BOOK.example + * bun run domain:host-discover -- --url=https://BOOK.example --weigh --json + * bun run domain:host-discover -- --url=https://BOOK.example --har=./session.har + * bun run domain:host-discover -- --all + * bun run domain:host-discover -- --url=https://BOOK.example --compare + * + * Legacy alias: partner:host-discover * * `--all` / `--compare` walk every apex host in SKINS[].hosts (plus optional --url target). * `--weigh` prints capped-category evidence breakdown (report.weigh / report.decision). diff --git a/tools/ops-cli.ts b/tools/ops-cli.ts new file mode 100644 index 00000000..6ad3a1ed --- /dev/null +++ b/tools/ops-cli.ts @@ -0,0 +1,52 @@ +#!/usr/bin/env bun +/** + * Seat-ops architecture status + expansion map (not desk matrix). + * + * bun run ops:status + * bun run ops:status -- --json + * bun run ops:map + * bun run ops:map -- --output=artifacts/partner-expansion.mmd + * + * Legacy: partner:domain · partner:map + * Desk matrix: domain:skins · domain:books · domain:host-discover + * + * @see docs/PARTNER-DOMAIN.md + * @see src/partner/architecture.ts + */ +import { + buildOpsStatusReport, + formatOpsStatusText, + formatPartnerExpansionMermaid, +} from '../src/partner/architecture.ts'; +import { mkdir } from 'node:fs/promises'; +import { dirname } from 'node:path'; + +const json = process.argv.includes('--json'); +const map = process.argv.includes('--map') || process.argv.includes('--mermaid'); +const outputArg = process.argv.find(arg => arg.startsWith('--output=')); +const outputPath = outputArg?.slice('--output='.length).trim(); +if (json && map) throw new TypeError('Choose one of --json or --map/--mermaid'); +if (outputArg && !outputPath) throw new TypeError('--output requires a path'); + +if (map) { + const content = `${formatPartnerExpansionMermaid()}\n`; + if (outputPath) { + await mkdir(dirname(outputPath), { recursive: true }); + await Bun.write(outputPath, content); + console.error(`wrote partner expansion map: ${outputPath}`); + } else { + console.log(content.trimEnd()); + } + process.exit(0); +} + +const report = buildOpsStatusReport(); +if (json) { + console.log(JSON.stringify(report, null, 2)); +} else { + console.log(formatOpsStatusText(report)); + console.log('\n · docs: docs/PARTNER-DOMAIN.md · desk matrix: src/domain/README.md'); + console.log(' · skin matrix: bun run domain:skins'); + console.log(' · book matrix: bun run domain:books'); + console.log(' · host discover: bun run domain:host-discover -- --url=https://…'); +} diff --git a/tools/partner-domain.ts b/tools/partner-domain.ts deleted file mode 100644 index 02b766db..00000000 --- a/tools/partner-domain.ts +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bun -/** - * Print Partner domain architecture status (five layers) + skin/book matrices. - * - * bun run partner:domain - * bun run partner:domain -- --json - * bun run partner:domain -- --skins - * bun run partner:domain -- --skins --json - * bun run partner:domain -- --books - * bun run partner:domain -- --books --json - * bun run partner:map - * bun run partner:map -- --output=artifacts/partner-expansion.mmd - * - * @see docs/PARTNER-DOMAIN.md - * @see src/partner/domain.ts - * @see src/domain/skin-matrix.ts - * @see src/domain/books.ts - */ -import { - buildBookMatrixRows, - buildSkinMatrixRows, - formatBooksMatrixText, - formatSkinMatrixText, -} from '../src/domain/index.ts'; -import { - buildDomainStatusReport, - formatDomainStatusText, - formatPartnerExpansionMermaid, -} from '../src/partner/domain.ts'; -import { mkdir } from 'node:fs/promises'; -import { dirname } from 'node:path'; - -const json = process.argv.includes('--json'); -const skins = process.argv.includes('--skins') || process.argv.includes('--skin-matrix'); -const books = process.argv.includes('--books') || process.argv.includes('--book-matrix'); -const map = process.argv.includes('--map') || process.argv.includes('--mermaid'); -const outputArg = process.argv.find(arg => arg.startsWith('--output=')); -const outputPath = outputArg?.slice('--output='.length).trim(); -if (json && map) throw new TypeError('Choose one of --json or --map/--mermaid'); -if (skins && map) throw new TypeError('Choose one of --skins or --map/--mermaid'); -if (books && map) throw new TypeError('Choose one of --books or --map/--mermaid'); -if (skins && books) throw new TypeError('Choose one of --skins or --books'); -if (outputArg && !outputPath) throw new TypeError('--output requires a path'); - -if (map) { - const content = `${formatPartnerExpansionMermaid()}\n`; - if (outputPath) { - await mkdir(dirname(outputPath), { recursive: true }); - await Bun.write(outputPath, content); - console.error(`wrote partner expansion map: ${outputPath}`); - } else { - console.log(content.trimEnd()); - } - process.exit(0); -} - -if (skins) { - const rows = buildSkinMatrixRows(); - if (json) { - console.log( - JSON.stringify( - { - skins: rows, - count: rows.length, - withGaps: rows.filter(r => r.gaps.length > 0).length, - fingerprintPending: rows.filter(r => r.fingerprintPending).map(r => r.skinId), - }, - null, - 2 - ) - ); - } else { - console.log(formatSkinMatrixText(rows)); - console.log('\n · SSOT: src/domain/skins.ts · docs: src/domain/README.md'); - } - process.exit(0); -} - -if (books) { - const rows = buildBookMatrixRows(); - if (json) { - const bySkin: Record = {}; - for (const r of rows) { - const list = bySkin[r.skinId] ?? []; - list.push(r.bookId); - bySkin[r.skinId] = list; - } - console.log( - JSON.stringify( - { - books: rows, - count: rows.length, - bySkin, - }, - null, - 2 - ) - ); - } else { - console.log(formatBooksMatrixText(rows)); - console.log('\n · SSOT: src/domain/books.ts · docs: src/domain/README.md'); - } - process.exit(0); -} - -const report = buildDomainStatusReport(); - -if (json) { - console.log(JSON.stringify(report, null, 2)); -} else { - console.log(formatDomainStatusText(report)); - console.log('\n · docs: docs/PARTNER-DOMAIN.md'); - console.log(' · skin matrix: bun run partner:domain -- --skins'); - console.log(' · book matrix: bun run partner:domain -- --books'); -} diff --git a/tools/partner-health.ts b/tools/partner-health.ts index 76fc65bb..63865d3f 100644 --- a/tools/partner-health.ts +++ b/tools/partner-health.ts @@ -12,7 +12,7 @@ // @see https://bun.com/docs/runtime/sqlite import { openEventStore } from "../src/institutions/event-store/open-db.ts"; import { DEFAULT_EVENT_STORE_DB } from "../src/institutions/event-store/paths.ts"; -import { buildDomainStatusReport } from "../src/partner/domain.ts"; +import { buildOpsStatusReport } from "../src/partner/architecture.ts"; import { computeProviderCapacity, ensurePartnerRegistrySchema, @@ -54,7 +54,7 @@ async function main(): Promise { const accounts = listActiveBettingAccounts(db); const capacity = computeProviderCapacity(accounts); const env = checkPartnersEnvPresence(accounts); - const domain = buildDomainStatusReport(); + const domain = buildOpsStatusReport(); const ledgerFreshness = listLedgerFreshness(db); const riskThreshold = parseRiskThreshold( argValue("risk-threshold") ?? process.env.PARTNER_FINANCE_RISK_THRESHOLD, diff --git a/tools/provision-fantasy402-vault.ts b/tools/provision-fantasy402-vault.ts index 5dcc4895..7979834f 100644 --- a/tools/provision-fantasy402-vault.ts +++ b/tools/provision-fantasy402-vault.ts @@ -284,7 +284,7 @@ async function main(): Promise { ` ${envPrefix}CUSTOMER_ID ${envPrefix}AGENT_ID`, ` ${envPrefix}PASSWORD ${envPrefix}BEARER_TOKEN`, "Fallback chain also accepts partner- or book-level keys.", - "Optional: PARTNER_DOMAIN (or per-out *DOMAIN → SKINS host/SkinId) SKIN CURRENCY", + "Optional: DESK_DOMAIN (legacy PARTNER_DOMAIN; or per-out *DOMAIN → SKINS host/SkinId) SKIN CURRENCY", "", "Export them in this shell (or paste from browser DevTools JWT), then re-run with --apply.", "Do not commit secrets. Prefer short-lived JWT + renewToken.",