From 236e21bcd9f42ee0bb83d24c1d03db5013b67bf5 Mon Sep 17 00:00:00 2001 From: Volodymyr Zakhovaiko Date: Tue, 4 Aug 2026 23:13:43 +0200 Subject: [PATCH] refactor(server): serve runtime openapi reference --- .rulesync/commands/pre-pr.md | 2 +- .rulesync/commands/regen.md | 5 +- .rulesync/commands/scaffold-module.md | 2 +- .rulesync/commands/scaffold-route.md | 2 +- .rulesync/rules/conventions.md | 2 +- .rulesync/rules/overview.md | 18 +- .rulesync/subagents/contract-reviewer.md | 4 +- .rulesync/subagents/dev.md | 2 +- .rulesync/subagents/docs.md | 5 +- .rulesync/subagents/expert.md | 2 +- .rulesync/subagents/module-author.md | 4 +- .rulesync/subagents/operator.md | 2 +- .rulesync/subagents/plugin-author.md | 2 +- CONTRIBUTING.md | 6 +- GEMINI.md | 14 +- README.md | 4 +- apps/mcp-server-dev/src/main.ts | 48 +----- docs/adr/0035-runtime-openapi-reference.md | 51 ++++++ docs/agent-quickstart.md | 2 +- docs/agentic-workflow.md | 4 +- docs/architecture.md | 12 +- docs/catalog.json | 163 ------------------ docs/downstream-consumer.md | 6 +- docs/glossary.md | 2 +- docs/mcp-setup.md | 3 +- docs/standards/database.md | 2 +- docs/system-design.md | 4 +- extensions.config.ts | 4 +- package.json | 5 +- packages/core/generators/src/config.ts | 26 --- .../generators/src/templates/contract.hbs | 2 +- .../runtime/__tests__/create-app.test.ts | 21 ++- .../core/src/server/runtime/create-app.ts | 38 ++-- packages/core/src/server/runtime/index.ts | 3 - packages/core/src/server/runtime/openapi.ts | 34 ---- packages/mcp/docs/catalog.json | 3 +- packages/mcp/src/main.ts | 9 +- packages/testing/src/app.ts | 10 +- tools/create/create-service.ts | 4 +- tools/gen/build-contract.ts | 57 ------ tools/gen/gen-catalog.ts | 25 +-- tools/gen/gen-openapi.ts | 22 --- tools/setup/setup-agent.ts | 2 +- tools/templates/consumer/README.md.tpl | 2 +- .../commands/scaffold-route.md | 2 +- .../consumer/apps/api/src/main.ts.tpl | 4 +- 46 files changed, 152 insertions(+), 494 deletions(-) create mode 100644 docs/adr/0035-runtime-openapi-reference.md delete mode 100644 packages/core/src/server/runtime/openapi.ts delete mode 100644 tools/gen/build-contract.ts delete mode 100644 tools/gen/gen-openapi.ts diff --git a/.rulesync/commands/pre-pr.md b/.rulesync/commands/pre-pr.md index 367856bc..b4c7340c 100644 --- a/.rulesync/commands/pre-pr.md +++ b/.rulesync/commands/pre-pr.md @@ -1,7 +1,7 @@ --- targets: - '*' -description: 'Run the full pre-PR gate locally - `pnpm verify` plus the drift check CI runs (`pnpm check:drift`), which `pnpm verify` alone does NOT cover. Catches stale catalog/openapi/agent-docs before push.' +description: 'Run the full pre-PR gate locally - `pnpm verify` plus the drift check CI runs (`pnpm check:drift`), which `pnpm verify` alone does NOT cover. Catches stale catalog/agent-docs before push.' --- Run the same gate CI enforces, in order. Stop at the first failure and report it. diff --git a/.rulesync/commands/regen.md b/.rulesync/commands/regen.md index 7f37f307..609db082 100644 --- a/.rulesync/commands/regen.md +++ b/.rulesync/commands/regen.md @@ -1,15 +1,14 @@ --- targets: - '*' -description: 'Regenerate all derived artifacts - oRPC OpenAPI spec, Drizzle client, and the machine-readable docs/catalog.json. Run after any change to Drizzle tables or oRPC contracts.' +description: 'Regenerate all derived artifacts - Drizzle client and the machine-readable docs/catalog.json. Run after any change to Drizzle tables or oRPC contracts.' --- Run `pnpm regen` in the repo root. This runs in order (see root `package.json`): -1. `turbo run check:types` - emits `docs/openapi.json` from the composed oRPC contract and - regenerates any per-package codegen registered with turbo. +1. `pnpm gen:tsconfig` - synchronizes TypeScript path aliases. 2. `pnpm gen:drizzle` (`scripts/generate-all.mjs`) - discovers every module's `src/**/drizzle.config.ts` and runs `drizzle-kit generate` per module, against that module's own co-located `drizzle/migrations/` history (ADR-0027). diff --git a/.rulesync/commands/scaffold-module.md b/.rulesync/commands/scaffold-module.md index ae75c906..d1b61b2d 100644 --- a/.rulesync/commands/scaffold-module.md +++ b/.rulesync/commands/scaffold-module.md @@ -13,6 +13,6 @@ The scaffold ships a buildable module - a `list` route wired end to end (contrac 3. `contract/index.ts` - Zod input/output schemas for the routes (the single source of wire truth). 4. `service/.service.ts` - business logic; inject `DRIZZLE` + `EVENT_BUS` + adapter ports via the constructor; never inline fetch/SQL. Audit every mutation. 5. `router/index.ts` - oRPC routes with imported schemas; admin routes call `await adminGuard.assert(context)` first. -6. `pnpm regen` (OpenAPI + migration + catalog), then `pnpm verify`. +6. `pnpm regen` (migration + catalog), then `pnpm verify`. Tell the user what was generated and what remains to fill in. diff --git a/.rulesync/commands/scaffold-route.md b/.rulesync/commands/scaffold-route.md index d4f47dab..b8228c97 100644 --- a/.rulesync/commands/scaffold-route.md +++ b/.rulesync/commands/scaffold-route.md @@ -6,7 +6,7 @@ description: 'Add an oRPC route stub to an existing module. Args: . The domain is the module's dir under `packages/core/src/`. -Before adding, call the MCP tool `query-openapi` with the path to confirm the route doesn't already exist. +Before adding, call the MCP tool `list-routes` to confirm the route doesn't already exist. Run `pnpm gen route ` in the repo root. The generator adds BOTH a contract procedure (in the module's `contract/index.ts`) and a matching router handler - no inline Zod in the router. diff --git a/.rulesync/rules/conventions.md b/.rulesync/rules/conventions.md index 08d42f54..415510ad 100644 --- a/.rulesync/rules/conventions.md +++ b/.rulesync/rules/conventions.md @@ -55,7 +55,7 @@ The always-on core of the code standard: what you must obey while typing. Detail - Inline `fetch`/`axios` in module code - third-party access is a port + adapter bound at the root. - Comments. The only exception is a fact the code cannot contain (external-system behaviour, a spec constraint) and JSDoc on a public export. A rationale is not a fact - it goes in the commit or an ADR. - Deep (`../../`+) relative imports that leave your zone/module, imports of another module's internals, import cycles, deep `dist/`/`src/` paths into another package. -- Hand-edited generated files: migrations, `docs/openapi.json`, `docs/catalog.json`, per-tool agent mirrors. +- Hand-edited generated files: migrations, `docs/catalog.json`, per-tool agent mirrors. ## Always diff --git a/.rulesync/rules/overview.md b/.rulesync/rules/overview.md index 1a63b41f..47842769 100644 --- a/.rulesync/rules/overview.md +++ b/.rulesync/rules/overview.md @@ -22,8 +22,8 @@ Before acting on any non-trivial request - and before delegating - run the `enha ## Architecture pillars -1. **Zod-first contracts.** Every shape is a Zod schema; types are `z.infer`'d, never hand-written. Cross-cutting schemas in `packages/core/src/contracts/schemas/`; each module OWNS its route contract + req/res schemas + `z.infer`'d types in its `contract/` dir - the single source of wire truth, nothing else re-declares a wire shape. `composeContract` (`@openora/core/contracts`) owns only `health`; the composition root (`tools/gen/build-contract.ts` here, the consumer's entry when deployed) composes each enabled module's `/contract` slice into the one runtime contract the SDK links against. ADR-0021/0025. -2. **oRPC + Hono.** oRPC owns route definition + Zod validation + OpenAPI emit; its `OpenAPIHandler` mounts on a Hono server. DI is a functional `Container` (`@openora/core/server`) - typed-token factories, no decorators, no `reflect-metadata`. ADR-0009. +1. **Zod-first contracts.** Every shape is a Zod schema; types are `z.infer`'d, never hand-written. Cross-cutting schemas in `packages/core/src/contracts/schemas/`; each module OWNS its route contract + req/res schemas + `z.infer`'d types in its `contract/` dir - the single source of wire truth, nothing else re-declares a wire shape. `composeContract` (`@openora/core/contracts`) owns only `health`; the consumer composition root composes each enabled module's `/contract` slice into the one runtime contract the SDK links against. ADR-0021/0025. +2. **oRPC + Hono.** oRPC owns route definition + Zod validation; its `OpenAPIHandler` mounts on a Hono server with a live OpenAPI reference. DI is a functional `Container` (`@openora/core/server`) - typed-token factories, no decorators, no `reflect-metadata`. ADR-0009. 3. **Plugin host.** `definePlugin({ id, dependsOn, register })` is the only way new functionality enters. Everything (core modules included) loads through `extensions.config.ts`. 4. **Headless.** Backend modules + contracts + SDK surface only. UI lives in the consumer, which imports `@openora/core/react` (hooks, typed client, auth, realtime). No UI packages here. 5. **Explicit > magic.** No auto-discovery, no decorators. Everything greppable; every wiring point a typed call. @@ -51,13 +51,13 @@ packages/ docs/ adr/ # architecture decision records catalog.json # generated surface (routes/schemas/adapters/slots/events); read by @openora/mcp -tools/ # grouped: gen/ (gen.ts scaffolder, build-contract, gen-openapi, gen-catalog), lint/ (oxlint plugins, verify-module-shape), create/, db/ (seed), setup/ +tools/ # grouped: gen/ (gen.ts scaffolder, gen-catalog), lint/ (oxlint plugins, verify-module-shape), create/, db/ (seed), setup/ extensions.config.ts # the single registry of enabled plugins ``` ## Where does X go? (decision tree) -- **New business domain** (eg "tournaments") -> `pnpm gen module ` creates `packages/core/src///`, wires the domain barrels + the `@openora/core` exports map + the `/contract` slice in `tools/gen/build-contract.ts`, and registers it in `extensions.config.ts`. Every module owns its `drizzle.config.ts` + migration history. ADR-0024/0025/0027. +- **New business domain** (eg "tournaments") -> `pnpm gen module ` creates `packages/core/src///`, wires the domain barrels + the `@openora/core` exports map, and registers it in `extensions.config.ts`. Every module owns its `drizzle.config.ts` + migration history. ADR-0024/0025/0027. - **Extend/override an existing module** -> overlay plugin: `pnpm gen plugin ` -> `extensions//plugin.ts` (repo root here; the consumer's app when deployed). - **New HTTP route** -> the module's `router/index.ts` via `pnpm gen route `. Player routes resolve the caller from `x-user-id`; admin routes MUST be guarded (next). - **Admin-only route** -> `plugin.ts` resolves `AdminGuard` (`c.get(ADMIN_GUARD)`, seeded by `createApp`) and passes it into the router; `await adminGuard.assert(context)` is the handler's FIRST line. The single admin-enforcement point - never re-implement the role check. @@ -105,13 +105,13 @@ Scripts are grouped by prefix - `check:*` reports, `fix:*` rewrites, `gen:*` emi ``` pnpm setup # first time: docker + db + mcp + summary pnpm dev # turbo dev (docs, mcp) -pnpm regen # tsconfig paths + openapi emit + drizzle generate + catalog +pnpm regen # tsconfig paths + drizzle generate + catalog pnpm db:seed # demo data (idempotent; admin@oss.dev / password123) pnpm verify # the full gate: every check:* + test:unit + test:integration + test:tools, in parallel pnpm test:unit # infra-free suite (~4s); *.int.test.ts run in test:integration (docker pg + redis) pnpm db:setup:test:fresh # recreate the shared e2e db after editing an already-applied migration pnpm check:boundaries # just the whole-graph boundary + cycle gate -pnpm check:drift # catalog/openapi staleness (CI-only; not part of verify) +pnpm check:drift # catalog staleness (CI-only; not part of verify) pnpm fix:lint # oxlint --fix; pair with fix:format pnpm -F @openora/core vitest run # one test file/dir, eg src/iam/__tests__ ``` @@ -148,9 +148,9 @@ For platform development (this repo); consumer agents ship in `tools/templates/c ## Working rules for agents -- Use the `oss-dev` MCP server (`.mcp.json`, pre-approved) for read-only inspection: `read-agents-md`, `list-modules`, `describe-module`, `list-routes`, `list-extension-points`, `query-openapi`, `get-drizzle-schema`, `propose-table-change`, `schema-get`, `docs-search`, `db-query-readonly`. Faster than grep, reflects current state. -- Before a route: `query-openapi`. Before a table: `propose-table-change`. After any change: `pnpm verify --filter `; fix failures before continuing. +- Use the `oss-dev` MCP server (`.mcp.json`, pre-approved) for read-only inspection: `read-agents-md`, `list-modules`, `describe-module`, `list-routes`, `list-extension-points`, `get-drizzle-schema`, `propose-table-change`, `schema-get`, `docs-search`, `db-query-readonly`. Faster than grep, reflects current state. +- Before a route: `list-routes`. Before a table: `propose-table-change`. After any change: `pnpm verify --filter `; fix failures before continuing. - Read the touched module's `AGENTS.md` before editing it; keep it updated when invariants or extension seams change. Every `AGENTS.md` (this one included) stays lean: only what code can't say - invariants, rationale, gotchas, extension seams. Never route/table/event listings or "see `contract/`" pointers; agents already know to read `contract/`, `schema/`, `docs/catalog.json`. Claude Code loads them via generated per-module `CLAUDE.md` stubs (`tools/gen/gen-claude-stubs.mjs`, gitignored). - Small PRs scoped to one module; cross-module changes need human approval. Never commit unless asked; never push without explicit per-action confirmation. - ASCII only in code; short dashes (-) only, never long dashes. -- **Never run two agents that both call `pnpm regen` (or `pnpm sync:agents`) against the same working tree.** Both rewrite shared generated state - drizzle migration journals, `docs/catalog.json`, `docs/openapi.json` - and the second run silently discards the first agent's freshly generated migration. The tree then holds a new enum value or table with NO migration: unit tests, lint and `boundaries` all still pass, so it surfaces at deploy, not in CI. Parallelise agents only across disjoint modules with regen serialised afterwards by one owner, and `git status -- '**/drizzle/migrations/**'` before handing off. +- **Never run two agents that both call `pnpm regen` (or `pnpm sync:agents`) against the same working tree.** Both rewrite shared generated state - drizzle migration journals and `docs/catalog.json` - and the second run silently discards the first agent's freshly generated migration. The tree then holds a new enum value or table with NO migration: unit tests, lint and `boundaries` all still pass, so it surfaces at deploy, not in CI. Parallelise agents only across disjoint modules with regen serialised afterwards by one owner, and `git status -- '**/drizzle/migrations/**'` before handing off. diff --git a/.rulesync/subagents/contract-reviewer.md b/.rulesync/subagents/contract-reviewer.md index 45a7ea5c..3e75e165 100644 --- a/.rulesync/subagents/contract-reviewer.md +++ b/.rulesync/subagents/contract-reviewer.md @@ -15,7 +15,7 @@ Stance: assume the change is BROKEN until you trace it working - review to falsi ## Grounding -If the orchestrator passed a base ref + changed-file list, use them - do not re-scope the diff. Otherwise: `git diff origin/dev...HEAD --name-only`. READ each changed file before judging it - never infer behavior from a hunk. Compare route changes against the committed `docs/openapi.json`. Cite the rule doc (`conventions`, `docs/standards/*.md`, root `AGENTS.md`) or ADR each finding rests on. +If the orchestrator passed a base ref + changed-file list, use them - do not re-scope the diff. Otherwise: `git diff origin/dev...HEAD --name-only`. READ each changed file before judging it - never infer behavior from a hunk. Compare route changes against the module contract. Cite the rule doc (`conventions`, `docs/standards/*.md`, root `AGENTS.md`) or ADR each finding rests on. ## Checklist @@ -29,7 +29,7 @@ If the orchestrator passed a base ref + changed-file list, use them - do not re- - [ ] Zod schemas live in the module's `contract/`/`schemas/` or core contracts - no ad-hoc schemas in handlers; no `z.any()`/`z.unknown()` in public contracts. - [ ] Every oRPC procedure has typed `.input()` and `.output()`; no hand-written response types (all `z.infer`'d). -- [ ] Breaking changes to existing routes flagged (vs committed `docs/openapi.json`). +- [ ] Breaking changes to existing routes flagged (vs the module contract). ### Drizzle diff --git a/.rulesync/subagents/dev.md b/.rulesync/subagents/dev.md index 642101d3..69fa702e 100644 --- a/.rulesync/subagents/dev.md +++ b/.rulesync/subagents/dev.md @@ -26,7 +26,7 @@ Your prompt contains requirements + acceptance criteria. Build to those. If the 1. Read root `AGENTS.md` (decision tree, boundaries, forbidden patterns) and the sibling rules (`conventions`, `messaging-and-microservices`) plus the `docs/standards/` file matching what you are changing. Follow exactly. 2. Read the touched module's `AGENTS.md` and any related `docs/adr/`. -3. Inspect current state via `oss-dev` MCP: `list-modules`, `describe-module`, `list-routes` (collision check), `query-openapi`, `get-drizzle-schema`, `propose-table-change` (before any table), `schema-get`. +3. Inspect current state via `oss-dev` MCP: `list-modules`, `describe-module`, `list-routes` (collision check), `get-drizzle-schema`, `propose-table-change` (before any table), `schema-get`. 4. Pick the home via the decision tree. Use the scaffolders (`pnpm gen module|route|plugin|adapter|job-worker`) - don't hand-write skeletons. 5. Library API in doubt (Hono, oRPC, Drizzle, Zod, better-auth)? Check current docs via context7/web search - don't code from memory. diff --git a/.rulesync/subagents/docs.md b/.rulesync/subagents/docs.md index 65640b24..1a2c56ad 100644 --- a/.rulesync/subagents/docs.md +++ b/.rulesync/subagents/docs.md @@ -16,7 +16,6 @@ claudecode: - mcp__oss-dev__describe-module - mcp__oss-dev__list-routes - mcp__oss-dev__list-extension-points - - mcp__oss-dev__query-openapi - mcp__oss-dev__schema-get - mcp__oss-dev__docs-search - mcp__oss-dev__read-agents-md @@ -28,7 +27,7 @@ You keep the OSS docs honest. Read the code first, write the docs second - never - **Edit docs only.** Never touch `apps/`, `packages/`, `tools/`, `extensions.config.ts`, schemas, services, routers, plugins. - **Never edit generated mirrors** (`AGENTS.md`, `CLAUDE.md`, `.codex/config.toml`, `.github/copilot-instructions.md`, `.claude/`+`.github/` subagent/command files) - edit the `.rulesync/` source, then `pnpm gen:agents`. -- **Never touch generated artifacts** (`docs/openapi.json`, `docs/catalog.json`, drizzle migrations) - `pnpm regen` owns them. +- **Never touch generated artifacts** (`docs/catalog.json`, drizzle migrations) - `pnpm regen` owns them. - **No new docs unless asked**; if a fact has no home, raise it. **Don't invent** - if you can't verify a claim from code, omit it. ## Ground each claim in code @@ -37,7 +36,7 @@ You keep the OSS docs honest. Read the code first, write the docs second - never | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | Repo map / "what lives where" | `ls apps/ packages/` - every named dir must exist and match its `package.json`/`AGENTS.md` | | Module roster / domain claims | `mcp__oss-dev__list-modules` + `extensions.config.ts` | -| Route / adapter / extension claims | `list-routes`, `list-extension-points`, `query-openapi` | +| Route / adapter / extension claims | `list-routes`, `list-extension-points` | | Scaffolder flags + templates | `tools/gen/gen.ts`, `packages/core/generators/src/config.ts`, `tools/create/create-igaming-app.ts`, `ls tools/templates/` | | MCP tools listed in agent docs | the `server.tool(...)` registrations in `apps/mcp-server-dev/src/main.ts` | | ADR "is" claims | if Status is Accepted but the code disagrees, the ADR is stale - add a dated Update block | diff --git a/.rulesync/subagents/expert.md b/.rulesync/subagents/expert.md index a9312aed..9161766a 100644 --- a/.rulesync/subagents/expert.md +++ b/.rulesync/subagents/expert.md @@ -15,7 +15,7 @@ You are a senior iGaming product/domain expert who has shipped multiple real-mon ## Grounding (do this first) 1. Read root `AGENTS.md` (mission, pillars, decision tree) so requirements map onto how this platform is built. -2. Inventory what exists: `list-modules`, `list-routes`, `list-extension-points`, `query-openapi` via the `oss-dev` MCP; read active modules' `AGENTS.md`. Don't spec what already ships. +2. Inventory what exists: `list-modules`, `list-routes`, `list-extension-points` via the `oss-dev` MCP; read active modules' `AGENTS.md`. Don't spec what already ships. 3. Read `docs/catalog.json` for the adapter surface - which vendor ports exist, wired vs stubbed. ## How you work diff --git a/.rulesync/subagents/module-author.md b/.rulesync/subagents/module-author.md index d37522a1..877de7cf 100644 --- a/.rulesync/subagents/module-author.md +++ b/.rulesync/subagents/module-author.md @@ -24,7 +24,7 @@ You are an expert TypeScript / Hono / oRPC engineer implementing a module for th 1. Read root `AGENTS.md` + the `conventions` sibling rule + matching `docs/standards/` files, especially `database.md` and `module-structure.md`. Follow exactly. 2. Read an existing module (eg `packages/core/src/wallet/`) for the exact file shape. -3. Check current state via `oss-dev` MCP: `list-modules`, `describe-module`, `list-routes`, `query-openapi` (route collisions), `get-drizzle-schema`, `propose-table-change` (before ANY table). +3. Check current state via `oss-dev` MCP: `list-modules`, `describe-module`, `list-routes` (route collisions), `get-drizzle-schema`, `propose-table-change` (before ANY table). 4. Unanswered domain question in the brief? STOP and spawn `expert` before writing code. 5. Library API in doubt (Hono, oRPC, Drizzle, Zod)? Check current docs via context7/web search - don't code from memory. @@ -49,7 +49,7 @@ Creates the module as a standalone package with all required files and registers | `plugin.ts` | `definePlugin` - DI wiring only. | | `AGENTS.md` | ONLY what code can't say: invariants, rationale, gotchas, extension seams. No route/table/layout listings - they duplicate `contract/`/`schema/` and drift. | -Headless repo: build no UI. After filling in: `pnpm regen` (migration + OpenAPI + catalog), then `pnpm verify` and fix everything. +Headless repo: build no UI. After filling in: `pnpm regen` (migration + catalog), then `pnpm verify` and fix everything. ## Finish criteria diff --git a/.rulesync/subagents/operator.md b/.rulesync/subagents/operator.md index 2cbdd1e9..957f7d98 100644 --- a/.rulesync/subagents/operator.md +++ b/.rulesync/subagents/operator.md @@ -19,7 +19,7 @@ You are a technical founder standing up a new online igaming on top of `@openora ## Verify outside-in -- Don't trust docs - run things. `list-modules`/`list-routes`/`query-openapi` (MCP) for the declared surface; boot the probe app + `pnpm db:seed`, hit endpoints via curl to confirm they work, not just that they're declared. +- Don't trust docs - run things. `list-modules`/`list-routes` (MCP) for the declared surface; boot the probe app + `pnpm db:seed`, hit endpoints via curl to confirm they work, not just that they're declared. - Check each module's ports + `adapters/` to confirm vendor seams are real and overridable (KYC/PSP/notifications). `docs/catalog.json` marks each adapter wired vs stub - note stub-only ones. ## Readiness checklist (score Have / Partial / Missing, with the specific gap) diff --git a/.rulesync/subagents/plugin-author.md b/.rulesync/subagents/plugin-author.md index 8ac7b974..c3db5bd1 100644 --- a/.rulesync/subagents/plugin-author.md +++ b/.rulesync/subagents/plugin-author.md @@ -23,7 +23,7 @@ You build overlay plugins for the OSS igaming platform - extending behavior with ## Grounding (do this first) 1. Read root `AGENTS.md` (plugin system, boundaries, forbidden patterns). -2. `list-extension-points` (MCP) for available tokens and event types; `list-routes`/`query-openapi` to confirm new routes don't collide. +2. `list-extension-points` (MCP) for available tokens and event types; `list-routes` to confirm new routes don't collide. Library API in doubt (Hono, oRPC, Drizzle, Zod)? Check current docs via context7/web search - don't code from memory. 3. Scaffold - never write from scratch: ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28bfec89..4e77dede 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,14 +31,14 @@ Scripts are grouped by prefix: `check:*` reports, `fix:*` rewrites, `gen:*` emit | ------------------------- | ---------------------------------------------------------------------------- | | `pnpm dev` | turbo dev across docs, mcp | | `pnpm verify` | the full gate - every `check:*` plus `test:unit` + `test:tools`, in parallel | -| `pnpm regen` | tsconfig paths + openapi emit + drizzle generate + catalog | +| `pnpm regen` | tsconfig paths + drizzle generate + catalog | | `pnpm check:types` | `tsc --noEmit` across the workspace | | `pnpm check:lint` | oxlint (incl. the `oss-boundaries/*` plugin) | | `pnpm check:format` | oxfmt in check mode | | `pnpm check:boundaries` | dependency-cruiser whole-graph boundary + cycle gate | | `pnpm check:shape` | module layout conformance | | `pnpm check:deprecations` | fails on any use of a `@deprecated` symbol | -| `pnpm check:drift` | regenerates catalog/openapi and fails if the committed output is stale | +| `pnpm check:drift` | regenerates the catalog and fails if the committed output is stale | | `pnpm fix:lint` | oxlint `--fix` | | `pnpm fix:format` | oxfmt write + final-newline pass | | `pnpm test:unit` | vitest, no external services | @@ -151,7 +151,7 @@ generator and fails on an uncommitted diff. So if you touched schemas or routes, ports are doubled. - New functionality enters only via `definePlugin`. No auto-discovery, no magic. - ASCII only in code. Short dashes (-) only. -- Don't hand-edit generated files: drizzle migrations, `docs/openapi.json`, `docs/catalog.json`, +- Don't hand-edit generated files: drizzle migrations, `docs/catalog.json`, and the rulesync-generated agent files (`AGENTS.md`, `CLAUDE.md`, `.codex/config.toml`, `.github/copilot-instructions.md`, and the `.claude/`, `.github/` mirrors) - edit `.rulesync/` and run `pnpm gen:agents`. diff --git a/GEMINI.md b/GEMINI.md index f0cbbcaf..8cb08daa 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -31,8 +31,8 @@ Before acting on any non-trivial request - and before delegating to an agent - r ## Architecture pillars -1. **Zod-first contracts.** Every shape is a Zod schema; types are `z.infer`'d, never hand-written. Cross-cutting schemas in `packages/core/src/contracts/schemas/`; each module OWNS its route contract + req/res schemas + `z.infer`'d types in its `contract/` dir - the single source of wire truth, nothing else re-declares a wire shape. `composeContract` (`@openora/core/contracts`) owns only `health`; the composition root (`tools/gen/build-contract.ts` here, the consumer's entry when deployed) composes each enabled module's `/contract` slice into the one runtime contract the SDK links against. ADR-0021/0025. -2. **oRPC + Hono.** oRPC owns route definition + Zod validation + OpenAPI emit; its `OpenAPIHandler` mounts on a Hono server. DI is a functional `Container` (`@openora/core/server`) - typed-token factories, no decorators, no `reflect-metadata`. ADR-0009. +1. **Zod-first contracts.** Every shape is a Zod schema; types are `z.infer`'d, never hand-written. Cross-cutting schemas in `packages/core/src/contracts/schemas/`; each module OWNS its route contract + req/res schemas + `z.infer`'d types in its `contract/` dir - the single source of wire truth, nothing else re-declares a wire shape. `composeContract` (`@openora/core/contracts`) owns only `health`; the consumer composition root composes each enabled module's `/contract` slice into the one runtime contract the SDK links against. ADR-0021/0025. +2. **oRPC + Hono.** oRPC owns route definition + Zod validation; its `OpenAPIHandler` mounts on a Hono server with a live OpenAPI reference. DI is a functional `Container` (`@openora/core/server`) - typed-token factories, no decorators, no `reflect-metadata`. ADR-0009. 3. **Plugin host.** `definePlugin({ id, dependsOn, register })` is the only way new functionality enters. Everything (core modules included) loads through `extensions.config.ts`. 4. **Headless.** Backend modules + contracts + SDK surface only. UI lives in the consumer, which imports `@openora/core/react` (hooks, typed client, auth, realtime). No UI packages here. 5. **Explicit > magic.** No auto-discovery, no decorators. Everything greppable; every wiring point a typed call. @@ -62,13 +62,13 @@ packages/ docs/ adr/ # architecture decision records catalog.json # generated surface (routes/schemas/adapters/slots/events); read by @openora/mcp -tools/ # grouped: gen/ (gen.ts scaffolder, build-contract, gen-openapi, gen-catalog), lint/ (oxlint plugins, verify-module-shape), create/, db/ (seed, migrate-all), setup/ +tools/ # grouped: gen/ (gen.ts scaffolder, gen-catalog), lint/ (oxlint plugins, verify-module-shape), create/, db/ (seed, migrate-all), setup/ extensions.config.ts # the single registry of enabled plugins ``` ## Where does X go? (decision tree) -- **New business domain** (eg "tournaments") -> `pnpm gen module ` creates `@openora-addons/` under `packages/addons//` and registers it in `extensions.config.ts` (no `kind` = core, `kind: 'addon'` = gated). Core: add its `/contract` slice to `tools/gen/build-contract.ts`. Every module owns its `drizzle.config.ts` + migration history. ADR-0021/0024/0027. +- **New business domain** (eg "tournaments") -> `pnpm gen module ` creates `@openora-addons/` under `packages/addons//` and registers it in `extensions.config.ts` (no `kind` = core, `kind: 'addon'` = gated). Every module owns its `drizzle.config.ts` + migration history. ADR-0021/0024/0027. - **Extend/override an existing module** -> overlay plugin: `pnpm gen plugin ` -> `extensions//plugin.ts` (repo root here; the consumer's app when deployed). - **New HTTP route** -> the module's `router/index.ts` via `pnpm gen route `. Player routes resolve the caller from `x-user-id`; admin routes MUST be guarded (next). - **Admin-only route** -> `plugin.ts` resolves `AdminGuard` (`c.get(ADMIN_GUARD)`, seeded by `createApp`) and passes it into the router; `await adminGuard.assert(context)` is the handler's FIRST line. The single admin-enforcement point - never re-implement the role check. @@ -113,7 +113,7 @@ Lint-enforced cross-cutting bans in `conventions`: `any` outside tests, `interfa ``` pnpm setup:agent # first time: docker + db + mcp + summary pnpm dev # turbo dev (docs, mcp) -pnpm regen # openapi emit + drizzle-kit generate + catalog +pnpm regen # drizzle-kit generate + catalog pnpm seed # demo data (idempotent; admin@oss.dev / password123) pnpm boundaries # whole-graph boundary + cycle gate pnpm -F @openora/core vitest run # one test file/dir, eg src/iam/__tests__ @@ -152,8 +152,8 @@ For platform development (this repo); consumer agents ship in `tools/templates/c ## Working rules for agents -- Use the `oss-dev` MCP server (`.mcp.json`, pre-approved) for read-only inspection: `read-agents-md`, `list-modules`, `describe-module`, `list-routes`, `list-extension-points`, `query-openapi`, `get-drizzle-schema`, `propose-table-change`, `schema-get`, `docs-search`, `db-query-readonly`. Faster than grep, reflects current state. -- Before a route: `query-openapi`. Before a table: `propose-table-change`. After any change: `pnpm verify --filter `; fix failures before continuing. +- Use the `oss-dev` MCP server (`.mcp.json`, pre-approved) for read-only inspection: `read-agents-md`, `list-modules`, `describe-module`, `list-routes`, `list-extension-points`, `get-drizzle-schema`, `propose-table-change`, `schema-get`, `docs-search`, `db-query-readonly`. Faster than grep, reflects current state. +- Before a route: `list-routes`. Before a table: `propose-table-change`. After any change: `pnpm verify --filter `; fix failures before continuing. - Read the touched module's `AGENTS.md` before editing it; keep it updated when invariants or extension seams change. An `AGENTS.md` holds ONLY what code can't say: invariants, rationale, gotchas, extension seams, and where-to-look pointers - never route/table/layout/event listings (they duplicate `contract/`, `schema/`, `docs/catalog.json` and drift). Claude Code loads them via generated per-module `CLAUDE.md` stubs (`tools/gen/gen-claude-stubs.mjs`, gitignored). - Small PRs scoped to one module; cross-module changes need human approval. Never commit unless asked; never push without explicit per-action confirmation. - ASCII only in code; short dashes (-) only, never long dashes. diff --git a/README.md b/README.md index 67b9a5cf..4acc10e2 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The platform ships the backend surface (auth, wallet, player management, complia - **Headless by design** - backend modules, contracts, and an SDK consumption surface only. No UI ships here; you own the frontend. - **Plugin host** - `definePlugin({ id, dependsOn, register })` is the single way new functionality enters the system. Overlay a folder or install an npm package; same contract. -- **Zod-first contracts** - every shape is a Zod schema; types are inferred, never hand-written. Routes are oRPC on Hono with OpenAPI emitted at build time. +- **Zod-first contracts** - every shape is a Zod schema; types are inferred, never hand-written. Routes are oRPC on Hono with a live OpenAPI reference. - **Explicit wiring** - a small functional DI container with typed tokens. No decorators, no auto-discovery; everything is greppable. - **Swappable vendor seams** - PSP, KYC, aggregator, chat, realtime transport, job queue, and message broker are ports with default in-process drivers and adapter overrides. - **Regulatory audit log** - append-only, sha256 hash-chained. Every state-changing action leaves a trail. @@ -66,7 +66,7 @@ The `oss-dev` MCP dev server is wired in [`.mcp.json`](./.mcp.json) (stdio, laun claude mcp list # verify the oss-dev server is connected ``` -Then use `list-modules`, `list-routes`, `query-openapi`, `get-drizzle-schema`, `propose-table-change`, `docs-search`, `db-query-readonly`, and the `scaffold-*` tools. See [docs/agent-quickstart.md](./docs/agent-quickstart.md). +Then use `list-modules`, `list-routes`, `get-drizzle-schema`, `propose-table-change`, `docs-search`, `db-query-readonly`, and the `scaffold-*` tools. See [docs/agent-quickstart.md](./docs/agent-quickstart.md). ### 2. Manual run diff --git a/apps/mcp-server-dev/src/main.ts b/apps/mcp-server-dev/src/main.ts index d75f2da5..e4a98d8f 100644 --- a/apps/mcp-server-dev/src/main.ts +++ b/apps/mcp-server-dev/src/main.ts @@ -276,7 +276,7 @@ function buildPlaybook( case 'feature': return [ '## Where it goes', - 'A new business domain -> a new module under `packages/core/src///` (registered in extensions.config.ts, contract slice composed in tools/gen/build-contract.ts).', + 'A new business domain -> a new module under `packages/core/src///` (registered in extensions.config.ts).', '', '## Existing modules (avoid name collisions)', moduleList, @@ -287,7 +287,7 @@ function buildPlaybook( '3. Run `scaffold-module ` (MCP tool). In a consumer repo, an overlay is `pnpm gen plugin` instead.', '4. Fill the `// AGENT: implement here` regions: schema/ (pgTable), contract/ (Zod), service/, router/. Leave the wiring alone.', '5. Add routes with `scaffold-route `. Admin routes MUST `await adminGuard.assert(context)` first.', - '6. Run `regen` (drizzle migration + OpenAPI + SDK + catalog), then `run-verify`.', + '6. Run `regen` (drizzle migration + catalog), then `run-verify`.', '7. Hand the build to `module-author` (or `dev`) with the spec from step 1.', ].join('\n'); case 'adapter': @@ -324,7 +324,7 @@ function buildPlaybook( moduleList, '', '## Playbook', - '1. Call `query-openapi ` first to confirm the route does not already exist.', + '1. Call `list-routes` first to confirm the route does not already exist.', '2. Run `scaffold-route `.', '3. Player routes resolve the caller from the verified better-auth session (getUserId); admin routes MUST assert AdminGuard as the first line.', '4. Run `regen` then `run-verify`.', @@ -560,46 +560,6 @@ server.registerTool( }, ); -server.registerTool( - 'query-openapi', - { - description: 'Search the generated OpenAPI spec for paths or operations matching a keyword.', - inputSchema: { keyword: z.string() }, - }, - async ({ keyword }) => { - const specPath = repoPath('docs', 'openapi.json'); - if (!existsSync(specPath)) { - return { - content: [ - { type: 'text', text: 'OpenAPI spec not generated yet. Run `pnpm regen` first.' }, - ], - }; - } - const spec = JSON.parse(readFileSync(specPath, 'utf8')); - const kw = keyword.toLowerCase(); - const matches: string[] = []; - for (const [path, methods] of Object.entries(spec.paths ?? {})) { - if (path.toLowerCase().includes(kw)) { - const methodList = Object.keys(methods as object) - .join(', ') - .toUpperCase(); - matches.push(`${methodList} ${path}`); - } - } - return { - content: [ - { - type: 'text', - text: - matches.length > 0 - ? `Routes matching "${keyword}":\n${matches.join('\n')}` - : `No routes match "${keyword}"`, - }, - ], - }; - }, -); - server.registerTool( 'scaffold-module', { @@ -698,7 +658,7 @@ server.registerTool( 'regen', { description: - 'Run pnpm regen: drizzle-kit generate (migrations from pgTable schemas) + emit OpenAPI spec + regenerate the typed SDK.', + 'Run pnpm regen: drizzle-kit generate (migrations from pgTable schemas) + regenerate the catalog.', inputSchema: {}, }, async () => { diff --git a/docs/adr/0035-runtime-openapi-reference.md b/docs/adr/0035-runtime-openapi-reference.md new file mode 100644 index 00000000..bdb9966b --- /dev/null +++ b/docs/adr/0035-runtime-openapi-reference.md @@ -0,0 +1,51 @@ +# ADR-0035: Serve OpenAPI from the running API + +**Date**: 2026-08-04 +**Status**: Proposed +**Supersedes**: the static OpenAPI artifact decisions in ADR-0001 and ADR-0009, and the edition-aware static artifact assumption in ADR-0021. + +## Context + +The generated `docs/openapi.json` was a build-time snapshot of a composed contract. It +could become stale when a consumer changed its enabled plugins or contract composition, +and it required a separate generator and drift check to stay aligned with the API that +was actually serving requests. + +`createApp()` already has the final router after plugins have registered. oRPC's +`OpenAPIReferencePlugin` can expose both an API reference and its OpenAPI document from +that router, so a checked-in copy has no separate value. + +## Decision + +Serve the API reference from `createApp()` at `/docs` and the matching OpenAPI document +at `/openapi.json`. Both are derived from the router that the running application serves. + +Remove the static `docs/openapi.json` generation path and references to it. The catalog +remains a generated repository artifact for the MCP development surface; it is not an +API specification replacement. + +This ADR supersedes the static OpenAPI artifact portions of ADR-0001 and ADR-0009 and +the edition-aware static artifact assumption in ADR-0021. Those ADRs remain unchanged +as historical records. + +## Consequences + +**Positive:** + +- The published document matches the runtime router, including the consumer's enabled + plugins and composed contract. +- Consumers have no OpenAPI generation step to run or static API artifact to commit. + +**Negative / trade-offs:** + +- The reference is available only while an API instance is running; a release does not + include a checked-in OpenAPI snapshot. +- Consumers that need a versioned snapshot must retrieve `/openapi.json` from the + deployed API as part of their own release process. + +## References + +- `packages/core/src/server/runtime/create-app.ts` - runtime reference registration. +- ADR-0001 - original oRPC and OpenAPI decision. +- ADR-0009 - Hono runtime migration. +- ADR-0021 - contract composition and edition behavior. diff --git a/docs/agent-quickstart.md b/docs/agent-quickstart.md index 0d3de991..4c365f29 100644 --- a/docs/agent-quickstart.md +++ b/docs/agent-quickstart.md @@ -58,7 +58,7 @@ Edit the module's `contract/` dir (`packages/core/src///contract ``` schema-get name= -query-openapi keyword="" +list-routes ``` If a shared schema exists in `@openora/core/contracts`, re-export it instead of duplicating. diff --git a/docs/agentic-workflow.md b/docs/agentic-workflow.md index f924d5e1..75c0c8d0 100644 --- a/docs/agentic-workflow.md +++ b/docs/agentic-workflow.md @@ -73,7 +73,7 @@ When you want to drive implementation yourself instead of `/add-feature`: Fill in the `// AGENT: implement here` regions (or tell the agent to), then after any schema/contract edit: ``` -/regen # migrations + openapi + catalog +/regen # migrations + catalog /verify # typecheck + lint + unit tests, same as CI /verify --filter @openora/core # scoped to one package (faster) ``` @@ -99,7 +99,7 @@ Read-only inspection via the `oss-dev` MCP tools - any agent can answer these di ``` what modules exist? # list-modules -does a route for X already exist? # list-routes / query-openapi +does a route for X already exist? # list-routes what does the wallet schema look like? # get-drizzle-schema module=wallet would table "tournament_entry" collide? # propose-table-change ``` diff --git a/docs/architecture.md b/docs/architecture.md index e9e54d11..89e74921 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -22,10 +22,10 @@ flowchart TB subgraph contracts["@openora/core/contracts - the source of truth (isomorphic)"] zod["/contracts schemas
single Zod root"] orpc["/contracts composeContract
(health only; no aggregation)"] - openapi["docs/openapi.json
(emitted)"] + openapi["runtime OpenAPI reference
(Scalar/Swagger)"] client["typed client
(zero codegen)"] zod --> orpc - orpc --> openapi + hono --> openapi orpc --> client end @@ -33,7 +33,7 @@ flowchart TB cfg["extensions.config.ts
plugin registry"] host["plugin-host
definePlugin / ModuleRegistry"] container["Container
functional composition (tokens -> factories)"] - hono["Hono + oRPC OpenAPIHandler
validation, OpenAPI emit"] + hono["Hono + oRPC OpenAPIHandler
validation, live OpenAPI reference"] cfg --> host host --> container container --> hono @@ -88,13 +88,13 @@ Solid arrows are runtime/build dependencies; dashed arrows are **adapter seams** **Contracts** - **`@openora/core/contracts` schemas** - shared Zod schemas (cross-cutting primitives + identity) under `contracts/schemas/`. Per-module request/response schemas live in that module's `contract/` dir. Every type is `z.infer`'d, never hand-written. ADR-0004. -- **`composeContract`** (`@openora/core/contracts`) - the composition root composes each enabled module's contract slice into one runtime contract. From it the build emits `docs/openapi.json` and a fully typed client (no codegen step). ADR-0001. +- **`composeContract`** (`@openora/core/contracts`) - the composition root composes each enabled module's contract slice into one runtime contract and the typed client consumes that same contract (no codegen step). ADR-0001. **API runtime** - **extensions.config.ts** - the one list of enabled plugins (modules + overlays). The only place wiring is turned on. - **plugin-host** - `definePlugin({ id, dependsOn, register })` + `ModuleRegistry`. In `register(ctx)` a plugin binds providers (`ctx.provide(token, factory)`), mounts routers (`ctx.routers.add(namespace, (c) => router)`), subscribes to events, and registers MCP tools. Overlays add their own `pgTable` in their module's `schema/index.ts`. ADR-0002. -- **Hono + oRPC** - oRPC defines routes and validates I/O against the Zod contract; its `OpenAPIHandler` is mounted on a Hono server and emits `docs/openapi.json`. Dependency wiring is a small **functional composition `Container`** (`@openora/core/server`): typed-token factories, lazy + last-wins, no decorators. Downstream consumers call `createApp()` from `@openora/core/server` to boot their API entry. ADR-0009. +- **Hono + oRPC** - oRPC defines routes and validates I/O against the Zod contract; its `OpenAPIHandler` is mounted on a Hono server with a live API reference. Dependency wiring is a small **functional composition `Container`** (`@openora/core/server`): typed-token factories, lazy + last-wins, no decorators. Downstream consumers call `createApp()` from `@openora/core/server` to boot their API entry. ADR-0009. **Engine** (`@openora/core/server`) - the node runtime, all under one subpath: `db` (Drizzle client, drizzle-kit migrations, `DrizzleService`, the framework-free `@openora/core/server/orm` re-export), `auth` (better-auth + the shared `AdminGuard`), `kernel` (logger, typed `EventBus`, composition `Container`), `plugin-host` (the plugin loader), and `createApp()` - which is domain-agnostic (the consumer injects the PAM identity schema, ADR-0025/0026: single-tenant, no resolveTenant). @@ -113,7 +113,7 @@ Solid arrows are runtime/build dependencies; dashed arrows are **adapter seams** **AI dev surface** -- **mcp-server-dev** - a stdio MCP server (registered in `.mcp.json`, not a port) exposing read-only inspection (`list-modules`, `list-routes`, `query-openapi`, `get-drizzle-schema`, ...) and write tools that delegate to the scaffolder. +- **mcp-server-dev** - a stdio MCP server (registered in `.mcp.json`, not a port) exposing read-only inspection (`list-modules`, `list-routes`, `get-drizzle-schema`, ...) and write tools that delegate to the scaffolder. - **tools/gen/gen.ts** (-> `@openora/core/generators`) - deterministic code-mods behind the `/scaffold-*` slash commands (module, plugin, route). - **AGENTS.md** - per-package brief; the first thing an agent reads. diff --git a/docs/catalog.json b/docs/catalog.json index 66b58311..82c88589 100644 --- a/docs/catalog.json +++ b/docs/catalog.json @@ -1777,168 +1777,5 @@ "jobs", "mcp", "routers" - ], - "httpRoutes": [ - "DELETE /backoffice/chat/rooms/{id}", - "DELETE /chat/blocks/{blockedId}", - "DELETE /chat/ignores/{ignoredId}", - "DELETE /chat/messages/{id}", - "DELETE /chat/rooms/{roomId}", - "DELETE /cms/banners/{id}", - "DELETE /cms/pages/{id}", - "DELETE /compliance/limits/{id}", - "DELETE /iam/assignments", - "DELETE /iam/roles/{roleId}", - "DELETE /player/{playerId}/player-tag/{tagKey}", - "DELETE /players/{playerId}", - "DELETE /tag/{key}", - "DELETE /wallet/auto-withdrawal-rules/{userId}", - "GET /analytics/financial/ggr", - "GET /analytics/financial/summary", - "GET /analytics/funnel/conversion", - "GET /audit/export", - "GET /audit/logs", - "GET /backoffice/analytics/games", - "GET /backoffice/analytics/players", - "GET /backoffice/chat/rooms", - "GET /backoffice/stats", - "GET /backoffice/transactions", - "GET /backoffice/transactions/{id}", - "GET /backoffice/users", - "GET /backoffice/users/{userId}", - "GET /chat-command/commands", - "GET /chat-command/gift/{id}", - "GET /chat-command/mention-search", - "GET /chat-command/player-profile/{userId}", - "GET /chat-command/player-search", - "GET /chat/blocks", - "GET /chat/connection", - "GET /chat/global", - "GET /chat/ignores", - "GET /chat/online-count", - "GET /chat/rooms", - "GET /chat/rooms/{roomId}", - "GET /chat/rooms/{roomId}/members", - "GET /chat/rooms/{roomId}/messages", - "GET /chat/stream", - "GET /cms/banners", - "GET /cms/banners/{placement}", - "GET /cms/pages", - "GET /cms/pages/{slug}", - "GET /compliance/geo-check", - "GET /compliance/geo-rules", - "GET /compliance/kyc/stream", - "GET /compliance/limits", - "GET /compliance/players/{userId}/kyc", - "GET /compliance/players/{userId}/rg", - "GET /compliance/rg-flags", - "GET /gaming/games", - "GET /gaming/games/{id}", - "GET /gaming/rounds", - "GET /health", - "GET /iam/assignments", - "GET /iam/catalog", - "GET /iam/invitations", - "GET /iam/my-permissions", - "GET /iam/roles", - "GET /iam/roles/{roleId}", - "GET /identity/me", - "GET /identity/session/stream", - "GET /identity/sessions", - "GET /lobby/categories", - "GET /lobby/categories/{slug}", - "GET /lobby/featured", - "GET /lobby/search", - "GET /notifications", - "GET /player/{playerId}/assignable-tags", - "GET /player/{playerId}/note", - "GET /player/{playerId}/player-tag", - "GET /players", - "GET /players/by-user/{userId}", - "GET /players/stats/registrations", - "GET /players/stats/summary", - "GET /players/{playerId}", - "GET /profile", - "GET /tag-rule", - "GET /wallet/auto-withdrawal-rules/{userId}", - "GET /wallet/balance", - "GET /wallet/transactions", - "GET /wallet/transactions/{userId}", - "GET /wallet/withdrawals", - "PATCH /backoffice/chat/rooms/{id}", - "PATCH /backoffice/users/{userId}", - "PATCH /chat-command/admin/commands/{key}", - "PATCH /iam/roles/{roleId}", - "PATCH /identity/profile", - "PATCH /players/{playerId}", - "PATCH /profile", - "POST /backoffice/chat/rooms", - "POST /chat-command/execute", - "POST /chat-command/gift/{id}/claim", - "POST /chat/blocks", - "POST /chat/global", - "POST /chat/ignores", - "POST /chat/rooms/join", - "POST /chat/rooms/private", - "POST /chat/rooms/{roomId}/ban", - "POST /chat/rooms/{roomId}/kick", - "POST /chat/rooms/{roomId}/leave", - "POST /chat/rooms/{roomId}/messages", - "POST /cms/banners", - "POST /cms/pages", - "POST /compliance/geo-rules", - "POST /compliance/kyc", - "POST /compliance/kyc/bulk-approve", - "POST /compliance/kyc/webhook", - "POST /compliance/players/{userId}/cooling-off", - "POST /compliance/players/{userId}/cooling-off/lift", - "POST /compliance/players/{userId}/kyc/override", - "POST /compliance/players/{userId}/kyc/resubmit", - "POST /compliance/players/{userId}/self-exclusion", - "POST /compliance/players/{userId}/self-exclusion/lift", - "POST /gaming/rounds/start", - "POST /gaming/rounds/{roundId}/end", - "POST /iam/assignments", - "POST /iam/assignments/force-logout", - "POST /iam/effective-permissions", - "POST /iam/invitations", - "POST /iam/invitations/accept", - "POST /iam/roles", - "POST /identity/2fa/disable", - "POST /identity/2fa/enable", - "POST /identity/2fa/verify", - "POST /identity/email/change", - "POST /identity/email/verify", - "POST /identity/email/verify/send", - "POST /identity/login", - "POST /identity/logout", - "POST /identity/password/change", - "POST /identity/password/forgot", - "POST /identity/password/reset", - "POST /identity/password/verify-otp", - "POST /identity/phone-login/request", - "POST /identity/phone-login/verify", - "POST /identity/register", - "POST /identity/sessions/revoke", - "POST /identity/sessions/revoke-all", - "POST /identity/unlock", - "POST /notifications/read-all", - "POST /notifications/{id}/read", - "POST /player/{playerId}/note", - "POST /player/{playerId}/player-tag", - "POST /tag", - "POST /wallet/deposit", - "POST /wallet/deposits/address", - "POST /wallet/webhook", - "POST /wallet/withdraw", - "POST /wallet/withdrawals/{withdrawalId}/approve", - "POST /wallet/withdrawals/{withdrawalId}/reject", - "PUT /cms/banners/{id}", - "PUT /cms/pages/{id}", - "PUT /compliance/limits", - "PUT /compliance/players/{userId}/limits", - "PUT /iam/roles/{roleId}/permissions", - "PUT /tag-rule/{tagKey}", - "PUT /wallet/auto-withdrawal-rules/{userId}" ] } diff --git a/docs/downstream-consumer.md b/docs/downstream-consumer.md index 76e0f9a6..0736b741 100644 --- a/docs/downstream-consumer.md +++ b/docs/downstream-consumer.md @@ -55,19 +55,19 @@ import { extensions } from './extensions.config.js'; // their own plugin list // Compose only the modules you enable (composeContract adds `health` itself). const contract = composeContract({ identity: identityContract, wallet: walletContract }); -const { listen, emitOpenApiSpec } = await createApp({ +const { listen } = await createApp({ plugins: extensions, contract, authSchema: { user, session, account, verification, twoFactor }, port: 3001, cors: { origins: ['https://my-igaming.example'] }, - openapi: { info: { title: 'my-igaming API', version: '1.0.0' } }, }); await listen(); -await emitOpenApiSpec(); ``` +`createApp` serves a live API reference at `/docs` and its matching OpenAPI document at `/openapi.json`. + Downstream consumers create their own thin entrypoint that calls `createApp` and bring their own `extensions.config.ts`. See `tools/templates/consumer/apps/api/src/main.ts` for the reference. diff --git a/docs/glossary.md b/docs/glossary.md index dd7d5659..a2e48666 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -21,7 +21,7 @@ Shared vocabulary for this repo: the **roles** (who's who), the **platform/archi | **Module** | A business domain packaged as an independently loadable unit (auth, wallet, gaming...). Never imports another module. | `packages/core/src///` | | **Plugin** / **extension** / **overlay** | A drop-in unit that adds or overrides behavior via `definePlugin({ id, register })`. The only way new functionality enters the system. | `extensions/*`, consumer plugins | | **Adapter** | The vendor-agnostic interface a module depends on (e.g. `KycAdapter`, `PaymentAdapter`) plus its per-vendor implementations. The swap seam: a module declares the interface + DI token in `@openora/core/contracts`; an operator binds a concrete impl. | `@openora/core/contracts` + `//adapters//` | -| **Contract** | The composed oRPC router. Drives request validation, the typed client, and the emitted OpenAPI spec. | `@openora/core/contracts` | +| **Contract** | The composed oRPC router. Drives request validation, the typed client, and the live OpenAPI reference. | `@openora/core/contracts` | | **Domain schema** | A Zod schema - the single source of truth for a shape. Types are `z.infer`'d, never hand-written. | `@openora/core/contracts`, module `schemas/` | | **UI plugin** | A client-side extension that customizes the look/feel. Lives in the consumer frontend, not in this repo. The platform is headless backend only. | ADR-0013 (superseded) | | **Slot** | A typed injection point in a UI surface that a plugin can fill. Owned by the consumer frontend. | ADR-0013 (superseded) | diff --git a/docs/mcp-setup.md b/docs/mcp-setup.md index 9752aba9..277cad51 100644 --- a/docs/mcp-setup.md +++ b/docs/mcp-setup.md @@ -63,7 +63,6 @@ The server uses stdio transport (no port). Add this to your editor's MCP config: | `describe-module` | Full module surface: AGENTS.md + tables + schemas + routes in one call | | `list-routes` | oRPC route namespaces (filter by module name) | | `list-extension-points` | UI slots, exported events, adapter port interfaces | -| `query-openapi` | Search the generated OpenAPI spec by keyword | | `get-drizzle-schema` | pgTable definitions across all modules (filter by module) | | `propose-table-change` | Collision-check a new table name before adding it | | `schema-get` | Find a Zod schema by name with its file location | @@ -82,7 +81,7 @@ The server uses stdio transport (no port). Add this to your editor's MCP config: | `scaffold-plugin` | Creates a new overlay extension skeleton | | `scaffold-route` | Adds an oRPC route stub to a module | | `scaffold-app` | Bootstraps a new downstream consumer repo (api + web + backoffice) linked to this checkout | -| `regen` | Runs drizzle-kit generate + OpenAPI emit + catalog regeneration | +| `regen` | Runs drizzle-kit generate + catalog regeneration | | `run-verify` | Runs pnpm verify (typecheck + lint + tests) | ## The consumer-facing server (`@openora/mcp`) diff --git a/docs/standards/database.md b/docs/standards/database.md index c81758ab..65f515b9 100644 --- a/docs/standards/database.md +++ b/docs/standards/database.md @@ -76,6 +76,6 @@ await db.transaction(async (t) => { ## Migrations -- Never hand-edit generated migrations, `docs/openapi.json`, or `docs/catalog.json`. Change the `pgTable`, then run `pnpm regen`. +- Never hand-edit generated migrations or `docs/catalog.json`. Change the `pgTable`, then run `pnpm regen`. - Every module owns its own `drizzle/migrations/` + `__drizzle_migrations_` tracking table, co-located with its schema. One shared database, one journal per module. - A Postgres extension an index needs (for example `pg_trgm`) goes in the module `migrate()` `extensions` option, never hand-edited into a regenerated migration. diff --git a/docs/system-design.md b/docs/system-design.md index bff4b199..9b05c063 100644 --- a/docs/system-design.md +++ b/docs/system-design.md @@ -55,14 +55,14 @@ flowchart TB subgraph SDK["@openora/core/react · headless SDK (browser)"] HOOKS["data hooks · auth · transport"] TC["typed oRPC client
(zero codegen)"] - OAPI["docs/openapi.json (emitted)"] + OAPI["runtime OpenAPI reference"] end %% ============ ENGINE (@openora/core/server) ============ subgraph RT["@openora/core/server · createApp() (node engine)"] PH["plugin-host
definePlugin · ModuleRegistry · applyServiceManifest"] DI["Container
tokens to factories (last-wins overlay)"] - HONO["Hono + oRPC OpenAPIHandler
validation · OpenAPI emit"] + HONO["Hono + oRPC OpenAPIHandler
validation · live OpenAPI reference"] GATE["SERVICE_MANIFEST module filter"] end diff --git a/extensions.config.ts b/extensions.config.ts index 38daa98a..13cfd3e8 100644 --- a/extensions.config.ts +++ b/extensions.config.ts @@ -4,10 +4,10 @@ // // Every module lives under packages/core/src/// (compiled to // dist///plugin.js) and owns its route contract slice (its /contract -// dir), composed in tools/gen/build-contract.ts. See ADR-0025. +// dir). Downstream consumers compose the contract slices they enable. See ADR-0025. export const extensions = [ - // --- MODULES (always loaded; contracts composed in tools/gen/build-contract.ts) --- + // --- MODULES (always loaded) --- { id: 'audit', path: './packages/core/dist/audit/plugin.js' }, { id: 'iam', path: './packages/core/dist/iam/plugin.js' }, // Platform - shared substrate used by both surfaces diff --git a/package.json b/package.json index 70617b0a..6ab316fd 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "check:boundaries": "depcruise apps packages --config .dependency-cruiser.cjs", "check:shape": "tsx tools/lint/verify-module-shape.ts", "check:deprecations": "depretec --fail-on-found --exclude packages/core/src/pam/identity/adapters/identity-reader.service.ts", - "check:drift": "tsx tools/gen/sync-tsconfig-paths.ts --check && tsx tools/gen/gen-openapi.ts && tsx tools/gen/gen-catalog.ts && git diff --exit-code -- docs/catalog.json", + "check:drift": "tsx tools/gen/sync-tsconfig-paths.ts --check && tsx tools/gen/gen-catalog.ts && git diff --exit-code -- docs/catalog.json", "fix:lint": "oxlint . --fix", "fix:format": "oxfmt && node tools/lint/ensure-final-newline.mjs", "test:unit": "turbo run test:unit", @@ -29,12 +29,11 @@ "test:tools": "node --test \"tools/__tests__/*.test.mjs\"", "test:scaffold": "tsx tools/gen/eval-scaffold.ts", "gen": "tsx tools/gen/gen.ts", - "gen:openapi": "tsx tools/gen/gen-openapi.ts", "gen:catalog": "tsx tools/gen/gen-catalog.ts", "gen:drizzle": "pnpm -F @openora/core gen:drizzle", "gen:tsconfig": "tsx tools/gen/sync-tsconfig-paths.ts", "gen:agents": "rulesync generate && node tools/gen/gen-claude-stubs.mjs", - "regen": "pnpm run gen:tsconfig && pnpm run gen:openapi && pnpm run gen:drizzle && pnpm run gen:catalog", + "regen": "pnpm run gen:tsconfig && pnpm run gen:drizzle && pnpm run gen:catalog", "db:migrate": "openora-migrate", "db:seed": "tsx tools/db/seed.ts", "db:setup:test": "tsx tools/db/setup-test-db.ts", diff --git a/packages/core/generators/src/config.ts b/packages/core/generators/src/config.ts index 6475e112..bf318ad9 100644 --- a/packages/core/generators/src/config.ts +++ b/packages/core/generators/src/config.ts @@ -212,31 +212,6 @@ function wireCoreExports(domain: string, name: string): string { : 'no new @openora/core exports needed'; } -/** - * Registers the module's contract slice in the composition root so its routes - * reach the emitted OpenAPI spec and the typed client. - */ -function wireBuildContract(domain: string, name: string): string { - const file = join(root(), 'tools', 'gen', 'build-contract.ts'); - if (!existsSync(file)) { - return 'no tools/gen/build-contract.ts (skipped)'; - } - const camel = toCamel(name); - const contractName = `${camel}Contract`; - let src = readFileSync(file, 'utf8'); - if (src.includes(contractName)) { - return `build-contract.ts already composes '${camel}'`; - } - const importLine = `import { ${contractName} } from '@openora/core/${domain}/contracts/${name}';`; - src = src.replace(/(\n\n\/\/ oxlint-disable-next-line)/, `\n${importLine}$1`); - src = src.replace( - /(const SLICES: Record = \{)/, - `$1\n ${camel}: ${contractName},`, - ); - writeFileSync(file, src); - return `composed '${camel}' in tools/gen/build-contract.ts`; -} - function appendRoute( domain: string, moduleName: string, @@ -317,7 +292,6 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void { file('AGENTS.md', 'module/agents.hbs'), () => wireDomainBarrels(domain, name), () => wireCoreExports(domain, name), - () => wireBuildContract(domain, name), () => registerExtension(name, `./packages/core/dist/${domain}/${name}/plugin.js`), () => `next: pnpm gen:drizzle (this module's migration history) && pnpm regen && pnpm verify`, diff --git a/packages/core/generators/src/templates/contract.hbs b/packages/core/generators/src/templates/contract.hbs index 2303ac6f..ea7edb16 100644 --- a/packages/core/generators/src/templates/contract.hbs +++ b/packages/core/generators/src/templates/contract.hbs @@ -3,7 +3,7 @@ import * as z from 'zod'; import { TimestampSchema, UuidSchema } from '@openora/core/contracts'; // Canonical request/response shapes for the {{pascalCase name}} module. This is the -// single source of truth - the router validates against it, OpenAPI + the typed +// single source of truth - the router validates against it, live OpenAPI + the typed // client are emitted from it. Derive related shapes with .pick()/.omit()/.extend() // rather than re-typing fields. Promote anything shared across domains to // @openora/core/contracts. This dir is isomorphic: Zod + @openora/core/contracts only. diff --git a/packages/core/src/server/runtime/__tests__/create-app.test.ts b/packages/core/src/server/runtime/__tests__/create-app.test.ts index cf4c7c64..28a61ddb 100644 --- a/packages/core/src/server/runtime/__tests__/create-app.test.ts +++ b/packages/core/src/server/runtime/__tests__/create-app.test.ts @@ -11,9 +11,9 @@ const DUMMY_DATABASE_URL = 'postgres://test:test@127.0.0.1:1/create_app_test'; describe('createApp - distributed-only durable seams (ADR-0030)', () => { it('throws a clear, actionable error when no durable seam is bound', async () => { - await expect( - createApp({ plugins: [], databaseUrl: DUMMY_DATABASE_URL, openapi: { enabled: false } }), - ).rejects.toThrow(/MESSAGE_BROKER.*JOB_QUEUE.*CACHE.*RATE_LIMITER/s); + await expect(createApp({ plugins: [], databaseUrl: DUMMY_DATABASE_URL })).rejects.toThrow( + /MESSAGE_BROKER.*JOB_QUEUE.*CACHE.*RATE_LIMITER/s, + ); }); it('boots and serves once REDIS_URL auto-binds all four seams', async () => { @@ -23,7 +23,6 @@ describe('createApp - distributed-only durable seams (ADR-0030)', () => { const created = await createApp({ plugins: [], databaseUrl: DUMMY_DATABASE_URL, - openapi: { enabled: false }, }); for (const token of [MESSAGE_BROKER, JOB_QUEUE, CACHE, RATE_LIMITER]) { @@ -33,6 +32,14 @@ describe('createApp - distributed-only durable seams (ADR-0030)', () => { expect(res.status).toBe(200); expect(await res.json()).toMatchObject({ status: 'ok' }); + const spec = await created.app.request('/openapi.json'); + expect(spec.status).toBe(200); + expect(await spec.json()).toMatchObject({ openapi: expect.any(String) }); + + const docs = await created.app.request('/docs'); + expect(docs.status).toBe(200); + expect(await docs.text()).toContain('API Reference'); + await created.close(); } finally { if (saved === undefined) { @@ -70,8 +77,8 @@ describe('createApp - service name for the Redis Streams consumer group', () => process.env['SERVICE_MANIFEST'] = 'wallet,iam'; delete process.env['SERVICE_NAME']; - await expect( - createApp({ plugins: [], databaseUrl: DUMMY_DATABASE_URL, openapi: { enabled: false } }), - ).rejects.toThrow(/SERVICE_MANIFEST is set but SERVICE_NAME is not/); + await expect(createApp({ plugins: [], databaseUrl: DUMMY_DATABASE_URL })).rejects.toThrow( + /SERVICE_MANIFEST is set but SERVICE_NAME is not/, + ); }); }); diff --git a/packages/core/src/server/runtime/create-app.ts b/packages/core/src/server/runtime/create-app.ts index 9e25933a..760411f8 100644 --- a/packages/core/src/server/runtime/create-app.ts +++ b/packages/core/src/server/runtime/create-app.ts @@ -1,14 +1,13 @@ import { OpenAPIHandler } from '@orpc/openapi/fetch'; +import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins'; import { implement, onError, ORPCError, type AnyRouter } from '@orpc/server'; import { ResponseHeadersPlugin } from '@orpc/server/plugins'; -import type { ContractRouter } from '@orpc/contract'; +import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'; import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { etag } from 'hono/etag'; import { HTTPException } from 'hono/http-exception'; import { serve, type ServerType } from '@hono/node-server'; -import { resolve } from 'node:path'; -import { generateOpenApiSpec } from './openapi.js'; import { Container, BullMqJobQueue, @@ -33,7 +32,6 @@ import { RATE_LIMITER, CACHE, ERROR_TRACKING, - composeContract, healthContract, IGAMING_CONFIG, type IgamingConfig, @@ -104,17 +102,6 @@ export type CreateAppConfig = { databaseUrl?: string; - // The shape is genuinely unknown at this factory boundary (an external oRPC generic) - - // the documented `any` exception for an external library's untyped surface. - // oxlint-disable-next-line typescript/no-explicit-any - contract?: ContractRouter; - - openapi?: { - enabled?: boolean; - info?: { title?: string; version?: string }; - outputPath?: string; - }; - igaming?: IgamingConfig; // GET-only, path-prefix-matched Cache-Control on PUBLIC_HTTP_CACHE_PATHS (or a @@ -131,7 +118,6 @@ export type CreatedApp = { container: Container; port: number; listen(): Promise; - emitOpenApiSpec(): Promise; close(): Promise; }; @@ -335,7 +321,14 @@ export async function createApp(config: CreateAppConfig): Promise { } const handler = new OpenAPIHandler(router, { - plugins: [new ResponseHeadersPlugin()], + plugins: [ + new OpenAPIReferencePlugin({ + docsPath: '/docs', + specPath: '/openapi.json', + schemaConverters: [new ZodToJsonSchemaConverter()], + }), + new ResponseHeadersPlugin(), + ], interceptors: [ onError((error) => { // oRPC wraps a thrown native error (a DB failure, a bug) into an @@ -460,17 +453,6 @@ export async function createApp(config: CreateAppConfig): Promise { server = serve({ fetch: app.fetch, port }); process.stdout.write(`API listening on :${port}\n`); }, - async emitOpenApiSpec() { - if (config.openapi?.enabled === false) { - return null; - } - const outPath = await generateOpenApiSpec(config.contract ?? composeContract({}), { - info: config.openapi?.info, - outputPath: config.openapi?.outputPath ?? resolve(process.cwd(), 'docs/openapi.json'), - }); - process.stdout.write(`OpenAPI spec written to ${outPath}\n`); - return outPath; - }, async close() { server?.close(); await container.dispose(); diff --git a/packages/core/src/server/runtime/index.ts b/packages/core/src/server/runtime/index.ts index 2635956b..c3d5d65c 100644 --- a/packages/core/src/server/runtime/index.ts +++ b/packages/core/src/server/runtime/index.ts @@ -3,9 +3,6 @@ export type { CreateAppConfig, CreatedApp } from './create-app.js'; export { CORE_TOKEN_CATALOG } from './core-token-catalog.js'; export type { CoreTokenCatalog } from './core-token-catalog.js'; -export { generateOpenApiSpec } from './openapi.js'; -export type { GenerateOpenApiSpecOptions } from './openapi.js'; - export { definePlugin, type Plugin, diff --git a/packages/core/src/server/runtime/openapi.ts b/packages/core/src/server/runtime/openapi.ts deleted file mode 100644 index 1e5f2cba..00000000 --- a/packages/core/src/server/runtime/openapi.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { OpenAPIGenerator } from '@orpc/openapi'; -import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'; -import type { ContractRouter } from '@orpc/contract'; -import { writeFile, mkdir } from 'node:fs/promises'; -import { dirname } from 'node:path'; - -export type GenerateOpenApiSpecOptions = { - info?: { title?: string; version?: string }; - outputPath: string; -}; - -/** - * Generate the OpenAPI spec from a contract and write it to disk. - * Pure codegen - no server boot, no DB. Used by `createApp().emitOpenApiSpec()` - * and by the standalone `codegen` script (`pnpm regen`). - */ -export async function generateOpenApiSpec( - // oxlint-disable-next-line typescript/no-explicit-any - contract: ContractRouter, - options: GenerateOpenApiSpecOptions, -): Promise { - const generator = new OpenAPIGenerator({ - schemaConverters: [new ZodToJsonSchemaConverter()], - }); - const spec = await generator.generate(contract, { - info: { - title: options.info?.title ?? 'OSS Igaming API', - version: options.info?.version ?? '0.0.1', - }, - }); - await mkdir(dirname(options.outputPath), { recursive: true }); - await writeFile(options.outputPath, JSON.stringify(spec, null, 2) + '\n', 'utf8'); - return options.outputPath; -} diff --git a/packages/mcp/docs/catalog.json b/packages/mcp/docs/catalog.json index fcc667d2..d367268e 100644 --- a/packages/mcp/docs/catalog.json +++ b/packages/mcp/docs/catalog.json @@ -481,6 +481,5 @@ "providers", "routers", "slots" - ], - "httpRoutes": [] + ] } diff --git a/packages/mcp/src/main.ts b/packages/mcp/src/main.ts index edb6dad9..2f65e2cb 100644 --- a/packages/mcp/src/main.ts +++ b/packages/mcp/src/main.ts @@ -46,7 +46,6 @@ type Catalog = { schemas: CatalogSchema[]; config: CatalogConfig; pluginContract: string[]; - httpRoutes: string[]; }; const NOT_FOUND_MESSAGE = @@ -121,8 +120,7 @@ server.registerTool( const lines: string[] = ['=== OSS igaming platform catalog ===']; lines.push( `modules: ${c.modules.length} adapters: ${c.adapters.length} events: ${c.events.length} ` + - `schemas: ${c.schemas.length} ` + - `httpRoutes: ${c.httpRoutes.length}`, + `schemas: ${c.schemas.length}`, ); lines.push('\n--- Adapter seams (implement an interface, bind to the token) ---'); @@ -172,7 +170,7 @@ server.registerTool( 'list-routes', { description: - 'List oRPC route namespaces exposed by the platform modules. Pass `module` to scope to a single module. Includes top-level httpRoutes when present.', + 'List oRPC route namespaces exposed by the platform modules. Pass `module` to scope to a single module.', inputSchema: { module: z.string().optional().describe('Module id to filter by (e.g. "wallet")'), }, @@ -200,9 +198,6 @@ server.registerTool( lines.push(`${m.id}:\n${m.routes.map((r) => ` ${r}`).join('\n')}`); } } - if (!mod && c.httpRoutes.length > 0) { - lines.push(`httpRoutes:\n${c.httpRoutes.map((r) => ` ${r}`).join('\n')}`); - } const text = lines.join('\n\n') || 'No routes defined in the catalog yet.'; return { content: [{ type: 'text' as const, text }] }; }, diff --git a/packages/testing/src/app.ts b/packages/testing/src/app.ts index ca622ef1..a2a09776 100644 --- a/packages/testing/src/app.ts +++ b/packages/testing/src/app.ts @@ -32,17 +32,17 @@ export type TestApp = { close(): Promise; }; -export type BootTestAppConfig = Pick & { +export type BootTestAppConfig = Pick & { databaseUrl: string; }; /** * Boot the full Hono + oRPC app in-process against a test database. No network * listener is opened - exercise routes with `app.request()` (the canonical Hono - * test approach). OpenAPI emission is disabled; CORS is left at the default. + * test approach). CORS is left at the default. * - * Pass the same `plugins` + `contract` the real entrypoint uses (in OSS that is - * `loadExtensions()` + `@openora/core/contracts`; a consumer passes its own). + * Pass the same plugins the real entrypoint uses (in OSS that is `loadExtensions()`; + * a consumer passes its own). * * The four durable seams run on the SAME drivers production uses - Redis Streams, * BullMQ, Redis cache and Redis rate limiter - so event fan-out, job retries and @@ -64,11 +64,9 @@ export async function bootTestApp(config: BootTestAppConfig): Promise { const created = await createApp({ plugins: config.plugins, - ...(config.contract ? { contract: config.contract } : {}), ...(config.igaming ? { igaming: config.igaming } : {}), databaseUrl: config.databaseUrl, authSchema: { user, session, account, verification, twoFactor }, - openapi: { enabled: false }, configure(container: Container) { const redis = createRedisClient(redisDatabase.url); container.onDispose(() => redis.close()); diff --git a/tools/create/create-service.ts b/tools/create/create-service.ts index d4052eb1..9a23129c 100644 --- a/tools/create/create-service.ts +++ b/tools/create/create-service.ts @@ -103,9 +103,7 @@ process.env['SERVICE_MANIFEST'] ??= '${manifest.join(',')}'; async function bootstrap() { const plugins = await loadExtensions(); - // Routes come from the loaded plugins. To emit an OpenAPI spec for this - // service, compose its slices with composeContract({ ... }) from - // @openora/core/contracts and pass it as \`contract\`. See tools/gen/build-contract.ts. + // Routes come from the loaded plugins; createApp exposes runtime API docs. const { listen } = await createApp({ plugins }); await listen(); } diff --git a/tools/gen/build-contract.ts b/tools/gen/build-contract.ts deleted file mode 100644 index b9f74419..00000000 --- a/tools/gen/build-contract.ts +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env node -/** - * Assembles the full runtime contract from all domain slices. - * Used by gen-openapi.ts and as a reference for consumer composition roots. - * Each domain owns its own /contracts slice; this file is the only place that - * stitches them together. See ADR-0021/0025. - */ -import type { ContractRouter } from '@orpc/contract'; -import { composeContract } from '@openora/core/contracts'; -import { identityContract } from '@openora/core/pam/contracts/identity'; -import { complianceContract } from '@openora/core/compliance/contracts'; -import { profileContract } from '@openora/core/pam/contracts/profile'; -import { cmsContract } from '@openora/core/cms/contracts'; -import { notificationsContract } from '@openora/core/engagement/contracts/notifications'; -import { chatContract } from '@openora/core/engagement/contracts/chat'; -import { chatCommandsContract } from '@openora/core/engagement/contracts/chat-commands'; -import { walletContract } from '@openora/core/wallet/contract'; -import { gamingContract } from '@openora/core/casino/contracts/gaming'; -import { lobbyContract } from '@openora/core/casino/contracts/lobby'; -import { backofficeContract } from '@openora/core/admin-console/contract'; -import { analyticsContract } from '@openora/core/analytics/contract'; -import { iamContract } from '@openora/core/iam/contract'; -import { auditContract } from '@openora/core/audit/contract'; -import { playerContract } from '@openora/core/pam/contracts/player'; -import { tagContract } from '@openora/core/pam/contracts/tag'; -import { playerNoteContract } from '@openora/core/pam/contracts/player-note'; - -// oxlint-disable-next-line typescript/no-explicit-any -- root contract is an external oRPC generic -type AnyContract = ContractRouter; - -// Order mirrors the historical aggregate so the emitted OpenAPI paths stay stable. -const SLICES: Record = { - identity: identityContract, - cms: cmsContract, - compliance: complianceContract, - notifications: notificationsContract, - wallet: walletContract, - gaming: gamingContract, - chat: chatContract, - 'chat-commands': chatCommandsContract, - lobby: lobbyContract, - backoffice: backofficeContract, - analytics: analyticsContract, - profile: profileContract, - iam: iamContract, - audit: auditContract, - tag: tagContract, - 'player-note': playerNoteContract, - player: playerContract, -}; - -/** - * Compose the full runtime contract from every module slice. - */ -export function buildContract(): AnyContract { - return composeContract(SLICES); -} diff --git a/tools/gen/gen-catalog.ts b/tools/gen/gen-catalog.ts index c2788813..33742d4d 100644 --- a/tools/gen/gen-catalog.ts +++ b/tools/gen/gen-catalog.ts @@ -4,8 +4,8 @@ * consumer's AI agent reads INSTEAD of grepping node_modules. Emits: * docs/catalog.json - structured, consumed at runtime by the published @openora/mcp * server (a consumer's node_modules has no platform source). - * Human/agent-readable access is the MCP dev server (describe-module, list-routes, - * query-openapi) and each module's AGENTS.md - no monolithic markdown dump. + * Human/agent-readable access is the MCP dev server (describe-module, list-routes) + * and each module's AGENTS.md - no monolithic markdown dump. * * It captures: modules (+ tables + routes), adapter seams (+ wired-vs-stub * status), domain events, UI slots, Zod schema index, the igaming-config shape, @@ -246,26 +246,6 @@ function collectPluginSurface(): string[] { return [...body.matchAll(/^\s{2}(\w+):/gm)].map((m) => m[1] ?? '').sort(); } -function collectOpenApiRoutes(): string[] { - const spec = read(join(repoRoot, 'docs', 'openapi.json')); - if (!spec) { - return []; - } - try { - const json = JSON.parse(spec); - const paths = Object.keys(json.paths ?? {}); - const out: string[] = []; - for (const p of paths) { - for (const method of Object.keys(json.paths[p])) { - out.push(`${method.toUpperCase()} ${p}`); - } - } - return out.sort(); - } catch { - return []; - } -} - const catalog = { modules: collectModules(), adapters: collectAdapters(), @@ -278,7 +258,6 @@ const catalog = { fields: collectConfigFields(), }, pluginContract: collectPluginSurface(), - httpRoutes: collectOpenApiRoutes(), }; const docsDir = join(repoRoot, 'docs'); diff --git a/tools/gen/gen-openapi.ts b/tools/gen/gen-openapi.ts deleted file mode 100644 index b8410f31..00000000 --- a/tools/gen/gen-openapi.ts +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env node -/** - * Emits docs/openapi.json from the assembled contract - no server boot, no DB. - * Runs via `pnpm regen` and in CI via `pnpm check:drift`. - */ -import { generateOpenApiSpec } from '@openora/core/server'; -import { buildContract } from './build-contract.js'; -import { resolve, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const here = dirname(fileURLToPath(import.meta.url)); -const outputPath = resolve(here, '../../docs/openapi.json'); - -async function main() { - const outPath = await generateOpenApiSpec(buildContract(), { - info: { title: 'OSS Igaming API', version: '0.0.1' }, - outputPath, - }); - process.stdout.write(`OpenAPI spec written to ${outPath}\n`); -} - -main(); diff --git a/tools/setup/setup-agent.ts b/tools/setup/setup-agent.ts index 781284b0..06695716 100644 --- a/tools/setup/setup-agent.ts +++ b/tools/setup/setup-agent.ts @@ -50,7 +50,7 @@ async function main() { console.log('\n--- Dependencies: already installed ---'); } - console.log('\n--- Generating OpenAPI spec (pnpm regen) ---'); + console.log('\n--- Generating derived artifacts (pnpm regen) ---'); run('pnpm regen'); console.log('\n--- Starting dev infra (docker compose up -d) ---'); diff --git a/tools/templates/consumer/README.md.tpl b/tools/templates/consumer/README.md.tpl index f854247f..7a2761bb 100644 --- a/tools/templates/consumer/README.md.tpl +++ b/tools/templates/consumer/README.md.tpl @@ -28,7 +28,7 @@ newest release; pin an exact version if you need reproducible installs. ```bash pnpm install # pulls @openora/* from npm pnpm setup:mcp # trust the MCP server + install the /start onboarding flow -pnpm regen # regenerate OpenAPI + catalog + Drizzle client (after schema changes) +pnpm regen # regenerate catalog + Drizzle client (after schema changes) cp .env.example .env # set DATABASE_URL + AUTH_SECRET pnpm db:migrate # apply the OSS schema to your database pnpm dev # api :3001 diff --git a/tools/templates/consumer/__dot__rulesync/commands/scaffold-route.md b/tools/templates/consumer/__dot__rulesync/commands/scaffold-route.md index 8c8ce1d0..238e4b67 100644 --- a/tools/templates/consumer/__dot__rulesync/commands/scaffold-route.md +++ b/tools/templates/consumer/__dot__rulesync/commands/scaffold-route.md @@ -5,7 +5,7 @@ description: 'Add an oRPC route stub to an overlay or local add-on. Args: `. First confirm the route does not already exist - `list-routes` (oss MCP) for the namespace, or -`query-openapi` with the path. +`list-routes` to confirm the route does not already exist. Run `pnpm gen route ` in the repo root. The generator adds both a contract procedure and a matching router handler - no inline Zod in the router. diff --git a/tools/templates/consumer/apps/api/src/main.ts.tpl b/tools/templates/consumer/apps/api/src/main.ts.tpl index ce44d72f..7bd7129f 100644 --- a/tools/templates/consumer/apps/api/src/main.ts.tpl +++ b/tools/templates/consumer/apps/api/src/main.ts.tpl @@ -67,18 +67,16 @@ process.env['EXTENSIONS_CONFIG'] ??= resolve( ); async function bootstrap() { - const { listen, emitOpenApiSpec } = await createApp({ + const { listen } = await createApp({ plugins: await loadExtensions(), contract, authSchema: { user, session, account, verification, twoFactor }, igaming, port: Number(process.env['PORT'] ?? 3001), cors: { origins: process.env['CORS_ORIGINS']?.split(',') ?? '*' }, - openapi: { info: { title: '{{name}} API', version: '0.1.0' } }, }); await listen(); - await emitOpenApiSpec(); } void bootstrap();