diff --git a/README.md b/README.md index f3c6de6..6526e9c 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Standalone Bun project for discovering and ranking public [Kalshi](https://kalsh - `bun install --frozen-lockfile` - Optional secrets via [Proton Pass CLI](https://protonpass.github.io/pass-cli/) — see [`docs/PROTONPASS.md`](docs/PROTONPASS.md) - Authorized execution operators: [`docs/AUTHORIZED_EXECUTION.md`](docs/AUTHORIZED_EXECUTION.md) +- Partner/Telegram/provider expansion map: [`docs/PARTNER-EXECUTION-EXPANSION.md`](docs/PARTNER-EXECUTION-EXPANSION.md) (`bun run partner:map`) +- Documentation hub: [GitHub Wiki](https://github.com/brendadeeznuts1111/Kalshi-bot/wiki) (index only; repository docs remain canonical) ## Quick start @@ -293,6 +295,7 @@ Details: [`docs/CRON.md`](docs/CRON.md) · Bun map: [`docs/BUN_NATIVE.md`](docs/ - [`docs/PLAN.md`](docs/PLAN.md) — as-built design - [`docs/BUN_NATIVE.md`](docs/BUN_NATIVE.md) — API map - [`docs/BUN_SHELL.md`](docs/BUN_SHELL.md) — `Bun.$` patterns +- [`docs/PARTNER-EXECUTION-EXPANSION.md`](docs/PARTNER-EXECUTION-EXPANSION.md) — Telegram → partner authorization → Kalshi, with honest Polymarket expansion gates ## Dependency rule diff --git a/bun.lock b/bun.lock index 8108de1..f2be398 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ }, "devDependencies": { "@types/bun": "1.3.14", + "bun-types": "1.3.14", "typescript": "6.0.3", }, }, diff --git a/docs/AUTHORIZED_EXECUTION.md b/docs/AUTHORIZED_EXECUTION.md index 4ee0564..a0dd171 100644 --- a/docs/AUTHORIZED_EXECUTION.md +++ b/docs/AUTHORIZED_EXECUTION.md @@ -1,5 +1,8 @@ # Authorized Partner Execution +Architecture map: [`PARTNER-EXECUTION-EXPANSION.md`](PARTNER-EXECUTION-EXPANSION.md) · +CLI: `bun run partner:map` + Status: implemented, default off. This is the operator contract and runbook for the authorization, Telegram approval, exposure reservation, Kalshi mapping, and live HTTP orchestration layers. @@ -283,3 +286,10 @@ live in - Keep Fantasy402 unwired until provider-side idempotency is proven. - Add credit-line or dedicated-wallet accounting only when an owned domain contract and ledger source exist. + +## See also + +- [Partner execution expansion map](PARTNER-EXECUTION-EXPANSION.md) +- [Partner domain architecture](PARTNER-DOMAIN.md) +- [Demo proof schema](EXECUTION_DEMO_PROOF_SCHEMA.md) +- [Regulatory agents and Polymarket intelligence](regulatory-agents.md) diff --git a/docs/AUTHORIZED_EXECUTION_REMAINING_WORK.md b/docs/AUTHORIZED_EXECUTION_REMAINING_WORK.md index d4b87cd..046cc30 100644 --- a/docs/AUTHORIZED_EXECUTION_REMAINING_WORK.md +++ b/docs/AUTHORIZED_EXECUTION_REMAINING_WORK.md @@ -13,8 +13,8 @@ pass. Implementation update (2026-08-06): Waves 0–2 and the Wave 3 authoritative demo evidence collector, deterministic failure-scenario runner, daily evidence -compiler, and seven-day chain verifier are implemented on the -authorized-execution branch: operator principals, regulatory lifecycle/sync, +compiler, and seven-day chain verifier are merged into `main`: operator +principals, regulatory lifecycle/sync, leased reconciliation and stale recovery, full Kalshi evidence binding, fail-closed risk health, authorized cancellation, independent workers, canonical provider lifecycle, and the immutable journal. The collector binds @@ -52,7 +52,7 @@ integration. | W1 authenticated cancellation | closed | `executeAuthorizedCancel`, scoped credentials, current grant/risk recheck, durable intent/receipt | | W1 stale placement/evidence | closed | stale placing recovery plus cursor-complete active/historical exact-term reconciliation | | W1 workers/alerts | closed | independent reconcile/lifecycle/receipt jobs, Bun cron register/remove/preview, deduplicated breaker receipts | -| W2 provider lifecycle | closed | cursor-complete account order/fill ingestion and provider-positive settlement accounting | +| W2 provider lifecycle | closed | cursor-complete account order/fill ingestion, canonical provider direction, strict primary-account identity, exact minor-unit fees, and provider-positive settlement accounting | | W2 immutable projections | closed | append-only integer journal, deterministic source keys, reversals, partner/out/skin projections and drift | | W3 tooling | closed | authoritative demo collector, deterministic service scenarios, daily compiler, seven-day chain verifier | | W3 elapsed graduation | **open** | seven consecutive real passing demo days and human artifact review have not elapsed | diff --git a/docs/BUN_NATIVE.md b/docs/BUN_NATIVE.md index 3d977e4..61e06d2 100644 --- a/docs/BUN_NATIVE.md +++ b/docs/BUN_NATIVE.md @@ -152,11 +152,12 @@ Deep dive: [`BUN_SHELL.md`](BUN_SHELL.md) (`Bun.$` patterns) | Agent CLI | status / patterns / blueprint / tennis over `cache.db` + event-store | [`cli.ts`](../src/agent/cli.ts), [`docs/AGENT.md`](../docs/AGENT.md) | | Kalshi live poll | `dns.prefetch` + `fetch.preconnect` + `mapPool` + `Bun.nanoseconds` | [`kalshi-network.ts`](../src/bot/kalshi-network.ts), [`live-scores.ts`](../src/institutions/event-store/live-scores.ts) | | Tennis canary artifacts | `Bun.write` + `Bun.hash` under `research/cache/tennis-canary/` | [`live-canary-store.ts`](../src/institutions/event-store/live-canary-store.ts) | -| Tennis WS dashboard ground | `Bun.WebView` (`backend`: webkit on macOS, chrome elsewhere; `url` + `data:text/html`) + `Bun.file().image()` chain | [`tennis-ws-ground.ts`](../src/institutions/event-store/tennis-ws-ground.ts), [`tennis-ws-dashboard.ts`](../src/institutions/event-store/tennis-ws-dashboard.ts), [`tennis-book-coverage.ts`](../src/institutions/event-store/tennis-book-coverage.ts) | -| Match liquidity ground | Same WebView + Image pipeline over `match_liquidity` KPIs | [`match-liquidity-ground.ts`](../src/institutions/event-store/match-liquidity-ground.ts), [`match-liquidity-dashboard.ts`](../src/institutions/event-store/match-liquidity-dashboard.ts), `bun run liquidity:ground` · snapshot: `tools/snapshot-data-plane.ts` `liquidity` block | +| Tennis WS dashboard ground | `Bun.WebView` (`backend`: webkit on macOS, chrome elsewhere; `url` + `data:text/html`) + `Bun.file().image()` chain; persisted `snapshotMeta` uses native WebView constructor and `Bun.Image.metadata()` return types | [`tennis-ws-ground.ts`](../src/institutions/event-store/tennis-ws-ground.ts), [`visual-snapshot-meta.ts`](../src/institutions/event-store/visual-snapshot-meta.ts), [`tennis-ws-dashboard.ts`](../src/institutions/event-store/tennis-ws-dashboard.ts), [`tennis-book-coverage.ts`](../src/institutions/event-store/tennis-book-coverage.ts) | +| Match liquidity ground | Same typed WebView + Image metadata pipeline over `match_liquidity` KPIs | [`match-liquidity-ground.ts`](../src/institutions/event-store/match-liquidity-ground.ts), [`visual-snapshot-meta.ts`](../src/institutions/event-store/visual-snapshot-meta.ts), [`match-liquidity-dashboard.ts`](../src/institutions/event-store/match-liquidity-dashboard.ts), `bun run liquidity:ground` · snapshot: `tools/snapshot-data-plane.ts` `liquidity` block | +| Partner WebView CDP capture | Native `Bun.WebView.addEventListener()`; a local structural adapter validates `data`/`detail` because Bun 1.3.14 exposes the documented callback event as `typeof MessageEvent` during TS resolution. No ambient `Bun` augmentation. | [`webview-ws-capture.ts`](../src/partner/webview-ws-capture.ts), [`webview-cdp-events.ts`](../src/partner/webview-cdp-events.ts), `bun run partner:webview-ws-capture` | | Match liquidity pipeline cron | In-process `Bun.cron` + OS `export default { scheduled }` | [`match-liquidity-pipeline.ts`](../src/institutions/event-store/match-liquidity-pipeline.ts), [`match-liquidity-scheduled.ts`](../tools/match-liquidity-scheduled.ts), `cron:start` · `liquidity:pipeline:register` | | Match liquidity db watch | `fs.watch` on **cache dir** for `event-store.db` (+ `-wal`/`-shm`) → debounced recompute + HTML ground | pure: [`match-liquidity-db-watch.ts`](../src/institutions/event-store/match-liquidity-db-watch.ts) · CLI: [`tools/match-liquidity-db-watch.ts`](../tools/match-liquidity-db-watch.ts) · `bun run liquidity:ground:watch-db` · `--once` · `--fetch-volume` | -| Kalshi WS orderbook | Bun client `WebSocket` + RSA handshake headers; JSON-only wire; official error codes 1–25; ping keepalive; reconnect jitter; `NO_PROXY` + proxy/TLS env | [`kalshi-ws.ts`](../src/bot/kalshi-ws.ts), [`kalshi-ws-errors.ts`](../src/bot/kalshi-ws-errors.ts), [`kalshi-ws-recorder.ts`](../src/institutions/event-store/kalshi-ws-recorder.ts), [`tennis-ws-recorder-store.ts`](../src/institutions/event-store/tennis-ws-recorder-store.ts) | +| Kalshi WS orderbook | Bun client `WebSocket` + RSA handshake headers; proxy/TLS types derive from `Bun.WebSocketOptions`. A local constructor cast is isolated at creation because `lib.dom` hides Bun 1.3.14's options overload during TypeScript resolution; JSON-only wire; official error codes 1–25; ping keepalive; reconnect jitter; `NO_PROXY` + proxy/TLS env | [`kalshi-ws.ts`](../src/bot/kalshi-ws.ts), [`kalshi-ws-errors.ts`](../src/bot/kalshi-ws-errors.ts), [`kalshi-ws-recorder.ts`](../src/institutions/event-store/kalshi-ws-recorder.ts), [`tennis-ws-recorder-store.ts`](../src/institutions/event-store/tennis-ws-recorder-store.ts) | | Cadence / scoreboard tables | `Bun.inspect.table` | [`terminal-out.ts`](../src/research/terminal-out.ts), tennis CLI + `agent tennis` | | Terminal reports | `Bun.markdown.ansi` + `Bun.wrapAnsi` | [`report-term.ts`](../src/agent/report-term.ts) | | TTY tables + OSC 8 links | `Bun.inspect.table` + `Bun.stringWidth` / `wrapAnsi` / `stripANSI` | [`terminal-out.ts`](../src/research/terminal-out.ts) | diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 78570c2..675290f 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -25,6 +25,26 @@ Categories: `market` · `model` · `tournament` · `warehouse` · `trading` · ` Add new terms only in `GLOSSARY_ENTRIES` (ids are stable tip keys). +The partner execution flow is represented by connected canonical concepts, not +by a separate documentation-only vocabulary: + +`partner.authorization.request` → `partner.authorization.grant` → +`partner.execution.gate` → `partner.execution.reservation` → +`partner.execution.provider_lifecycle` → `partner.execution.journal` → +`partner.execution.receipt`. + +`provider.polymarket.intelligence_only` records the current read-only boundary; +it is deliberately linked to market-data concepts and the execution gate, but +does not represent a Polymarket order adapter. See the +[partner execution expansion map](PARTNER-EXECUTION-EXPANSION.md). + +`snapshot.visual.provenance` describes the audit-only metadata emitted by the +native WebView/Image visual-ground pipeline. It records Bun version/revision, +browser backend and viewport, capture outcome, and decoded image dimensions and +format, plus artifact byte length and SHA-256 integrity. Persisted WebView/CDP +summary URLs omit query/hash credentials. It never substitutes for an executable quote, authorization, or +provider lifecycle evidence. + HQ fragment routing uses `URLPattern.hash`; patterns match the fragment without the leading `#`. A literal colon before a named group must be escaped: `new URLPattern({ hash: "glossary\\::concept" })`. diff --git a/docs/OFFICIAL_URLS.md b/docs/OFFICIAL_URLS.md index fe11aae..3d68211 100644 --- a/docs/OFFICIAL_URLS.md +++ b/docs/OFFICIAL_URLS.md @@ -2,7 +2,7 @@ Canonical external links for this repo. **Code SSOT:** [`src/institutions/official-urls.ts`](../src/institutions/official-urls.ts). -Last verified: **2026-07-31** +Last verified: **2026-08-06** **Liveness:** @@ -65,6 +65,27 @@ feeCents = ceil(rate × contracts × P × (1 − P) × 100) Set `ODDS_API_KEY` in env for live fetches. +## Telegram + +| Resource | URL | Notes | +| --- | --- | --- | +| Bot API | [core.telegram.org/bots/api](https://core.telegram.org/bots/api) | HTTP bot methods, message/topic identifiers, and request shapes | +| Bot platform introduction | [core.telegram.org/bots](https://core.telegram.org/bots) | BotFather setup and token-safety guidance | + +The partner authorization boundary stores numeric chat, topic, message, and +approver IDs as validated strings and never stores the bot token in SQLite. + +## Polymarket + +| Resource | URL | Notes | +| --- | --- | --- | +| API introduction | [docs.polymarket.com/api-reference/introduction](https://docs.polymarket.com/api-reference/introduction) | Gamma public market/event metadata surface | +| Quickstart | [docs.polymarket.com/quickstart](https://docs.polymarket.com/quickstart) | Current public API examples | + +Polymarket is currently a read-only regulatory market-intelligence source in +this repository. It is not an authorized partner execution provider; see +[`PARTNER-EXECUTION-EXPANSION.md`](PARTNER-EXECUTION-EXPANSION.md). + **Tennis coverage:** named ATP/WTA tournaments only — no Challenger/ITF sport keys. That boundary defines the two tennis program archetypes — see [`TENNIS_PROGRAM_ARCHETYPES.md`](TENNIS_PROGRAM_ARCHETYPES.md). ## Bun (runtime) diff --git a/docs/PARTNER-DOMAIN.md b/docs/PARTNER-DOMAIN.md index a09ea91..3a006d5 100644 --- a/docs/PARTNER-DOMAIN.md +++ b/docs/PARTNER-DOMAIN.md @@ -5,6 +5,7 @@ the local SSOT for registry + Fantasy Ultra until a separate seat-capital service is the only writer. Machine status: `bun run partner:domain` · `bun run partner:domain -- --json` +Expansion map: `bun run partner:map` · [`PARTNER-EXECUTION-EXPANSION.md`](PARTNER-EXECUTION-EXPANSION.md) Code map: [`src/partner/domain.ts`](../src/partner/domain.ts) --- @@ -189,6 +190,7 @@ bun run serve | Command | Layer | | ----------------------------------------------------- | --------------------------------------------------------------------- | | `partner:domain` | All (status) | +| `partner:map` | Telegram → authorization → provider expansion map (Mermaid) | | `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()` | @@ -224,6 +226,13 @@ bun run serve | devDependencies | Reproducible CI (`typescript`, types) | | `bunx` | One-off CLIs (`drizzle-kit`, generators) | +## See also + +- [Partner execution expansion map](PARTNER-EXECUTION-EXPANSION.md) +- [Authorized partner execution](AUTHORIZED_EXECUTION.md) +- [Regulatory agents and Polymarket intelligence](regulatory-agents.md) +- [Partner/Fantasy Ultra provider boundary](PARTNER-FANTASY-ULTRA.md) + ### Partners TOML (`Bun.TOML`) Non-secret registry on disk (v1.1 TOML via Bun native parse/stringify + **Zod** diff --git a/docs/PARTNER-EXECUTION-EXPANSION.md b/docs/PARTNER-EXECUTION-EXPANSION.md new file mode 100644 index 0000000..d3b2e31 --- /dev/null +++ b/docs/PARTNER-EXECUTION-EXPANSION.md @@ -0,0 +1,139 @@ +# Partner Execution Expansion Map + +This is the single orientation map for partner communication, permissioned +execution, provider lifecycle, and cross-venue expansion. It describes current +runtime authority, not a promise that every pictured provider can place orders. + +## Generate the map + +```bash +bun run partner:map +bun run partner:map -- --output=artifacts/partner-expansion.mmd +bun run partner:domain -- --json +``` + +`partner:map` emits Mermaid from the code-owned map in +[`src/partner/domain.ts`](../src/partner/domain.ts). Dashed edges are future or +intelligence-only contracts. They must not be interpreted as live execution. + +```mermaid +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 +``` + +## Current capability boundary + +| Surface | Current role | Execution state | +| --- | --- | --- | +| Telegram | Hash-bound approval/revocation provenance and durable receipts | Cannot place or arm a bet by itself | +| Partner registry | Resolves partner, out, skin, provider, currency, and limits | Required identity/configuration boundary | +| Kalshi | Authorized order/cancel, deterministic reconciliation, lifecycle, journal | Built, default off; production graduation still gated | +| Polymarket | Public Gamma market ingestion, ticks, line moves, compliance intelligence | Read-only; no partner authorization or order adapter | +| Fantasy402 | Inventory, odds/HAR mapping, and legacy ticket ingest | Not connected to authorized execution | + +## Identifier and mapping contract + +Do not collapse identifiers from different authorities: + +| Authority | Stable identifiers | Binding rule | +| --- | --- | --- | +| Partner | `partnerCode`, `outId`, `skin`, `provider`, `currency` | Exact tuple is bound by the authorization policy hash | +| Telegram | numeric chat, topic, message, and approving-user IDs stored as strings | Approval must match the allowlisted channel/topic/user and request hash | +| Local execution | reservation ID, execution idempotency key, authorization ID | One immutable request and exposure lane | +| Kalshi | ticker, order ID, deterministic `client_order_id`, fill/trade ID, subaccount | Primary-account evidence and exact order terms only | +| Polymarket | event ID, market ID, `condition_id`, slug | Intelligence identity only; never substitute for a Kalshi ticker | + +A future cross-venue adapter needs an explicit versioned mapping such as +`canonical_event_id ↔ Kalshi ticker ↔ Polymarket condition_id/market_id`, plus +provider-side idempotency, account authorization, balance/liquidity snapshots, +cancel semantics, partial-fill lifecycle, settlement evidence, and journal +mapping. None of those requirements may be inferred from a shared event name. + +## Expansion acceptance gates + +Polymarket execution remains unwired until it has parity with the Kalshi safety +boundary: + +1. provider-owned credentials and account identity; +2. deterministic provider-side idempotency and exact lookup reconciliation; +3. executable price/liquidity and signed balance snapshots; +4. order, partial-fill, cancellation, position, and settlement normalization; +5. authorization policy binding and transactional exposure reservation; +6. immutable integer-minor-unit journal entries and drift checks; +7. durable partner receipts, demo evidence, and a separately reviewed arm gate. + +Public Polymarket data may inform regulatory monitoring today, but it cannot +grant permission, select an account, or prove a provider execution outcome. + +## Canonical concept graph + +The glossary models this flow with stable IDs and reciprocal `seeAlso` edges: + +`partner.authorization.request` → `partner.authorization.grant` → +`partner.execution.gate` → `partner.execution.reservation` → +`partner.execution.provider_lifecycle` → `partner.execution.journal` → +`partner.execution.receipt`. + +The separate `provider.polymarket.intelligence_only` concept makes the +read-only boundary machine-discoverable. Inspect these entries through the HQ +glossary, `GET /api/glossary`, or `bun run glossary:dump`; validate the graph +with `bun run glossary:check` and `bun run partners:validate`. + +Visual evidence produced from partner/liquidity dashboards carries +`snapshot.visual.provenance`: native `Bun.WebView` backend/viewport and runtime +identity plus `Bun.Image.metadata()` for the screenshot and thumbnail. This is +review evidence only; execution freshness continues to come from the typed +market/account `ExecutionSnapshot` immediately before reservation. +The metadata also binds SHA-256 and byte length for generated artifacts; +partner WebView/CDP summaries use the same runtime/integrity vocabulary and +strip query/hash credentials from persisted URLs. + +## External references + +- [Telegram Bot API](https://core.telegram.org/bots/api) +- [Telegram bots introduction and token safety](https://core.telegram.org/bots) +- [Kalshi API documentation](https://docs.kalshi.com/) +- [Kalshi OpenAPI contract](https://docs.kalshi.com/openapi.yaml) +- [Polymarket API introduction](https://docs.polymarket.com/api-reference/introduction) +- [Polymarket quickstart](https://docs.polymarket.com/quickstart) + +## See also + +- [GitHub Wiki documentation hub](https://github.com/brendadeeznuts1111/Kalshi-bot/wiki) +- [Partner domain architecture](PARTNER-DOMAIN.md) +- [Canonical glossary and partner concept graph](GLOSSARY.md) +- [Authorized partner execution](AUTHORIZED_EXECUTION.md) +- [Authorized execution delivery record](AUTHORIZED_EXECUTION_REMAINING_WORK.md) +- [Demo proof schema](EXECUTION_DEMO_PROOF_SCHEMA.md) +- [Regulatory agents and Polymarket intelligence](regulatory-agents.md) +- [Partner/Fantasy Ultra boundary](PARTNER-FANTASY-ULTRA.md) +- [Official URL catalog](OFFICIAL_URLS.md) +- [Environment naming and arm flags](ENV_NAMING.md) diff --git a/package.json b/package.json index a1a58c6..70a8704 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "partner:capacity": "bun tools/partner-registry.ts", "partner:sports": "bun tools/partner-sports.ts", "partner:domain": "bun tools/partner-domain.ts", + "partner:map": "bun tools/partner-domain.ts --map", "partner:profile": "bun tools/partner-profile.ts", "partner:avatars": "bun tools/partner-profile.ts --png", "partner:toml": "bun tools/partner-toml.ts", @@ -178,6 +179,7 @@ }, "devDependencies": { "@types/bun": "1.3.14", + "bun-types": "1.3.14", "typescript": "6.0.3" } } diff --git a/scripts/audit-bun-native.ts b/scripts/audit-bun-native.ts index e8c6fe7..081d165 100644 --- a/scripts/audit-bun-native.ts +++ b/scripts/audit-bun-native.ts @@ -12,6 +12,7 @@ * @see https://bun.com/docs/runtime/utils */ import { join } from "node:path"; +import ts from "typescript"; export const BANNED_PACKAGES = new Map([ ["wrap-ansi", "Bun.wrapAnsi()"], @@ -21,9 +22,9 @@ export const BANNED_PACKAGES = new Map([ ["cli-table", "Bun.inspect.table()"], ["cli-table3", "Bun.inspect.table()"], ["cli-table2", "Bun.inspect.table()"], - ["toml", "Bun.TOML.parse() / Bun.TOML.stringify()"], - ["@iarna/toml", "Bun.TOML.parse() / Bun.TOML.stringify()"], - ["@ltd/j-toml", "Bun.TOML.parse() / Bun.TOML.stringify()"], + ["toml", "Bun.TOML.parse() / governed tomlStringify()"], + ["@iarna/toml", "Bun.TOML.parse() / governed tomlStringify()"], + ["@ltd/j-toml", "Bun.TOML.parse() / governed tomlStringify()"], ]); const DEPENDENCY_SECTIONS = [ @@ -164,6 +165,32 @@ export function findSourceViolations( : source; const imports = transpiler(sourceLoader(file)).scanImports(scannable); + const sourceFile = ts.createSourceFile( + file, + scannable, + ts.ScriptTarget.Latest, + false, + file.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); + const inspectDeclaration = (node: ts.Node): void => { + if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === "Bun") { + violations.push({ + file, + message: "ambient/local namespace Bun shadows canonical bun-types declarations", + }); + } else if ( + (ts.isInterfaceDeclaration(node) || ts.isClassDeclaration(node) || ts.isEnumDeclaration(node)) + && node.name?.text === "Bun" + ) { + violations.push({ + file, + message: "local Bun declaration shadows the canonical runtime/type namespace", + }); + } + ts.forEachChild(node, inspectDeclaration); + }; + inspectDeclaration(sourceFile); + for (const item of imports) { const dependency = packageName(item.path); const replacement = BANNED_PACKAGES.get(dependency); diff --git a/scripts/export-toml.ts b/scripts/export-toml.ts index 6d02877..50f362c 100644 --- a/scripts/export-toml.ts +++ b/scripts/export-toml.ts @@ -10,18 +10,19 @@ * Output: writes file.toml alongside the input JSON. * Validates against TOML restrictions (no null/undefined in arrays, no BigInt). */ -import { TOML } from "bun"; +import { tomlStringify } from "../src/partner/toml-stringify.ts"; function findUndefinedInArray(value: unknown[], path: string): string | null { for (let i = 0; i < value.length; i++) { - if (value[i] === undefined) { + const item = value[i]; + if (item === undefined) { return `TOML arrays cannot contain undefined (hole). Found at ${path}[${i}]`; } - if (Array.isArray(value[i])) { - const inner = findUndefinedInArray(value[i], `${path}[${i}]`); + if (Array.isArray(item)) { + const inner = findUndefinedInArray(item, `${path}[${i}]`); if (inner) return inner; - } else if (typeof value[i] === "object" && value[i] !== null) { - for (const [k, v] of Object.entries(value[i] as Record)) { + } else if (typeof item === "object" && item !== null) { + for (const [k, v] of Object.entries(item as Record)) { if (v === undefined) { return `TOML tables cannot contain undefined. Found at ${path}[${i}].${k}`; } @@ -38,7 +39,7 @@ function validateForToml(data: unknown, path = "$"): string | null { } if (typeof data === "object") { for (const [k, v] of Object.entries(data as Record)) { - if (v === undefined) continue; // skipped by TOML.stringify + if (v === undefined) continue; // skipped by the governed serializer if (Array.isArray(v)) { const err = findUndefinedInArray(v, `${path}.${k}`); if (err) return err; @@ -80,12 +81,12 @@ async function main() { } try { - const toml = TOML.stringify(data); + const toml = tomlStringify(data).trimEnd(); const tomlPath = jsonPath.replace(/\.json$/i, ".toml"); await Bun.write(tomlPath, toml + "\n"); console.log(`✅ ${jsonPath} → ${tomlPath}`); } catch (err) { - console.error(`TOML.stringify failed: ${err}`); + console.error(`TOML serialization failed: ${err}`); process.exit(1); } } diff --git a/scripts/partners-validate.ts b/scripts/partners-validate.ts index c6116b8..5a07fd0 100644 --- a/scripts/partners-validate.ts +++ b/scripts/partners-validate.ts @@ -47,6 +47,15 @@ export const KALSHI_PARTNER_OPS_CONCEPT_IDS = [ 'event.settlement.processed', 'event.telegram.invite_sent', 'event.telegram.message_pinned', + 'partner.authorization.request', + 'partner.authorization.grant', + 'partner.execution.gate', + 'partner.execution.reservation', + 'partner.execution.provider_lifecycle', + 'partner.execution.journal', + 'partner.execution.receipt', + 'provider.polymarket.intelligence_only', + 'snapshot.visual.provenance', ] as const; const EXPECTED_KIND: Record<(typeof KALSHI_PARTNER_OPS_CONCEPT_IDS)[number], 'ui' | 'composite'> = { @@ -81,6 +90,15 @@ const EXPECTED_KIND: Record<(typeof KALSHI_PARTNER_OPS_CONCEPT_IDS)[number], 'ui 'event.settlement.processed': 'composite', 'event.telegram.invite_sent': 'composite', 'event.telegram.message_pinned': 'composite', + 'partner.authorization.request': 'composite', + 'partner.authorization.grant': 'composite', + 'partner.execution.gate': 'composite', + 'partner.execution.reservation': 'composite', + 'partner.execution.provider_lifecycle': 'composite', + 'partner.execution.journal': 'composite', + 'partner.execution.receipt': 'composite', + 'provider.polymarket.intelligence_only': 'composite', + 'snapshot.visual.provenance': 'composite', }; const errs: string[] = []; diff --git a/scripts/surface-audit.ts b/scripts/surface-audit.ts index d747975..9d3c46f 100644 --- a/scripts/surface-audit.ts +++ b/scripts/surface-audit.ts @@ -32,7 +32,7 @@ const GLOSSARY_ID_RE = /"(kpi\.\w+|alert\.\w+|ops\.\w+|kalshi_\w+|poly_\w+|elo_\ let violations = 0; for (const [filePrefix, expectedSurface] of Object.entries(FILE_SURFACE_MAP)) { - const allowed = new Set(PAGE_SURFACES[expectedSurface]); + const allowed = new Set(PAGE_SURFACES[expectedSurface]); const files = await Array.fromAsync(new Bun.Glob(`${filePrefix}*`).scan()); for (const file of files) { diff --git a/src/bot/kalshi-ws.ts b/src/bot/kalshi-ws.ts index f5cc7ba..7bb6041 100644 --- a/src/bot/kalshi-ws.ts +++ b/src/bot/kalshi-ws.ts @@ -56,44 +56,26 @@ export type KalshiWsFactory = (url: string, headers: Record) => * All ws:// and wss:// combinations work through HTTP and HTTPS proxies. * @see https://bun.com/blog/bun-v1.3.6#http-https-proxy-support-for-websocket */ -export type KalshiWsProxyOptions = string | { url: string; headers?: Record }; +export type KalshiWsProxyOptions = NonNullable; /** * Granular TLS control values for the client WebSocket `tls` option. * Subset of Bun's TLSOptions relevant to a wss:// client; matches `fetch` TLS. * @see https://bun.com/docs/runtime/networking/fetch */ -export type KalshiWsTlsOptions = { - /** Override trusted CA certs (replaces Mozilla bundle). File path or PEM. */ - ca?: string | Bun.BunFile | Array; - /** Client certificate chain (mTLS). File path or PEM. */ - cert?: string | Bun.BunFile; - /** Client private key (mTLS). File path or PEM. */ - key?: string | Bun.BunFile; - /** Passphrase for an encrypted `key`. */ - passphrase?: string; - /** false accepts any certificate — corp-proxy/dev debugging only. */ - rejectUnauthorized?: boolean; - /** Explicit SNI server name (defaults to URL host). */ - serverName?: string; - /** OpenSSL cipher suite list. */ - ciphers?: string; - /** Custom certificate validation; return an Error to reject. */ - checkServerIdentity?: (hostname: string, peerCertificate: unknown) => Error | undefined; - /** Sets OPENSSL_RELEASE_BUFFERS=1: saves memory, hurts performance. */ - lowMemoryMode?: boolean; -}; +export type KalshiWsTlsOptions = NonNullable; -export type KalshiWsNetOptions = { - proxy?: KalshiWsProxyOptions; - tls?: KalshiWsTlsOptions; -}; +export type KalshiWsNetOptions = Pick; -type BunClientWebSocketOptions = { - headers: Record; - proxy?: KalshiWsProxyOptions; - tls?: KalshiWsTlsOptions; -}; +type BunClientWebSocketOptions = Pick< + Bun.WebSocketOptions, + "headers" | "proxy" | "tls" +>; + +type BunClientWebSocketConstructor = new ( + url: string | URL, + options?: Bun.WebSocketOptions, +) => WebSocket; function kalshiWsTargetHost( env: Record, @@ -217,12 +199,11 @@ function defaultWsFactory( net: KalshiWsNetOptions = resolveKalshiWsNetOptions(), ): KalshiWsSocket { const opts: BunClientWebSocketOptions = { headers, ...net }; - // Bun extension: headers + optional proxy/tls on client WebSocket (not in DOM lib typings). - const BunWebSocket = WebSocket as unknown as new ( - url: string, - opts: BunClientWebSocketOptions, - ) => KalshiWsSocket; - return new BunWebSocket(url, opts); + // lib.dom wins TypeScript's global constructor selection and hides Bun's + // canonical options overload. Keep the compatibility cast at this boundary; + // the constructor options themselves remain derived from Bun.WebSocketOptions. + const BunClientWebSocket = WebSocket as unknown as BunClientWebSocketConstructor; + return new BunClientWebSocket(url, opts); } export function resolveKalshiWsUrl( diff --git a/src/institutions/event-store/match-liquidity-ground.ts b/src/institutions/event-store/match-liquidity-ground.ts index ade63b5..b0e8099 100644 --- a/src/institutions/event-store/match-liquidity-ground.ts +++ b/src/institutions/event-store/match-liquidity-ground.ts @@ -23,6 +23,11 @@ import { TENNIS_WS_GROUND_WEBVIEW_HEIGHT, TENNIS_WS_GROUND_WEBVIEW_WIDTH, } from "./tennis-lane-constants.ts"; +import { + buildVisualSnapshotMeta, + type BunWebViewOptions, + type VisualSnapshotMeta, +} from "./visual-snapshot-meta.ts"; export const MATCH_LIQUIDITY_GROUND_DIR = joinPath("research/cache/match-liquidity-ground"); export const MATCH_LIQUIDITY_GROUND_LATEST = join(MATCH_LIQUIDITY_GROUND_DIR, "latest.json"); @@ -34,6 +39,8 @@ export type MatchLiquidityGroundArtifact = { thumbWebp: string; webview: boolean; image: boolean; + /** Bun-native capture provenance; absent only on legacy/injected artifacts. */ + snapshotMeta?: VisualSnapshotMeta; model: MatchLiquidityDashboardModel; }; @@ -44,6 +51,7 @@ export type MatchLiquidityGroundLatest = { thumbWebp: string; webview: boolean; image: boolean; + snapshotMeta: VisualSnapshotMeta; total: number; quoted: number; liquidityOk: number; @@ -51,9 +59,7 @@ export type MatchLiquidityGroundLatest = { rows: number; }; -type WebViewOptions = NonNullable[0]>; - -function resolveWebViewBackend(): WebViewOptions["backend"] { +function resolveWebViewBackend(): BunWebViewOptions["backend"] { return process.platform === "darwin" ? "webkit" : "chrome"; } @@ -62,8 +68,7 @@ function hasWebView(): boolean { } function hasImagePipeline(): boolean { - // Prefer file().image() chain (tennis-ws-ground); Bun.Image class may also exist. - return typeof Bun.Image === "function" || typeof (Bun.file as unknown) === "function"; + return typeof Bun.Image === "function"; } /** Write dashboard PNG + WebP thumb via Bun.WebView + Bun.Image. */ @@ -88,11 +93,13 @@ export async function captureMatchLiquidityGround( let webviewCaptured = false; let imageCaptured = false; + let webviewError: string | null = null; + let imageError: string | null = null; if (!options.htmlOnly && hasWebView()) { // @see https://bun.com/docs/runtime/webview const dataUrl = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; - const viewOptions: WebViewOptions = { + const viewOptions: BunWebViewOptions = { width: TENNIS_WS_GROUND_WEBVIEW_WIDTH, height: TENNIS_WS_GROUND_WEBVIEW_HEIGHT, backend: resolveWebViewBackend(), @@ -109,18 +116,38 @@ export async function captureMatchLiquidityGround( if (hasImagePipeline()) { // @see https://bun.com/docs/runtime/image - await Bun.file(dashboardPng) - .image() - .resize(TENNIS_WS_GROUND_THUMB_WIDTH, TENNIS_WS_GROUND_THUMB_HEIGHT, { fit: "inside" }) - .webp({ quality: TENNIS_WS_GROUND_WEBP_QUALITY }) - .write(thumbWebp); - imageCaptured = true; + try { + await Bun.file(dashboardPng) + .image() + .resize(TENNIS_WS_GROUND_THUMB_WIDTH, TENNIS_WS_GROUND_THUMB_HEIGHT, { fit: "inside" }) + .webp({ quality: TENNIS_WS_GROUND_WEBP_QUALITY }) + .write(thumbWebp); + imageCaptured = true; + } catch (error) { + imageError = errorMessage(error); + } } - } catch { + } catch (error) { // HTML still written + webviewError = errorMessage(error); } } + const snapshotMeta = await buildVisualSnapshotMeta({ + capturedAt: model.at, + backend: resolveWebViewBackend(), + width: TENNIS_WS_GROUND_WEBVIEW_WIDTH, + height: TENNIS_WS_GROUND_WEBVIEW_HEIGHT, + webviewCaptured, + webviewAttempted: !options.htmlOnly, + webviewError, + imageGenerated: imageCaptured, + imageAttempted: webviewCaptured && hasImagePipeline(), + imageError, + sourcePath: dashboardPng, + thumbnailPath: thumbWebp, + }); + return { at: model.at, dashboardHtml, @@ -128,6 +155,7 @@ export async function captureMatchLiquidityGround( thumbWebp, webview: webviewCaptured, image: imageCaptured, + snapshotMeta, model, }; } @@ -136,6 +164,20 @@ export async function persistMatchLiquidityGroundArtifact( artifact: MatchLiquidityGroundArtifact, latestPath: string = MATCH_LIQUIDITY_GROUND_LATEST, ): Promise { + const snapshotMeta = + artifact.snapshotMeta ?? + (await buildVisualSnapshotMeta({ + capturedAt: artifact.at, + backend: resolveWebViewBackend(), + width: TENNIS_WS_GROUND_WEBVIEW_WIDTH, + height: TENNIS_WS_GROUND_WEBVIEW_HEIGHT, + webviewCaptured: artifact.webview, + webviewAttempted: artifact.webview, + imageGenerated: artifact.image, + imageAttempted: artifact.image, + sourcePath: artifact.dashboardPng, + thumbnailPath: artifact.thumbWebp, + })); const latest: MatchLiquidityGroundLatest = { at: artifact.at, dashboardHtml: artifact.dashboardHtml, @@ -143,6 +185,7 @@ export async function persistMatchLiquidityGroundArtifact( thumbWebp: artifact.thumbWebp, webview: artifact.webview, image: artifact.image, + snapshotMeta, total: artifact.model.summary.total, quoted: artifact.model.summary.quoted, liquidityOk: artifact.model.summary.liquidityOk, @@ -153,6 +196,10 @@ export async function persistMatchLiquidityGroundArtifact( return latest; } +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + export async function loadLatestMatchLiquidityGround( latestPath: string = MATCH_LIQUIDITY_GROUND_LATEST, ): Promise { diff --git a/src/institutions/event-store/tennis-ws-ground.ts b/src/institutions/event-store/tennis-ws-ground.ts index 521b6c7..6e1f4b9 100644 --- a/src/institutions/event-store/tennis-ws-ground.ts +++ b/src/institutions/event-store/tennis-ws-ground.ts @@ -24,6 +24,11 @@ import { TENNIS_WS_GROUND_WEBVIEW_HEIGHT, TENNIS_WS_GROUND_WEBVIEW_WIDTH, } from "./tennis-lane-constants.ts"; +import { + buildVisualSnapshotMeta, + type BunWebViewOptions, + type VisualSnapshotMeta, +} from "./visual-snapshot-meta.ts"; export const TENNIS_WS_GROUND_DIR = joinPath("research/cache/tennis-ws-ground"); export const TENNIS_WS_GROUND_LATEST = join(TENNIS_WS_GROUND_DIR, "latest.json"); @@ -35,6 +40,8 @@ export type TennisWsGroundArtifact = { thumbWebp: string; webview: boolean; image: boolean; + /** Bun-native capture provenance; absent only on legacy/injected artifacts. */ + snapshotMeta?: VisualSnapshotMeta; model: TennisWsDashboardModel; }; @@ -46,6 +53,7 @@ export type TennisWsGroundLatest = { thumbWebp: string; webview: boolean; image: boolean; + snapshotMeta: VisualSnapshotMeta; watchEvents: number; watchTickers: number; wsTicks: number; @@ -56,9 +64,7 @@ export type TennisWsGroundLatest = { linkedEventsWithWs: number; }; -type WebViewOptions = NonNullable[0]>; - -function resolveWebViewBackend(): WebViewOptions["backend"] { +function resolveWebViewBackend(): BunWebViewOptions["backend"] { return process.platform === "darwin" ? "webkit" : "chrome"; } @@ -97,11 +103,13 @@ export async function captureTennisWsGround( let webviewCaptured = false; let imageCaptured = false; + let webviewError: string | null = null; + let imageError: string | null = null; if (!options.htmlOnly && hasWebView()) { // @see https://bun.com/docs/runtime/webview — data: URL navigation + screenshot const dataUrl = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; - const viewOptions: WebViewOptions = { + const viewOptions: BunWebViewOptions = { width: TENNIS_WS_GROUND_WEBVIEW_WIDTH, height: TENNIS_WS_GROUND_WEBVIEW_HEIGHT, backend: resolveWebViewBackend(), @@ -118,18 +126,38 @@ export async function captureTennisWsGround( if (hasImagePipeline()) { // @see https://bun.com/docs/runtime/image — chain resize + webp encode - await Bun.file(dashboardPng) - .image() - .resize(TENNIS_WS_GROUND_THUMB_WIDTH, TENNIS_WS_GROUND_THUMB_HEIGHT, { fit: "inside" }) - .webp({ quality: TENNIS_WS_GROUND_WEBP_QUALITY }) - .write(thumbWebp); - imageCaptured = true; + try { + await Bun.file(dashboardPng) + .image() + .resize(TENNIS_WS_GROUND_THUMB_WIDTH, TENNIS_WS_GROUND_THUMB_HEIGHT, { fit: "inside" }) + .webp({ quality: TENNIS_WS_GROUND_WEBP_QUALITY }) + .write(thumbWebp); + imageCaptured = true; + } catch (error) { + imageError = errorMessage(error); + } } - } catch { + } catch (error) { // WebView or Image unavailable at runtime — HTML artifact still written + webviewError = errorMessage(error); } } + const snapshotMeta = await buildVisualSnapshotMeta({ + capturedAt: model.at, + backend: resolveWebViewBackend(), + width: TENNIS_WS_GROUND_WEBVIEW_WIDTH, + height: TENNIS_WS_GROUND_WEBVIEW_HEIGHT, + webviewCaptured, + webviewAttempted: !options.htmlOnly, + webviewError, + imageGenerated: imageCaptured, + imageAttempted: webviewCaptured && hasImagePipeline(), + imageError, + sourcePath: dashboardPng, + thumbnailPath: thumbWebp, + }); + return { at: model.at, dashboardHtml, @@ -137,6 +165,7 @@ export async function captureTennisWsGround( thumbWebp, webview: webviewCaptured, image: imageCaptured, + snapshotMeta, model, }; } @@ -145,6 +174,20 @@ export async function persistTennisWsGroundArtifact( artifact: TennisWsGroundArtifact, latestPath: string = TENNIS_WS_GROUND_LATEST, ): Promise { + const snapshotMeta = + artifact.snapshotMeta ?? + (await buildVisualSnapshotMeta({ + capturedAt: artifact.at, + backend: resolveWebViewBackend(), + width: TENNIS_WS_GROUND_WEBVIEW_WIDTH, + height: TENNIS_WS_GROUND_WEBVIEW_HEIGHT, + webviewCaptured: artifact.webview, + webviewAttempted: artifact.webview, + imageGenerated: artifact.image, + imageAttempted: artifact.image, + sourcePath: artifact.dashboardPng, + thumbnailPath: artifact.thumbWebp, + })); const latest: TennisWsGroundLatest = { at: artifact.at, dashboardHtml: artifact.dashboardHtml, @@ -152,6 +195,7 @@ export async function persistTennisWsGroundArtifact( thumbWebp: artifact.thumbWebp, webview: artifact.webview, image: artifact.image, + snapshotMeta, watchEvents: artifact.model.watchEvents, watchTickers: artifact.model.watchTickers, wsTicks: artifact.model.wsTicks, @@ -165,6 +209,10 @@ export async function persistTennisWsGroundArtifact( return latest; } +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + /** Read latest visual ground artifact (cache-only; no WebView invoke). */ export async function loadLatestWsGround( latestPath: string = TENNIS_WS_GROUND_LATEST, diff --git a/src/institutions/event-store/visual-snapshot-meta.ts b/src/institutions/event-store/visual-snapshot-meta.ts new file mode 100644 index 0000000..3f8b5a8 --- /dev/null +++ b/src/institutions/event-store/visual-snapshot-meta.ts @@ -0,0 +1,128 @@ +/** Bun-native provenance for WebView/Image visual-ground artifacts. */ + +export type BunWebViewOptions = NonNullable< + ConstructorParameters[0] +>; +export type BunWebViewBackend = BunWebViewOptions["backend"]; +export type BunImageMetadata = Awaited>; + +export type SnapshotArtifactIntegrity = { + path: string; + sizeBytes: number; + sha256: string; +}; + +export type VisualImageArtifactMeta = SnapshotArtifactIntegrity & { + metadata: BunImageMetadata; +}; + +export type VisualSnapshotMeta = { + schemaVersion: 1; + capturedAt: string; + runtime: { + bunVersion: string; + bunRevision: string; + }; + webview: { + available: boolean; + attempted: boolean; + captured: boolean; + error: string | null; + backend: "webkit" | "chrome"; + width: number; + height: number; + }; + image: { + available: boolean; + attempted: boolean; + generated: boolean; + error: string | null; + source: VisualImageArtifactMeta | null; + thumbnail: VisualImageArtifactMeta | null; + }; +}; + +export function normalizeWebViewBackend( + backend: BunWebViewBackend, +): "webkit" | "chrome" { + return typeof backend === "object" ? backend.type : (backend ?? "webkit"); +} + +export async function buildVisualSnapshotMeta(input: { + capturedAt: string; + backend: BunWebViewBackend; + width: number; + height: number; + webviewCaptured: boolean; + webviewAttempted?: boolean; + webviewError?: string | null; + imageGenerated: boolean; + imageAttempted?: boolean; + imageError?: string | null; + sourcePath: string; + thumbnailPath: string; +}): Promise { + const imageAvailable = typeof Bun.Image === "function"; + return { + schemaVersion: 1, + capturedAt: input.capturedAt, + runtime: { + bunVersion: Bun.version, + bunRevision: Bun.revision, + }, + webview: { + available: typeof Bun.WebView === "function", + attempted: input.webviewAttempted ?? input.webviewCaptured, + captured: input.webviewCaptured, + error: input.webviewError ?? null, + backend: normalizeWebViewBackend(input.backend), + width: input.width, + height: input.height, + }, + image: { + available: imageAvailable, + attempted: input.imageAttempted ?? input.imageGenerated, + generated: input.imageGenerated, + error: input.imageError ?? null, + source: + imageAvailable && input.webviewCaptured + ? await readImageArtifactMeta(input.sourcePath) + : null, + thumbnail: + imageAvailable && input.imageGenerated + ? await readImageArtifactMeta(input.thumbnailPath) + : null, + }, + }; +} + +export async function readArtifactIntegrity( + path: string, +): Promise { + try { + const file = Bun.file(path); + if (!(await file.exists())) return null; + const bytes = await file.bytes(); + return { + path, + sizeBytes: bytes.byteLength, + sha256: new Bun.CryptoHasher("sha256").update(bytes).digest("hex"), + }; + } catch { + return null; + } +} + +async function readImageArtifactMeta( + path: string, +): Promise { + try { + const [integrity, metadata] = await Promise.all([ + readArtifactIntegrity(path), + Bun.file(path).image().metadata(), + ]); + return integrity ? { ...integrity, metadata } : null; + } catch { + return null; + } +} diff --git a/src/institutions/glossary.ts b/src/institutions/glossary.ts index 7495361..d56ff88 100644 --- a/src/institutions/glossary.ts +++ b/src/institutions/glossary.ts @@ -1057,7 +1057,7 @@ export const GLOSSARY_ENTRIES: readonly GlossaryEntry[] = [ color: "tennis", status: "active", synonyms: ["ready", "live", "operator ready"], - seeAlso: ["partner.phase.onboarding", "partner.phase.incomplete", "partner.phase.paused"], + seeAlso: ["partner.phase.onboarding", "partner.phase.incomplete", "partner.phase.paused", "partner.authorization.grant"], tone: "concept", added: "2026-07-31", }, @@ -1213,7 +1213,7 @@ export const GLOSSARY_ENTRIES: readonly GlossaryEntry[] = [ color: "tennis", status: "active", synonyms: ["out ready", "ready"], - seeAlso: ["out.status.deferred", "out.status.paused", "partner.phase.operator_ready"], + seeAlso: ["out.status.deferred", "out.status.paused", "partner.phase.operator_ready", "partner.authorization.grant"], tone: "concept", added: "2026-07-31", }, @@ -1457,10 +1457,128 @@ export const GLOSSARY_ENTRIES: readonly GlossaryEntry[] = [ color: "tennis", status: "active", synonyms: ["TELEGRAM_MESSAGE_PINNED"], - seeAlso: ["event.telegram.invite_sent", "accounting.deposit"], + seeAlso: ["event.telegram.invite_sent", "accounting.deposit", "partner.authorization.request", "partner.execution.receipt"], tone: "alert", added: "2026-07-31", }, + { + id: "partner.authorization.request", + kind: "composite", + label: "Authorization request", + description: "Pending partner policy snapshot whose immutable terms and Telegram provenance are bound by a canonical SHA-256 hash.", + category: "pipeline", + color: "middleware", + status: "active", + synonyms: ["approval request", "policy hash", "account_authorization_requests"], + seeAlso: ["partner.authorization.grant", "event.telegram.message_pinned", "partner.phase.onboarding"], + tone: "concept", + added: "2026-08-06", + }, + { + id: "partner.authorization.grant", + kind: "composite", + label: "Active authorization grant", + description: "Non-revoked, currently valid SQLite grant for an exact partner, out, provider, skin, scope, limits, currency, and approval provenance tuple.", + category: "pipeline", + color: "tennis", + status: "active", + synonyms: ["live_trade grant", "account_authorizations", "approved authorization"], + seeAlso: ["partner.authorization.request", "partner.execution.gate", "partner.phase.operator_ready", "out.status.ready"], + tone: "concept", + added: "2026-08-06", + }, + { + id: "partner.execution.gate", + kind: "composite", + label: "Authorized execution gate", + description: "Fail-closed decision boundary that verifies the active grant, policy hash, freshness, integer stake caps, provider session, and risk health before reservation.", + category: "trading", + color: "trading", + status: "active", + synonyms: ["execution gate", "evaluateExecutionGate", "risk gate"], + seeAlso: ["partner.authorization.grant", "partner.execution.reservation", "provider.polymarket.intelligence_only", "liquidity_ok", "exposureCents"], + tone: "concept", + added: "2026-08-06", + }, + { + id: "partner.execution.reservation", + kind: "composite", + label: "Exposure reservation", + description: "Transactional integer-minor-unit hold that prevents concurrent orders from exceeding daily, balance, liquidity, or outstanding-exposure limits.", + category: "trading", + color: "kalshi", + unit: "cents", + status: "active", + synonyms: ["exposure_reservations", "pending reservation", "effective stake"], + seeAlso: ["partner.execution.gate", "partner.execution.provider_lifecycle", "exposureCents", "balanceCents"], + tone: "concept", + added: "2026-08-06", + }, + { + id: "partner.execution.provider_lifecycle", + kind: "composite", + label: "Provider order lifecycle", + description: "Normalized provider outcome across accepted, resting, partial-fill, filled, canceled, rejected, ambiguous, and settled states with deterministic reconciliation.", + category: "trading", + color: "kalshi", + status: "active", + synonyms: ["Kalshi lifecycle", "order reconciliation", "provider lifecycle"], + seeAlso: ["partner.execution.reservation", "partner.execution.journal", "remainingCount", "fillCount"], + tone: "concept", + added: "2026-08-06", + }, + { + id: "partner.execution.journal", + kind: "composite", + label: "Authorized execution journal", + description: "Immutable audit record and projections for requests, reservations, provider evidence, cash, exposure, fees, realized P&L, and partner split.", + category: "trading", + color: "research", + status: "active", + synonyms: ["execution journal", "provider lifecycle journal", "audit trail"], + seeAlso: ["partner.execution.provider_lifecycle", "partner.execution.receipt", "snapshot.visual.provenance", "realizedPnlCents", "feesPaidCents"], + tone: "concept", + added: "2026-08-06", + }, + { + id: "partner.execution.receipt", + kind: "composite", + label: "Durable partner receipt", + description: "Retryable outbox message carrying an authorization or execution outcome back to the bound Telegram chat and topic without granting execution authority.", + category: "pipeline", + color: "middleware", + status: "active", + synonyms: ["receipt outbox", "Telegram receipt", "durable receipt"], + seeAlso: ["partner.execution.journal", "event.telegram.message_pinned", "alert.delivery"], + tone: "concept", + added: "2026-08-06", + }, + { + id: "provider.polymarket.intelligence_only", + kind: "composite", + label: "Polymarket intelligence only", + description: "Public Gamma market data may support matching and regulatory line-move intelligence, but cannot authorize an account or prove an execution outcome.", + category: "pipeline", + color: "polymarket", + status: "active", + synonyms: ["Polymarket read-only", "Gamma intelligence", "no Polymarket execution"], + seeAlso: ["poly_mid", "poly_volume", "ui.warehouse.poly_link", "partner.execution.gate"], + tone: "concept", + added: "2026-08-06", + }, + { + id: "snapshot.visual.provenance", + kind: "composite", + label: "Visual snapshot provenance", + description: "Bun-native WebView/Image metadata recording runtime version and revision, browser backend and viewport, capture outcome, decoded source/thumbnail dimensions and formats, and artifact SHA-256/byte length. Persisted capture URLs are credential-redacted. It is audit evidence, never execution authority.", + category: "pipeline", + color: "research", + status: "active", + synonyms: ["snapshot meta", "Bun.Image metadata", "WebView evidence"], + seeAlso: ["partner.execution.journal", "liquidity_ok", "desk.tradable"], + tone: "concept", + added: "2026-08-06", + }, ] as const; /** Which glossary concepts appear on each page/surface. */ @@ -1502,6 +1620,11 @@ export const PAGE_SURFACES = { "event.credit.extended", "event.free_roll.applied", "event.settlement.processed", "event.telegram.invite_sent", "event.telegram.message_pinned", + "partner.authorization.request", "partner.authorization.grant", + "partner.execution.gate", "partner.execution.reservation", + "partner.execution.provider_lifecycle", "partner.execution.journal", + "partner.execution.receipt", "provider.polymarket.intelligence_only", + "snapshot.visual.provenance", ], } as const satisfies Record; diff --git a/src/institutions/official-urls.ts b/src/institutions/official-urls.ts index 7805474..9be655f 100644 --- a/src/institutions/official-urls.ts +++ b/src/institutions/official-urls.ts @@ -2,7 +2,7 @@ * Canonical external URLs — verify when fee/API docs change. * Code cites these constants; do not hardcode stale paths in tenants. * - * Last verified: 2026-07-31 + * Last verified: 2026-08-06 * Note: portfolio endpoints now ship fixed-point fields (*_fp, *_dollars) * alongside legacy integer cents — parse via institutions/ledger-types.ts. * No programmatic deposit/withdraw endpoints exist (bank rails only). @@ -61,8 +61,14 @@ export const OFFICIAL_URLS = { polymarket: { home: "https://polymarket.com/", docs: "https://docs.polymarket.com/", + apiIntroduction: "https://docs.polymarket.com/api-reference/introduction", + quickstart: "https://docs.polymarket.com/quickstart", gammaApiBase: "https://gamma-api.polymarket.com", }, + telegram: { + bots: "https://core.telegram.org/bots", + botApi: "https://core.telegram.org/bots/api", + }, bun: { create: "https://bun.com/docs/runtime/templating/create", test: "https://bun.com/docs/test/index#run-tests", diff --git a/src/partner/domain.ts b/src/partner/domain.ts index ad8751c..ee8a2ce 100644 --- a/src/partner/domain.ts +++ b/src/partner/domain.ts @@ -399,3 +399,40 @@ export function formatDomainStatusText(report: DomainStatusReport): string { 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. + */ +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/fantasy-ultra/pandora-socket.ts b/src/partner/fantasy-ultra/pandora-socket.ts index 6b84fbb..3c3f181 100644 --- a/src/partner/fantasy-ultra/pandora-socket.ts +++ b/src/partner/fantasy-ultra/pandora-socket.ts @@ -257,7 +257,7 @@ export class PandoraSocket { }); ws.addEventListener('message', (ev) => { - const data = (ev as MessageEvent).data; + const data = ev.data; if (data instanceof ArrayBuffer) { this.handleBinaryAttachment(new Uint8Array(data)); return; diff --git a/src/partner/webview-cdp-events.ts b/src/partner/webview-cdp-events.ts new file mode 100644 index 0000000..ff2529d --- /dev/null +++ b/src/partner/webview-cdp-events.ts @@ -0,0 +1,54 @@ +/** Chrome CDP wire shapes used with Bun.WebView.addEventListener(). */ + +export type CdpWebSocketCreated = { + url?: string; + requestId?: string; +}; + +export type CdpWebSocketFrame = { + response?: { payloadData?: string }; + requestId?: string; +}; + +export type CdpWebSocketClosed = { + requestId?: string; +}; + +export function parseCdpWebSocketCreated(event: unknown): CdpWebSocketCreated { + const value = webViewEventPayload(event); + if (!isRecord(value)) return {}; + return { + ...(typeof value.url === "string" ? { url: value.url } : {}), + ...(typeof value.requestId === "string" ? { requestId: value.requestId } : {}), + }; +} + +export function parseCdpWebSocketFrame(event: unknown): CdpWebSocketFrame { + const value = webViewEventPayload(event); + if (!isRecord(value)) return {}; + const response = isRecord(value.response) ? value.response : null; + return { + ...(typeof value.requestId === "string" ? { requestId: value.requestId } : {}), + ...(response && typeof response.payloadData === "string" + ? { response: { payloadData: response.payloadData } } + : {}), + }; +} + +export function parseCdpWebSocketClosed(event: unknown): CdpWebSocketClosed { + const value = webViewEventPayload(event); + return isRecord(value) && typeof value.requestId === "string" + ? { requestId: value.requestId } + : {}; +} + +function webViewEventPayload(event: unknown): unknown { + if (!isRecord(event)) return undefined; + if ("data" in event) return event.data; + if ("detail" in event) return event.detail; + return undefined; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/src/partner/webview-ws-capture.ts b/src/partner/webview-ws-capture.ts index 1aeb0fd..768b327 100644 --- a/src/partner/webview-ws-capture.ts +++ b/src/partner/webview-ws-capture.ts @@ -8,7 +8,32 @@ // @see https://bun.com/docs/runtime/webview import { mkdirSync } from "node:fs"; import { join } from "node:path"; +import { + readArtifactIntegrity, + type SnapshotArtifactIntegrity, +} from "../institutions/event-store/visual-snapshot-meta.ts"; import type { WebViewWsFrame } from "./webview-ws-ingest.ts"; +import { + parseCdpWebSocketClosed, + parseCdpWebSocketCreated, + parseCdpWebSocketFrame, + type CdpWebSocketClosed, + type CdpWebSocketCreated, + type CdpWebSocketFrame, +} from "./webview-cdp-events.ts"; + +export type PartnerWebViewSnapshotMeta = { + schemaVersion: 1; + capturedAt: string; + runtime: { bunVersion: string; bunRevision: string }; + webview: { + backend: "chrome"; + width: 1280; + height: 800; + cdpNetworkCapture: true; + }; + artifact: SnapshotArtifactIntegrity; +}; export type WebViewCaptureOptions = { /** plive sport widget id (default 220 table tennis) */ @@ -29,13 +54,14 @@ export type WebViewCaptureResult = { subscribeMsg: string | null; coeffSubscribeCount: number; frameCount: number; + snapshotMeta: PartnerWebViewSnapshotMeta; }; export async function capturePandoraViaWebView( options: WebViewCaptureOptions = {}, ): Promise { if (typeof Bun.WebView !== "function") { - throw new Error("Bun.WebView unavailable — need Bun ≥1.4 with WebView"); + throw new Error("Bun.WebView unavailable in the active Bun runtime/build"); } const seconds = Math.min( @@ -71,24 +97,21 @@ export async function capturePandoraViaWebView( await view.navigate("about:blank"); await view.cdp("Network.enable", {}); - view.addEventListener("Network.webSocketCreated", (ev) => { - const d = (ev as unknown as CustomEvent).detail ?? (ev as MessageEvent).data; - const data = d as { url?: string; requestId?: string }; - if (data?.requestId && data.url) requestUrlById.set(data.requestId, data.url); + view.addEventListener("Network.webSocketCreated", (ev) => { + const data = parseCdpWebSocketCreated(ev); + const safeUrl = data?.url ? redactCaptureUrl(data.url) : undefined; + if (data?.requestId && safeUrl) requestUrlById.set(data.requestId, safeUrl); frames.push({ t: Date.now(), dir: "created", - url: data?.url, + url: safeUrl, requestId: data?.requestId, }); }); const pushFrame = ( dir: "sent" | "recv", - data: { - response?: { payloadData?: string }; - requestId?: string; - }, + data: CdpWebSocketFrame, ) => { const payload = data?.response?.payloadData ?? ""; frames.push({ @@ -102,19 +125,16 @@ export async function capturePandoraViaWebView( }); }; - view.addEventListener("Network.webSocketFrameSent", (ev) => { - const d = (ev as unknown as CustomEvent).detail ?? (ev as MessageEvent).data; - pushFrame("sent", d as { response?: { payloadData?: string }; requestId?: string }); + view.addEventListener("Network.webSocketFrameSent", (ev) => { + pushFrame("sent", parseCdpWebSocketFrame(ev)); }); - view.addEventListener("Network.webSocketFrameReceived", (ev) => { - const d = (ev as unknown as CustomEvent).detail ?? (ev as MessageEvent).data; - pushFrame("recv", d as { response?: { payloadData?: string }; requestId?: string }); + view.addEventListener("Network.webSocketFrameReceived", (ev) => { + pushFrame("recv", parseCdpWebSocketFrame(ev)); }); - view.addEventListener("Network.webSocketClosed", (ev) => { - const d = (ev as unknown as CustomEvent).detail ?? (ev as MessageEvent).data; - const data = d as { requestId?: string }; + view.addEventListener("Network.webSocketClosed", (ev) => { + const data = parseCdpWebSocketClosed(ev); frames.push({ t: Date.now(), dir: "closed", @@ -147,6 +167,20 @@ export async function capturePandoraViaWebView( outPath, frames.map((f) => JSON.stringify(f)).join("\n") + "\n", ); + const artifact = await readArtifactIntegrity(outPath); + if (!artifact) throw new Error("WebView capture artifact integrity unavailable"); + const snapshotMeta: PartnerWebViewSnapshotMeta = { + schemaVersion: 1, + capturedAt: new Date(stamp).toISOString(), + runtime: { bunVersion: Bun.version, bunRevision: Bun.revision }, + webview: { + backend: "chrome", + width: 1280, + height: 800, + cdpNetworkCapture: true, + }, + artifact, + }; const sent42 = frames.filter( (f) => f.dir === "sent" && (f.payload?.startsWith("42") ?? false), @@ -166,12 +200,13 @@ export async function capturePandoraViaWebView( const summary = { outPath, summaryPath, - startUrl, - finalUrl: view.url, + startUrl: redactCaptureUrl(startUrl), + finalUrl: redactCaptureUrl(String(view.url ?? startUrl)), seconds, frameCount: frames.length, subscribeMsg: subscribeMsgs[0]?.payload ?? null, coeffSubscribeCount: coeffSubscribes.length, + snapshotMeta, }; await Bun.write(summaryPath, JSON.stringify(summary, null, 2) + "\n"); @@ -185,5 +220,16 @@ export async function capturePandoraViaWebView( subscribeMsg: summary.subscribeMsg, coeffSubscribeCount: coeffSubscribes.length, frameCount: frames.length, + snapshotMeta, }; } + +/** Strip credentials/query/hash from persisted capture summaries. Raw capture stays local. */ +export function redactCaptureUrl(value: string): string { + try { + const url = new URL(value); + return `${url.protocol}//${url.host}${url.pathname}`; + } catch { + return "[redacted-invalid-url]"; + } +} diff --git a/tests/audit-bun-native.test.ts b/tests/audit-bun-native.test.ts index cb486a4..ffffb62 100644 --- a/tests/audit-bun-native.test.ts +++ b/tests/audit-bun-native.test.ts @@ -22,7 +22,7 @@ describe("bun-native guard", () => { expect(violations.map((item) => item.message)).toEqual([ "dependencies.wrap-ansi duplicates Bun.wrapAnsi()", - "devDependencies.@iarna/toml duplicates Bun.TOML.parse() / Bun.TOML.stringify()", + "devDependencies.@iarna/toml duplicates Bun.TOML.parse() / governed tomlStringify()", ]); }); @@ -36,7 +36,7 @@ describe("bun-native guard", () => { expect(violations.map((item) => item.message)).toEqual([ "dependencies.ansi aliases wrap-ansi; use Bun.wrapAnsi()", - "dependencies.parser aliases @iarna/toml; use Bun.TOML.parse() / Bun.TOML.stringify()", + "dependencies.parser aliases @iarna/toml; use Bun.TOML.parse() / governed tomlStringify()", ]); }); @@ -66,6 +66,13 @@ describe("bun-native guard", () => { expect(violations).toEqual([]); }); + test("blocks declarations that shadow canonical Bun types", () => { + expect(findSourceViolations("declare namespace Bun { interface WebView {} }")[0]?.message) + .toContain("canonical bun-types"); + expect(findSourceViolations("interface Bun { version: string }")[0]?.message) + .toContain("canonical runtime/type namespace"); + }); + test("audits an explicit Git file universe and ignores nested dependency trees", async () => { const root = mkdtempSync(join(tmpdir(), "bun-native-guard-")); try { diff --git a/tests/institutions/match-liquidity-ground.test.ts b/tests/institutions/match-liquidity-ground.test.ts index a4c36c3..a1bb50c 100644 --- a/tests/institutions/match-liquidity-ground.test.ts +++ b/tests/institutions/match-liquidity-ground.test.ts @@ -78,6 +78,9 @@ describe("match-liquidity ground + snapshot summary", () => { const outDir = mkdtempSync(join(tmpdir(), "liq-ground-")); const artifact = await captureMatchLiquidityGround(db, { htmlOnly: true, outDir }); expect(artifact.webview).toBe(false); + expect(artifact.snapshotMeta?.runtime.bunVersion).toBe(Bun.version); + expect(artifact.snapshotMeta?.webview.width).toBe(1280); + expect(artifact.snapshotMeta?.image.thumbnail).toBeNull(); expect(await Bun.file(artifact.dashboardHtml).exists()).toBe(true); const html = await Bun.file(artifact.dashboardHtml).text(); expect(html).toContain("liq_ok"); @@ -87,6 +90,7 @@ describe("match-liquidity ground + snapshot summary", () => { ); expect(latest.tradable).toBe(1); expect(latest.rows).toBe(1); + expect(latest.snapshotMeta.schemaVersion).toBe(1); const lines = formatMatchLiquidityGroundLines(artifact); expect(lines.some((l) => l.includes("webview=skipped"))).toBe(true); }); diff --git a/tests/institutions/tennis-ws-ground.test.ts b/tests/institutions/tennis-ws-ground.test.ts index c68b9aa..e8f8d33 100644 --- a/tests/institutions/tennis-ws-ground.test.ts +++ b/tests/institutions/tennis-ws-ground.test.ts @@ -104,6 +104,10 @@ describe("tennis-ws-ground artifact", () => { expect(html).toContain("kalshi-ws ticks"); expect(artifact.webview).toBe(false); expect(artifact.image).toBe(false); + expect(artifact.snapshotMeta?.runtime.bunVersion).toBe(Bun.version); + expect(artifact.snapshotMeta?.runtime.bunRevision).toBe(Bun.revision); + expect(artifact.snapshotMeta?.webview.captured).toBe(false); + expect(artifact.snapshotMeta?.image.source).toBeNull(); } finally { await Bun.$`rm -rf ${outDir}`.nothrow().quiet(); } diff --git a/tests/institutions/visual-snapshot-meta.test.ts b/tests/institutions/visual-snapshot-meta.test.ts new file mode 100644 index 0000000..c2cfcdb --- /dev/null +++ b/tests/institutions/visual-snapshot-meta.test.ts @@ -0,0 +1,52 @@ +import { expect, test } from "bun:test"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { mkdtempSync } from "node:fs"; +import { buildVisualSnapshotMeta } from "../../src/institutions/event-store/visual-snapshot-meta.ts"; + +test("visual snapshot metadata is derived through Bun.Image and Bun.WebView types", async () => { + const dir = mkdtempSync(join(tmpdir(), "visual-snapshot-meta-")); + const sourcePath = join(dir, "source.png"); + const thumbnailPath = join(dir, "thumbnail.png"); + const onePixelPng = Uint8Array.from( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", + "base64", + ), + ); + try { + await Promise.all([ + Bun.write(sourcePath, onePixelPng), + Bun.write(thumbnailPath, onePixelPng), + ]); + const meta = await buildVisualSnapshotMeta({ + capturedAt: "2026-08-06T00:00:00.000Z", + backend: "webkit", + width: 1280, + height: 720, + webviewCaptured: true, + imageGenerated: true, + sourcePath, + thumbnailPath, + }); + expect(meta.runtime).toEqual({ + bunVersion: Bun.version, + bunRevision: Bun.revision, + }); + expect(meta.webview).toMatchObject({ + available: true, + captured: true, + backend: "webkit", + width: 1280, + height: 720, + }); + expect(meta.image.source?.metadata).toEqual({ width: 1, height: 1, format: "png" }); + expect(meta.image.thumbnail?.metadata).toEqual({ width: 1, height: 1, format: "png" }); + expect(meta.image.source?.sizeBytes).toBe(onePixelPng.byteLength); + expect(meta.image.source?.sha256).toMatch(/^[a-f0-9]{64}$/); + expect(meta.webview.attempted).toBe(true); + expect(meta.webview.error).toBeNull(); + } finally { + await Bun.$`rm -rf ${dir}`.nothrow().quiet(); + } +}); diff --git a/tests/partner/domain.test.ts b/tests/partner/domain.test.ts index c1797e6..1db4f2c 100644 --- a/tests/partner/domain.test.ts +++ b/tests/partner/domain.test.ts @@ -5,6 +5,7 @@ import { PARTNER_NAMING, buildDomainStatusReport, formatDomainStatusText, + formatPartnerExpansionMermaid, } from "../../src/partner/domain.ts"; describe("partner domain architecture", () => { @@ -25,4 +26,14 @@ describe("partner domain architecture", () => { expect(PARTNER_NAMING.outIdExample).toBe("out-SPEN-1"); expect(formatDomainStatusText(report)).toContain("partner domain"); }); + + test("expansion map distinguishes built execution from intelligence and unwired providers", () => { + const map = formatPartnerExpansionMermaid(); + expect(map).toContain("Telegram group/topic"); + expect(map).toContain("Kalshi V2 execution"); + expect(map).toContain("Polymarket Gamma market data"); + expect(map).toContain("Polymarket execution adapter
not implemented"); + expect(map).toContain("future provider-parity contract"); + expect(map).toContain("blocked pending idempotency contract"); + }); }); diff --git a/tests/partner/webview-ws-capture.test.ts b/tests/partner/webview-ws-capture.test.ts new file mode 100644 index 0000000..4c1ad41 --- /dev/null +++ b/tests/partner/webview-ws-capture.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from "bun:test"; +import { redactCaptureUrl } from "../../src/partner/webview-ws-capture.ts"; +import { + parseCdpWebSocketCreated, + parseCdpWebSocketFrame, +} from "../../src/partner/webview-cdp-events.ts"; + +test("WebView capture summaries redact signed URL material", () => { + expect( + redactCaptureUrl( + "https://plive.example/live/path?token=secret&account=42#!/sport/220", + ), + ).toBe("https://plive.example/live/path"); + expect(redactCaptureUrl("not a URL")).toBe("[redacted-invalid-url]"); +}); + +test("local CDP adapter validates data/detail without augmenting Bun types", () => { + expect( + parseCdpWebSocketCreated({ + data: { requestId: "r1", url: "wss://example.test/socket", ignored: 1 }, + }), + ).toEqual({ requestId: "r1", url: "wss://example.test/socket" }); + expect( + parseCdpWebSocketFrame({ + detail: { requestId: "r1", response: { payloadData: "42[]" } }, + }), + ).toEqual({ requestId: "r1", response: { payloadData: "42[]" } }); + expect(parseCdpWebSocketFrame({ data: "invalid" })).toEqual({}); +}); diff --git a/tools/partner-domain.ts b/tools/partner-domain.ts index e1cd6ad..6f0aac0 100644 --- a/tools/partner-domain.ts +++ b/tools/partner-domain.ts @@ -4,6 +4,8 @@ * * bun run partner:domain * bun run partner:domain -- --json + * bun run partner:map + * bun run partner:map -- --output=artifacts/partner-expansion.mmd * * @see docs/PARTNER-DOMAIN.md * @see src/partner/domain.ts @@ -11,9 +13,30 @@ 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 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 = buildDomainStatusReport(); if (json) { diff --git a/tools/partner-webview-ws-capture.ts b/tools/partner-webview-ws-capture.ts index b6ed44d..3ec6119 100644 --- a/tools/partner-webview-ws-capture.ts +++ b/tools/partner-webview-ws-capture.ts @@ -18,6 +18,16 @@ */ import { mkdirSync } from 'node:fs'; import { join } from 'node:path'; +import { readArtifactIntegrity } from '../src/institutions/event-store/visual-snapshot-meta.ts'; +import { redactCaptureUrl } from '../src/partner/webview-ws-capture.ts'; +import { + parseCdpWebSocketClosed, + parseCdpWebSocketCreated, + parseCdpWebSocketFrame, + type CdpWebSocketClosed, + type CdpWebSocketCreated, + type CdpWebSocketFrame, +} from '../src/partner/webview-cdp-events.ts'; function argValue(name: string): string | undefined { const hit = process.argv.find(a => a.startsWith(`--${name}=`)); @@ -46,7 +56,7 @@ const frames: Frame[] = []; const requestUrlById = new Map(); if (typeof Bun.WebView !== 'function') { - console.error('Bun.WebView unavailable — need Bun ≥1.4 with WebView'); + console.error('Bun.WebView unavailable in the active Bun runtime/build'); process.exit(1); } @@ -91,25 +101,21 @@ await using view = new Bun.WebView({ await view.navigate('about:blank'); await view.cdp('Network.enable', {}); -view.addEventListener('Network.webSocketCreated', ev => { - const d = (ev as unknown as CustomEvent).detail ?? (ev as MessageEvent).data; - const data = d as { url?: string; requestId?: string }; - if (data?.requestId && data.url) requestUrlById.set(data.requestId, data.url); +view.addEventListener('Network.webSocketCreated', ev => { + const data = parseCdpWebSocketCreated(ev); + const safeUrl = data?.url ? redactCaptureUrl(data.url) : undefined; + if (data?.requestId && safeUrl) requestUrlById.set(data.requestId, safeUrl); frames.push({ t: Date.now(), dir: 'created', - url: data?.url, + url: safeUrl, requestId: data?.requestId, }); - console.error('WS created', data?.url?.slice(0, 120) ?? '(no url)'); + console.error('WS created', safeUrl?.slice(0, 120) ?? '(no url)'); }); -view.addEventListener('Network.webSocketFrameSent', ev => { - const d = (ev as unknown as CustomEvent).detail ?? (ev as MessageEvent).data; - const data = d as { - response?: { payloadData?: string }; - requestId?: string; - }; +view.addEventListener('Network.webSocketFrameSent', ev => { + const data = parseCdpWebSocketFrame(ev); const payload = data?.response?.payloadData ?? ''; const url = data?.requestId ? requestUrlById.get(data.requestId) : undefined; frames.push({ @@ -128,12 +134,8 @@ view.addEventListener('Network.webSocketFrameSent', ev => { } }); -view.addEventListener('Network.webSocketFrameReceived', ev => { - const d = (ev as unknown as CustomEvent).detail ?? (ev as MessageEvent).data; - const data = d as { - response?: { payloadData?: string }; - requestId?: string; - }; +view.addEventListener('Network.webSocketFrameReceived', ev => { + const data = parseCdpWebSocketFrame(ev); const payload = data?.response?.payloadData ?? ''; const url = data?.requestId ? requestUrlById.get(data.requestId) : undefined; frames.push({ @@ -156,9 +158,8 @@ view.addEventListener('Network.webSocketFrameReceived', ev => { } }); -view.addEventListener('Network.webSocketClosed', ev => { - const d = (ev as unknown as CustomEvent).detail ?? (ev as MessageEvent).data; - const data = d as { requestId?: string }; +view.addEventListener('Network.webSocketClosed', ev => { + const data = parseCdpWebSocketClosed(ev); frames.push({ t: Date.now(), dir: 'closed', @@ -187,6 +188,20 @@ try { await Bun.sleep(seconds * 1000); await Bun.write(outPath, frames.map(f => JSON.stringify(f)).join('\n') + '\n'); +const artifact = await readArtifactIntegrity(outPath); +if (!artifact) throw new Error('WebView capture artifact integrity unavailable'); +const snapshotMeta = { + schemaVersion: 1 as const, + capturedAt: new Date(stamp).toISOString(), + runtime: { bunVersion: Bun.version, bunRevision: Bun.revision }, + webview: { + backend: 'chrome' as const, + width: 1280 as const, + height: 800 as const, + cdpNetworkCapture: true as const, + }, + artifact, +}; const sent42 = frames.filter(f => f.dir === 'sent' && (f.payload?.startsWith('42') ?? false)); const recv42 = frames.filter(f => f.dir === 'recv' && (f.payload?.startsWith('42') ?? false)); @@ -230,11 +245,12 @@ const pandoraCreated = frames.filter(f => f.dir === 'created' && isPandoraUrl(f. const summary = { outPath, summaryPath, - startUrl, - finalUrl: view.url, + startUrl: redactCaptureUrl(startUrl), + finalUrl: redactCaptureUrl(String(view.url ?? startUrl)), seconds, frameCount: frames.length, - pandoraSockets: pandoraCreated.map(f => f.url), + pandoraSockets: pandoraCreated.map(f => f.url ? redactCaptureUrl(f.url) : undefined), + snapshotMeta, topics: [...topics].sort(), /** Primary client emits that start the live book (paste into adapter / probe). */ subscribeMsg: subscribeMsgs[0]?.payload ?? null, diff --git a/tsconfig.json b/tsconfig.json index 8064f8b..0de8bda 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "allowImportingTsExtensions": true, "verbatimModuleSyntax": true }, - "include": ["src/**/*.ts", "tests/**/*.ts", "alpha/**/*.ts", "tools/**/*.ts", "scripts/train-elo.ts"] + "include": ["src/**/*.ts", "tests/**/*.ts", "alpha/**/*.ts", "tools/**/*.ts", "scripts/**/*.ts"] }