From 59ba4cf5bd8bc351dade3597514293619110297b Mon Sep 17 00:00:00 2001 From: Pavlov Alexandr Date: Tue, 16 Jun 2026 19:00:58 +0700 Subject: [PATCH] chore(gsd): build intel store (C5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .planning/intel/: file-roles, api-map (from frozen OpenAPI 1.0.0), dependency-graph, arch-decisions, stack — consumed by the planner at plan:pre. Co-Authored-By: Claude Opus 4.8 (1M context) --- .planning/intel/.last-refresh.json | 11 + .planning/intel/API-SURFACE.md | 6 + .planning/intel/api-map.json | 571 ++++++++++++++++++++ .planning/intel/arch-decisions.json | 89 +++ .planning/intel/dependency-graph.json | 223 ++++++++ .planning/intel/file-roles.json | 751 ++++++++++++++++++++++++++ .planning/intel/stack.json | 44 ++ 7 files changed, 1695 insertions(+) create mode 100644 .planning/intel/.last-refresh.json create mode 100644 .planning/intel/API-SURFACE.md create mode 100644 .planning/intel/api-map.json create mode 100644 .planning/intel/arch-decisions.json create mode 100644 .planning/intel/dependency-graph.json create mode 100644 .planning/intel/file-roles.json create mode 100644 .planning/intel/stack.json diff --git a/.planning/intel/.last-refresh.json b/.planning/intel/.last-refresh.json new file mode 100644 index 0000000..da63f9c --- /dev/null +++ b/.planning/intel/.last-refresh.json @@ -0,0 +1,11 @@ +{ + "hashes": { + "file-roles.json": "2a89e690db6b951b4f0f15b0b03489bb4903c2712a5ef7a21b50378512d6ea2a", + "api-map.json": "1875e8078dc5b12239e3440c2868d13439662fac331fc03e07f1eea14ce5ab27", + "dependency-graph.json": "aa2d60cc15cba2566fafbdfebacaed089617d0cfc5237a347f48d300beba9dc0", + "arch-decisions.json": "48e827bd7994a6c5e186480fbade28b70fc7f7b0d1cb2a18b96d0eb5c3d992d6", + "stack.json": "85fb9514315f59e2699d7e975db382b716135c15302595619021dcc7b209784b" + }, + "timestamp": "2026-06-16T11:57:39.985Z", + "version": 1 +} diff --git a/.planning/intel/API-SURFACE.md b/.planning/intel/API-SURFACE.md new file mode 100644 index 0000000..a5ce94b --- /dev/null +++ b/.planning/intel/API-SURFACE.md @@ -0,0 +1,6 @@ +# API Surface + +> Generated from `.planning/intel/api-map.json`. Do not edit by hand. + +> **Incomplete:** api-map.json has no entries (intel extraction is regex/JS-only or not yet populated). +> Treat absence here as "unknown", not "does not exist". diff --git a/.planning/intel/api-map.json b/.planning/intel/api-map.json new file mode 100644 index 0000000..c8c387f --- /dev/null +++ b/.planning/intel/api-map.json @@ -0,0 +1,571 @@ +{ + "_meta": { + "updated_at": "2026-06-16T11:57:39.821Z", + "version": 2 + }, + "_note": "Authoritative surface = openapi/server-2.openapi.json (FROZEN v1.0.0, consumed by web via openapi-typescript). Fastify routes declared with `:id` params; OpenAPI uses `{id}`. Sitemap routes are intentionally excluded from the OpenAPI contract.", + "entries": { + "GET /live": { + "method": "GET", + "path": "/live", + "params": [], + "file": "src/modules/operations/routes.ts", + "tag": "operations", + "description": "Liveness probe" + }, + "GET /ready": { + "method": "GET", + "path": "/ready", + "params": [], + "file": "src/modules/operations/routes.ts", + "tag": "operations", + "description": "Readiness probe aggregating db/parser/queue/storage health checks" + }, + "GET /metrics": { + "method": "GET", + "path": "/metrics", + "params": [], + "file": "src/modules/operations/routes.ts", + "tag": "operations", + "description": "Prometheus metrics (prom-client registry)" + }, + "GET /operations/ingest-staging": { + "method": "GET", + "path": "/operations/ingest-staging", + "params": [ + "page", + "pageSize", + "checksum", + "replayId", + "sourceReplayId", + "sourceSystem", + "status" + ], + "file": "src/modules/ingest/routes/routes.ts", + "tag": "operations", + "description": "List ingest staging records promoted from replays-fetcher" + }, + "GET /operations/ingest-staging/{id}": { + "method": "GET", + "path": "/operations/ingest-staging/:id", + "params": [ + "id" + ], + "file": "src/modules/ingest/routes/routes.ts", + "tag": "operations", + "description": "Get a single ingest staging record" + }, + "GET /operations/parse-jobs": { + "method": "GET", + "path": "/operations/parse-jobs", + "params": [ + "page", + "pageSize", + "checksum", + "jobId", + "replayId", + "status" + ], + "file": "src/modules/ingest/routes/routes.ts", + "tag": "operations", + "description": "List durable parse jobs" + }, + "GET /operations/parse-jobs/{id}": { + "method": "GET", + "path": "/operations/parse-jobs/:id", + "params": [ + "id" + ], + "file": "src/modules/ingest/routes/routes.ts", + "tag": "operations", + "description": "Get a single parse job" + }, + "GET /operations/parse-jobs/{id}/history": { + "method": "GET", + "path": "/operations/parse-jobs/:id/history", + "params": [ + "id" + ], + "file": "src/modules/ingest/routes/routes.ts", + "tag": "operations", + "description": "Parse job state-transition history" + }, + "POST /operations/parse-jobs/{id}/retry": { + "method": "POST", + "path": "/operations/parse-jobs/:id/retry", + "params": [ + "id" + ], + "file": "src/modules/ingest/routes/actions.ts", + "tag": "operations", + "description": "Retry a failed parse job (ingestCommands.retryParseJob)" + }, + "POST /operations/replays/{id}/reparse": { + "method": "POST", + "path": "/operations/replays/:id/reparse", + "params": [ + "id" + ], + "file": "src/modules/ingest/routes/actions.ts", + "tag": "operations", + "description": "Manually request a replay reparse (ingestCommands.createManualReparse)" + }, + "GET /admin/users": { + "method": "GET", + "path": "/admin/users", + "params": [], + "file": "src/modules/admin/routes/models.ts", + "tag": "admin", + "description": "List users with roles (admin-only)" + }, + "PUT /admin/users/{id}/roles": { + "method": "PUT", + "path": "/admin/users/:id/roles", + "params": [ + "id" + ], + "file": "src/modules/admin/routes/models.ts", + "tag": "admin", + "description": "Assign roles to a user (admin-only)" + }, + "GET /auth/steam/login": { + "method": "GET", + "path": "/auth/steam/login", + "params": [ + "redirectTo" + ], + "file": "src/modules/auth/routes/routes.ts", + "tag": "auth", + "description": "Begin Steam OpenID login, redirects to Steam" + }, + "GET /auth/steam/callback": { + "method": "GET", + "path": "/auth/steam/callback", + "params": [ + "openid.claimed_id", + "openid.mode", + "redirectTo" + ], + "file": "src/modules/auth/routes/steam-openid.ts", + "tag": "auth", + "description": "Steam OpenID callback, verifies assertion and establishes session cookie" + }, + "GET /auth/session": { + "method": "GET", + "path": "/auth/session", + "params": [], + "file": "src/modules/auth/routes/routes.ts", + "tag": "auth", + "description": "Current authenticated session/user" + }, + "POST /auth/logout": { + "method": "POST", + "path": "/auth/logout", + "params": [], + "file": "src/modules/auth/routes/routes.ts", + "tag": "auth", + "description": "Destroy session and clear cookie" + }, + "POST /requests": { + "method": "POST", + "path": "/requests", + "params": [], + "file": "src/modules/requests/routes/routes.ts", + "tag": "requests", + "description": "Create a player/identity correction request (login required)" + }, + "GET /requests": { + "method": "GET", + "path": "/requests", + "params": [], + "file": "src/modules/requests/routes/routes.ts", + "tag": "requests", + "description": "List the caller's own requests" + }, + "GET /requests/{id}": { + "method": "GET", + "path": "/requests/:id", + "params": [ + "id" + ], + "file": "src/modules/requests/routes/routes.ts", + "tag": "requests", + "description": "Get a single request" + }, + "POST /requests/{id}/attachments": { + "method": "POST", + "path": "/requests/:id/attachments", + "params": [ + "id" + ], + "file": "src/modules/requests/routes/routes.ts", + "tag": "requests", + "description": "Upload an attachment to a request (S3-backed)" + }, + "GET /requests/{id}/attachments": { + "method": "GET", + "path": "/requests/:id/attachments", + "params": [ + "id" + ], + "file": "src/modules/requests/routes/routes.ts", + "tag": "requests", + "description": "List request attachments (presigned)" + }, + "GET /moderation/requests": { + "method": "GET", + "path": "/moderation/requests", + "params": [], + "file": "src/modules/requests/routes/moderation/moderation.ts", + "tag": "moderation", + "description": "Moderator queue of requests (role required)" + }, + "GET /moderation/requests/{id}": { + "method": "GET", + "path": "/moderation/requests/:id", + "params": [ + "id" + ], + "file": "src/modules/requests/routes/moderation/moderation.ts", + "tag": "moderation", + "description": "Moderator view of a request" + }, + "POST /moderation/requests/{id}/decision": { + "method": "POST", + "path": "/moderation/requests/:id/decision", + "params": [ + "id" + ], + "file": "src/modules/requests/routes/moderation/moderation.ts", + "tag": "moderation", + "description": "Approve/reject a request" + }, + "POST /moderation/requests/{id}/audit-patches": { + "method": "POST", + "path": "/moderation/requests/:id/audit-patches", + "params": [ + "id" + ], + "file": "src/modules/requests/routes/audit-patches/audit-patches.ts", + "tag": "moderation", + "description": "Create an audited correction patch (preserved across reprocessing)" + }, + "GET /moderation/requests/{id}/audit-patches": { + "method": "GET", + "path": "/moderation/requests/:id/audit-patches", + "params": [ + "id" + ], + "file": "src/modules/requests/routes/audit-patches/audit-patches.ts", + "tag": "moderation", + "description": "List audit patches for a request" + }, + "POST /moderation/requests/{id}/workflows": { + "method": "POST", + "path": "/moderation/requests/:id/workflows", + "params": [ + "id" + ], + "file": "src/modules/requests/routes/workflows/workflows.ts", + "tag": "moderation", + "description": "Apply an identity workflow (steam link / merge / split) via workflowApplier" + }, + "GET /moderation/requests/{id}/workflows": { + "method": "GET", + "path": "/moderation/requests/:id/workflows", + "params": [ + "id" + ], + "file": "src/modules/requests/routes/workflows/workflows.ts", + "tag": "moderation", + "description": "List workflows applied to a request" + }, + "POST /admin/rotations": { + "method": "POST", + "path": "/admin/rotations", + "params": [], + "file": "src/modules/admin/routes/rotations.ts", + "tag": "admin", + "description": "Create a rotation (admin-only)" + }, + "PUT /admin/rotations/{id}": { + "method": "PUT", + "path": "/admin/rotations/:id", + "params": [ + "id" + ], + "file": "src/modules/admin/routes/rotations.ts", + "tag": "admin", + "description": "Update a rotation (admin-only)" + }, + "DELETE /admin/rotations/{id}": { + "method": "DELETE", + "path": "/admin/rotations/:id", + "params": [ + "id" + ], + "file": "src/modules/admin/routes/rotations.ts", + "tag": "admin", + "description": "Delete a rotation (admin-only)" + }, + "GET /stats/overview": { + "method": "GET", + "path": "/stats/overview", + "params": [ + "rotationId" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Aggregate overview, optionally scoped to a rotation" + }, + "GET /stats/rotations": { + "method": "GET", + "path": "/stats/rotations", + "params": [], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "List rotations" + }, + "GET /stats/rotations/{id}": { + "method": "GET", + "path": "/stats/rotations/:id", + "params": [ + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Single rotation detail" + }, + "GET /stats/players": { + "method": "GET", + "path": "/stats/players", + "params": [ + "cursor", + "limit", + "order", + "sort", + "rotationId", + "search" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Keyset-paginated player list" + }, + "GET /stats/players/{id}": { + "method": "GET", + "path": "/stats/players/:id", + "params": [ + "rotationId", + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Player profile/aggregates" + }, + "GET /stats/players/{id}/weapons": { + "method": "GET", + "path": "/stats/players/:id/weapons", + "params": [ + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Player weapon breakdown" + }, + "GET /stats/players/{id}/vehicles": { + "method": "GET", + "path": "/stats/players/:id/vehicles", + "params": [ + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Player vehicle breakdown" + }, + "GET /stats/players/{id}/relationships": { + "method": "GET", + "path": "/stats/players/:id/relationships", + "params": [ + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Player relationship stats" + }, + "GET /stats/players/{id}/weekly": { + "method": "GET", + "path": "/stats/players/:id/weekly", + "params": [ + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Player weekly time series" + }, + "GET /stats/players/{id}/name-history": { + "method": "GET", + "path": "/stats/players/:id/name-history", + "params": [ + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Player name history" + }, + "GET /stats/players/{id}/membership-history": { + "method": "GET", + "path": "/stats/players/:id/membership-history", + "params": [ + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Player squad membership history" + }, + "GET /stats/squads": { + "method": "GET", + "path": "/stats/squads", + "params": [ + "cursor", + "limit", + "order", + "sort", + "rotationId", + "search" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Keyset-paginated squad list" + }, + "GET /stats/squads/{id}": { + "method": "GET", + "path": "/stats/squads/:id", + "params": [ + "rotationId", + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Squad profile/aggregates" + }, + "GET /stats/squads/{id}/weapons": { + "method": "GET", + "path": "/stats/squads/:id/weapons", + "params": [ + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Squad weapon breakdown" + }, + "GET /stats/squads/{id}/relationships": { + "method": "GET", + "path": "/stats/squads/:id/relationships", + "params": [ + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Squad relationship stats" + }, + "GET /stats/squads/{id}/weekly": { + "method": "GET", + "path": "/stats/squads/:id/weekly", + "params": [ + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Squad weekly time series" + }, + "GET /stats/squads/{id}/membership-history": { + "method": "GET", + "path": "/stats/squads/:id/membership-history", + "params": [ + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Squad membership history" + }, + "GET /stats/commander-sides": { + "method": "GET", + "path": "/stats/commander-sides", + "params": [ + "rotationId", + "side" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Commander side statistics" + }, + "GET /stats/bounty": { + "method": "GET", + "path": "/stats/bounty", + "params": [ + "cursor", + "limit", + "order", + "sort", + "rotationId" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Bounty points leaderboard (keyset)" + }, + "GET /stats/leaderboards": { + "method": "GET", + "path": "/stats/leaderboards", + "params": [ + "rotationId", + "bountyCursor", + "limit", + "playersCursor", + "squadsCursor" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Combined bounty/players/squads leaderboards with per-section cursors" + }, + "GET /stats/replays": { + "method": "GET", + "path": "/stats/replays", + "params": [ + "cursor", + "limit", + "order", + "sort", + "rotationId", + "fromDate", + "toDate" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Keyset-paginated replay list" + }, + "GET /stats/replays/{id}": { + "method": "GET", + "path": "/stats/replays/:id", + "params": [ + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Single replay detail" + }, + "GET /stats/replays/{id}/events": { + "method": "GET", + "path": "/stats/replays/:id/events", + "params": [ + "cursor", + "limit", + "order", + "sort", + "id" + ], + "file": "src/modules/public-stats/routes/routes.ts", + "tag": "public-stats", + "description": "Keyset-paginated events for a replay" + } + } +} diff --git a/.planning/intel/arch-decisions.json b/.planning/intel/arch-decisions.json new file mode 100644 index 0000000..623245c --- /dev/null +++ b/.planning/intel/arch-decisions.json @@ -0,0 +1,89 @@ +{ + "_meta": { + "updated_at": "2026-06-16T11:57:39.903Z", + "version": 2 + }, + "entries": { + "overview": { + "pattern": "Modular monolith / hexagonal (ports-and-adapters) Fastify backend", + "description": "server-2 is the source of truth for Solid Stats. Feature modules under src/modules/* (operations, ingest, auth, admin, requests, public-stats, statistics) each expose Fastify route plugins. Within a module the layering is routes -> service -> repository (the AGENTS.md 'controllers->usecases->services->repositories' intent maps onto routes/service/repository here). Each module defines port interfaces in a models.ts/types.ts and ships two adapters: an InMemory* implementation for tests and a Pg* implementation for production." + }, + "data-flow": { + "flow": "replays-fetcher staging/outbox -> ingest promotion (PgIngestRepository) -> parse_jobs + RabbitMQ publish (ParseJobPublisher) -> replay-parser-2 -> parser artifact -> ParserResultRecalculationService persists aggregates -> PostgreSQL canonical state -> public-stats read model -> /stats/* API -> web", + "description": "createIngestRuntime runs polling interval tasks (interval-task.ts) that promote staging records, publish parse-job requests over RabbitMQ, and reconcile completions/failures. Parser artifacts are validated and recalculated into player/squad/commander/bounty aggregates. Public stats are served read-only from PgPublicStatsReadModel." + }, + "composition-root": { + "entry": "src/server.ts", + "description": "server.ts is the wiring root: loads config (envalid), constructs pg Pool, Pg* repositories, RabbitMQ runtime, S3 storage, then calls buildApp() (src/app.ts) injecting all production adapters plus the ingest runtime. src/instrument.ts (dotenv + Sentry) is imported first. buildApp() with no options falls back to InMemory adapters, which is how tests boot the app via app.inject()." + }, + "api-contract": { + "contract": "OpenAPI 1.0.0 (FROZEN)", + "description": "openapi/server-2.openapi.json is the frozen contract consumed by the web frontend via openapi-typescript. It is generated from TypeBox route schemas through @fastify/swagger (createOpenApiSchema / registerOpenApi). pnpm openapi:verify checks live app schema matches the committed JSON; pnpm openapi:check regenerates web client types. Any route/schema change must preserve client compatibility. Sitemap routes are deliberately registered as a separate top-level plugin and excluded from the contract." + }, + "validation": { + "approach": "Schema-first TypeBox", + "description": "Routes use @fastify/type-provider-typebox; request/response schemas (e.g. routes/schemas.ts, PaginationQuery) double as runtime validation and OpenAPI source. Environment config validated with envalid in config/env.ts (loadConfig, redactConfigForLogs)." + }, + "auth": { + "mechanism": "Steam OpenID + session cookie", + "description": "SteamOpenIdClient performs OpenID login/callback; sessions stored via SessionStore adapter (PgSessionStore in prod) keyed by a signed session cookie (cookies.ts). Authorization helpers (currentUser, requireRole, requireAnyRole) gate moderation/admin routes by role. Public /stats/* is anonymous; /requests/* requires login; /moderation and /admin require roles." + }, + "persistence": { + "store": "PostgreSQL via raw pg.Pool", + "description": "All Pg* repositories use the pg driver with hand-written SQL (kysely is a declared dependency but the query builder is not yet used; createDatabasePool returns a raw pg Pool). Migrations live in src/infra/db/migrations and run via src/infra/db/migrate.ts (pnpm db:migrate). Raw replay files and request attachments live in S3 (storage/client.ts), not in PostgreSQL." + }, + "queue": { + "broker": "RabbitMQ (amqplib)", + "description": "infra/queue/messages.ts defines the parser exchange and parse-requested/completed/failed routing keys and queues. createRabbitMqParserRuntime wires publishers/consumers; ingest publisher/reconciler coordinate durable parse_jobs state. Parser work must never bypass the parse_jobs table." + }, + "statistics": { + "responsibility": "Derived aggregates and parity", + "description": "statistics module computes player/squad/commander aggregates, bounty points (bounty.ts), game-type classification, player exclusions, and legacy public export. parity-formulas.ts (kdRatio, totalScore, weeklyScore, etc.) encodes the golden-data parity formulas validated against ~/sg_stats reference data. Audit patches from moderation are preserved across reprocessing via PgAuditPatchRecalculator / PgRequestWorkflowApplier." + }, + "observability": { + "tooling": "pino + prom-client + Sentry", + "description": "Structured pino logging (logger.ts), Prometheus metrics exposed at GET /metrics (metrics/registry.ts), Sentry init in instrument.ts. Health checks (infra/health.ts) for db/parser/queue/storage back the GET /ready probe." + }, + "conventions": { + "naming": "Factory functions createX / registerXRoutes; adapter classes prefixed InMemory* (tests) and Pg* (production); port interfaces and types in models.ts / types.ts per module.", + "file-organization": "Feature-first under src/modules/; cross-cutting infra under src/infra/*; OpenAPI under src/openapi/*; operational/maintenance scripts under src/operations/* (pnpm ops:*). Tests colocated in tests/ subdirs; integration tests under src/test/integration.", + "imports": "ESM with explicit .js extensions (NodeNext); import hygiene enforced via eslint-plugin-import-x; module boundaries guarded by ops:boundary:check (check-app-boundary-guards.ts)." + }, + "quality-gates": { + "verify": "pnpm verify", + "description": "Verification chain: prettier check, eslint (ESLint 10 all + strict typed + unicorn + import-x), tsc typecheck (very strict tsconfig), vitest unit, vitest integration (testcontainers), openapi:check (contract drift), ops:backup:check, ops:boundary:check, and V8 coverage gate." + }, + "component:operations": { + "path": "src/modules/operations/routes.ts", + "responsibility": "Health (/live, /ready), Prometheus (/metrics), OpenAPI/docs exposure" + }, + "component:ingest": { + "path": "src/modules/ingest", + "responsibility": "Promote replays-fetcher staging into canonical replays/parse_jobs, publish parse requests to RabbitMQ, reconcile completions, expose /operations/* read APIs and retry/reparse actions" + }, + "component:auth": { + "path": "src/modules/auth", + "responsibility": "Steam OpenID login/session/role identity and authorization pre-handlers" + }, + "component:admin": { + "path": "src/modules/admin", + "responsibility": "Admin-only user role management and rotation CRUD" + }, + "component:requests": { + "path": "src/modules/requests", + "responsibility": "Authenticated correction requests, S3 attachments, moderation decisions, audit patches, and identity workflows (steam link / merge / split)" + }, + "component:public-stats": { + "path": "src/modules/public-stats", + "responsibility": "Read-only keyset-paginated public statistics (players, squads, replays, bounty, leaderboards, rotations) plus replay sitemaps" + }, + "component:statistics": { + "path": "src/modules/statistics", + "responsibility": "Recalculation of derived aggregates from parser artifacts, bounty/parity formulas, game-type classification, readiness checks, and legacy export" + }, + "component:infra": { + "path": "src/infra", + "responsibility": "Shared adapters: pg pool, RabbitMQ runtime, S3 client, pino logging, prom-client metrics, health checks, interval task runtime" + } + } +} diff --git a/.planning/intel/dependency-graph.json b/.planning/intel/dependency-graph.json new file mode 100644 index 0000000..579e577 --- /dev/null +++ b/.planning/intel/dependency-graph.json @@ -0,0 +1,223 @@ +{ + "_meta": { + "updated_at": "2026-06-16T11:57:39.862Z", + "version": 2 + }, + "entries": { + "fastify": { + "version": "^5.8.5", + "type": "production", + "invocation": "require", + "used_by": [ + "src/app.ts", + "src/modules/**/routes/*.ts" + ] + }, + "@fastify/type-provider-typebox": { + "version": "^6.1.0", + "type": "production", + "invocation": "require", + "used_by": [ + "src/app.ts", + "src/modules/**/routes" + ] + }, + "@sinclair/typebox": { + "version": "^0.34.49", + "type": "production", + "invocation": "require", + "used_by": [ + "src/modules/**/routes/schemas.ts", + "src/openapi/schema.ts" + ] + }, + "@fastify/swagger": { + "version": "^9.7.0", + "type": "production", + "invocation": "require", + "used_by": [ + "src/openapi/register-openapi.ts" + ] + }, + "@fastify/swagger-ui": { + "version": "^5.2.6", + "type": "production", + "invocation": "require", + "used_by": [ + "src/openapi/register-openapi.ts" + ] + }, + "pg": { + "version": "^8.20.0", + "type": "production", + "invocation": "require", + "used_by": [ + "src/infra/db/client.ts", + "src/modules/**/postgres.ts", + "src/modules/**/repository*.ts" + ] + }, + "kysely": { + "version": "^0.29.0", + "type": "production", + "invocation": "require", + "used_by": [ + "declared dep; data access currently uses raw pg.Pool query strings, not the Kysely query builder" + ] + }, + "amqplib": { + "version": "^1.0.7", + "type": "production", + "invocation": "require", + "used_by": [ + "src/infra/queue/rabbitmq.ts", + "src/infra/queue/client.ts" + ] + }, + "@aws-sdk/client-s3": { + "version": "^3.1045.0", + "type": "production", + "invocation": "require", + "used_by": [ + "src/infra/storage/client.ts" + ] + }, + "@aws-sdk/s3-request-presigner": { + "version": "^3.1045.0", + "type": "production", + "invocation": "require", + "used_by": [ + "src/infra/storage/client.ts" + ] + }, + "envalid": { + "version": "^8.1.1", + "type": "production", + "invocation": "require", + "used_by": [ + "src/config/env.ts" + ] + }, + "dotenv": { + "version": "^17.4.2", + "type": "production", + "invocation": "require", + "used_by": [ + "src/instrument.ts", + "src/server.ts" + ] + }, + "@sentry/node": { + "version": "^10.57.0", + "type": "production", + "invocation": "require", + "used_by": [ + "src/instrument.ts" + ] + }, + "pino": { + "version": "^10.3.1", + "type": "production", + "invocation": "require", + "used_by": [ + "src/infra/logging/logger.ts" + ] + }, + "prom-client": { + "version": "^15.1.3", + "type": "production", + "invocation": "require", + "used_by": [ + "src/infra/metrics/registry.ts", + "src/modules/operations/routes.ts" + ] + }, + "tsx": { + "version": "^4.21.0", + "type": "development", + "invocation": "pnpm run dev", + "used_by": [ + "dev", + "db:migrate", + "openapi:export", + "openapi:verify", + "ops:*" + ] + }, + "vitest": { + "version": "^4.1.5", + "type": "development", + "invocation": "pnpm test", + "used_by": [ + "test", + "test:integration", + "test:schema" + ] + }, + "@vitest/coverage-v8": { + "version": "^4.1.5", + "type": "development", + "invocation": "pnpm run test:coverage", + "used_by": [ + "test:coverage" + ] + }, + "openapi-typescript": { + "version": "^7.13.0", + "type": "development", + "invocation": "pnpm run openapi:check", + "used_by": [ + "openapi:check" + ] + }, + "eslint": { + "version": "^10.3.0", + "type": "development", + "invocation": "pnpm run lint", + "used_by": [ + "lint" + ] + }, + "typescript-eslint": { + "version": "^8.59.2", + "type": "development", + "invocation": "pnpm run lint", + "used_by": [ + "lint" + ] + }, + "eslint-plugin-import-x": { + "version": "^4.16.2", + "type": "development", + "invocation": "pnpm run lint", + "used_by": [ + "lint" + ] + }, + "eslint-plugin-unicorn": { + "version": "^64.0.0", + "type": "development", + "invocation": "pnpm run lint", + "used_by": [ + "lint" + ] + }, + "prettier": { + "version": "^3.8.3", + "type": "development", + "invocation": "pnpm run format", + "used_by": [ + "format" + ] + }, + "typescript": { + "version": "^6.0.3", + "type": "development", + "invocation": "pnpm run build", + "used_by": [ + "build", + "typecheck" + ] + } + } +} diff --git a/.planning/intel/file-roles.json b/.planning/intel/file-roles.json new file mode 100644 index 0000000..505d1da --- /dev/null +++ b/.planning/intel/file-roles.json @@ -0,0 +1,751 @@ +{ + "_meta": { + "updated_at": "2026-06-16T11:57:39.777Z", + "version": 2 + }, + "entries": { + "src/server.ts": { + "exports": [], + "imports": [ + "./instrument.js", + "./app.js", + "./config/env.js", + "./infra/db/client.js", + "./infra/queue/rabbitmq.js", + "./infra/storage/client.js", + "./modules/ingest/runtime.js" + ], + "type": "entry-point" + }, + "src/app.ts": { + "exports": [ + "buildApp", + "createDefaultAuthOptions", + "BuildAppOptions" + ], + "imports": [ + "fastify", + "@fastify/type-provider-typebox", + "./openapi/register-openapi.js", + "./modules/*/routes/routes.js", + "./infra/health.js", + "./infra/metrics/registry.js" + ], + "type": "entry-point" + }, + "src/instrument.ts": { + "exports": [], + "imports": [ + "dotenv/config", + "@sentry/node" + ], + "type": "script" + }, + "src/config/env.ts": { + "exports": [ + "loadConfig", + "redactConfigForLogs" + ], + "imports": [ + "envalid" + ], + "type": "config" + }, + "src/infra/health.ts": { + "exports": [ + "createStaticHealthCheck" + ], + "imports": [], + "type": "module" + }, + "src/infra/db/client.ts": { + "exports": [ + "createDatabasePool", + "createDbClient", + "createDatabaseHealthCheck" + ], + "imports": [ + "pg", + "../../config/env.js", + "../health.js" + ], + "type": "module" + }, + "src/infra/db/migrate.ts": { + "exports": [], + "imports": [ + "pg", + "../../config/env.js" + ], + "type": "script" + }, + "src/infra/logging/logger.ts": { + "exports": [ + "createLoggerOptions" + ], + "imports": [ + "pino", + "../../config/env.js" + ], + "type": "module" + }, + "src/infra/metrics/registry.ts": { + "exports": [ + "createMetricsRegistry", + "registerOperationalMetrics" + ], + "imports": [ + "prom-client" + ], + "type": "module" + }, + "src/infra/queue/client.ts": { + "exports": [ + "createQueueClient" + ], + "imports": [ + "amqplib", + "../../config/env.js" + ], + "type": "module" + }, + "src/infra/queue/messages.ts": { + "exports": [ + "parserExchange", + "parseRequestedRoutingKey", + "parseCompletedRoutingKey", + "parseFailedRoutingKey", + "parseRequestedQueue", + "parseCompletedQueue", + "parseFailedQueue" + ], + "imports": [], + "type": "type-def" + }, + "src/infra/queue/rabbitmq.ts": { + "exports": [ + "createRabbitMqParserRuntime" + ], + "imports": [ + "amqplib", + "../../config/env.js", + "./messages.js" + ], + "type": "module" + }, + "src/infra/runtime/interval-task.ts": { + "exports": [ + "IntervalTask" + ], + "imports": [], + "type": "module" + }, + "src/infra/storage/client.ts": { + "exports": [ + "createStorageClient" + ], + "imports": [ + "@aws-sdk/client-s3", + "@aws-sdk/s3-request-presigner", + "../../config/env.js" + ], + "type": "module" + }, + "src/openapi/schema.ts": { + "exports": [ + "createOpenApiSchema" + ], + "imports": [ + "@sinclair/typebox" + ], + "type": "module" + }, + "src/openapi/register-openapi.ts": { + "exports": [ + "registerOpenApi" + ], + "imports": [ + "@fastify/swagger", + "@fastify/swagger-ui", + "./schema.js" + ], + "type": "module" + }, + "src/openapi/export-openapi.ts": { + "exports": [], + "imports": [ + "../app.js", + "./register-openapi.js" + ], + "type": "script" + }, + "src/openapi/verify-openapi.ts": { + "exports": [], + "imports": [ + "../app.js" + ], + "type": "script" + }, + "src/modules/operations/routes.ts": { + "exports": [ + "registerOperationsRoutes" + ], + "imports": [ + "../../infra/health.js", + "../../infra/metrics/registry.js" + ], + "type": "module" + }, + "src/modules/ingest/routes/routes.ts": { + "exports": [ + "registerIngestRoutes", + "createEmptyIngestCommandModel", + "createEmptyIngestReadModel", + "IngestCommandModel", + "IngestReadModel" + ], + "imports": [ + "fastify", + "../service.js", + "../types.js", + "./actions.js" + ], + "type": "module" + }, + "src/modules/ingest/routes/actions.ts": { + "exports": [ + "registerIngestActionRoutes", + "createEmptyIngestCommandModel" + ], + "imports": [ + "../service.js" + ], + "type": "module" + }, + "src/modules/ingest/service.ts": { + "exports": [ + "IngestPromotionService" + ], + "imports": [ + "./types.js", + "./repository/repository.js" + ], + "type": "module" + }, + "src/modules/ingest/runtime.ts": { + "exports": [ + "createIngestRuntime" + ], + "imports": [ + "./publisher.js", + "./reconciler.js", + "../../infra/runtime/interval-task.js" + ], + "type": "module" + }, + "src/modules/ingest/publisher.ts": { + "exports": [ + "ParseJobPublisher" + ], + "imports": [ + "../../infra/queue/messages.js" + ], + "type": "module" + }, + "src/modules/ingest/reconciler.ts": { + "exports": [ + "ParseJobReconciler" + ], + "imports": [ + "./repository/repository.js" + ], + "type": "module" + }, + "src/modules/ingest/repository/repository.ts": { + "exports": [ + "PgIngestRepository" + ], + "imports": [ + "pg", + "../types.js" + ], + "type": "module" + }, + "src/modules/ingest/types.ts": { + "exports": [], + "imports": [], + "type": "type-def" + }, + "src/modules/auth/routes/routes.ts": { + "exports": [ + "registerAuthRoutes" + ], + "imports": [ + "fastify", + "./models.js", + "./steam-openid.js", + "./cookies.js", + "./authorization.js" + ], + "type": "module" + }, + "src/modules/auth/routes/steam-openid.ts": { + "exports": [ + "SteamOpenIdClient" + ], + "imports": [], + "type": "module" + }, + "src/modules/auth/routes/authorization.ts": { + "exports": [ + "currentUser", + "requireRole", + "requireAnyRole" + ], + "imports": [ + "./models.js" + ], + "type": "module" + }, + "src/modules/auth/routes/cookies.ts": { + "exports": [ + "readCookie", + "sessionCookie", + "expiredSessionCookie" + ], + "imports": [], + "type": "module" + }, + "src/modules/auth/routes/role-routes.ts": { + "exports": [ + "registerRoleRoutes" + ], + "imports": [ + "./authorization.js" + ], + "type": "module" + }, + "src/modules/auth/routes/memory.ts": { + "exports": [ + "InMemoryAuthUserRepository", + "InMemorySessionStore" + ], + "imports": [ + "./models.js" + ], + "type": "module" + }, + "src/modules/auth/routes/postgres.ts": { + "exports": [ + "PgAuthUserRepository", + "PgSessionStore" + ], + "imports": [ + "pg", + "./models.js" + ], + "type": "module" + }, + "src/modules/auth/routes/models.ts": { + "exports": [], + "imports": [], + "type": "type-def" + }, + "src/modules/admin/routes/rotations.ts": { + "exports": [ + "registerAdminRoutes" + ], + "imports": [ + "fastify", + "./models.js", + "../../auth/routes/authorization.js" + ], + "type": "module" + }, + "src/modules/admin/routes/rotation-repository.ts": { + "exports": [ + "PgAdminRotationRepository" + ], + "imports": [ + "pg", + "./models.js" + ], + "type": "module" + }, + "src/modules/admin/routes/memory.ts": { + "exports": [ + "InMemoryAdminRotationRepository" + ], + "imports": [ + "./models.js" + ], + "type": "module" + }, + "src/modules/admin/routes/models.ts": { + "exports": [], + "imports": [], + "type": "type-def" + }, + "src/modules/requests/routes/routes.ts": { + "exports": [ + "registerRequestRoutes" + ], + "imports": [ + "fastify", + "./models.js", + "./attachment-storage.js", + "./reference-validator.js" + ], + "type": "module" + }, + "src/modules/requests/routes/moderation/moderation.ts": { + "exports": [ + "registerRequestModerationRoutes" + ], + "imports": [ + "fastify", + "../models.js", + "../../../auth/routes/authorization.js" + ], + "type": "module" + }, + "src/modules/requests/routes/audit-patches/audit-patches.ts": { + "exports": [ + "registerAuditPatchRoutes" + ], + "imports": [ + "fastify", + "../../models.js", + "../audit-recalculator.js" + ], + "type": "module" + }, + "src/modules/requests/routes/workflows/workflows.ts": { + "exports": [ + "registerRequestWorkflowRoutes" + ], + "imports": [ + "fastify", + "../../models.js", + "../workflow-applier.js" + ], + "type": "module" + }, + "src/modules/requests/routes/workflow-applier.ts": { + "exports": [ + "createNoopRequestWorkflowApplier", + "PgRequestWorkflowApplier" + ], + "imports": [ + "pg", + "../../statistics/repository/repository.js" + ], + "type": "module" + }, + "src/modules/requests/routes/audit-recalculator.ts": { + "exports": [ + "NoopAuditPatchRecalculator", + "PgAuditPatchRecalculator" + ], + "imports": [ + "pg", + "../../statistics/repository/repository.js" + ], + "type": "module" + }, + "src/modules/requests/routes/attachment-storage.ts": { + "exports": [ + "InMemoryRequestAttachmentStorage" + ], + "imports": [ + "../../../infra/storage/client.js" + ], + "type": "module" + }, + "src/modules/requests/routes/reference-validator.ts": { + "exports": [ + "EmptyReferenceValidator", + "PgReferenceValidator" + ], + "imports": [ + "pg", + "./models.js" + ], + "type": "module" + }, + "src/modules/requests/routes/memory.ts": { + "exports": [ + "InMemoryPlayerRequestRepository" + ], + "imports": [ + "./models.js" + ], + "type": "module" + }, + "src/modules/requests/routes/postgres.ts": { + "exports": [ + "PgPlayerRequestRepository", + "PgReferenceValidator" + ], + "imports": [ + "pg", + "./models.js" + ], + "type": "module" + }, + "src/modules/requests/routes/models.ts": { + "exports": [], + "imports": [], + "type": "type-def" + }, + "src/modules/public-stats/routes/routes.ts": { + "exports": [ + "registerPublicStatsRoutes", + "createEmptyPublicStatsReadModel", + "PublicStatsReadModel" + ], + "imports": [ + "fastify", + "./schemas.js", + "./models.js", + "./pagination/keyset.js", + "./filters.js" + ], + "type": "module" + }, + "src/modules/public-stats/routes/schemas.ts": { + "exports": [ + "PaginationQuery" + ], + "imports": [ + "@sinclair/typebox" + ], + "type": "module" + }, + "src/modules/public-stats/routes/sitemap-routes.ts": { + "exports": [ + "registerReplaySitemapRoutes" + ], + "imports": [ + "fastify", + "./sitemap.js", + "./models.js" + ], + "type": "module" + }, + "src/modules/public-stats/routes/sitemap.ts": { + "exports": [ + "escapeXml", + "urlsetXml", + "sitemapIndexXml", + "SITEMAP_PAGE_SIZE" + ], + "imports": [], + "type": "module" + }, + "src/modules/public-stats/routes/pagination/keyset.ts": { + "exports": [ + "buildKeysetPredicate" + ], + "imports": [ + "./cursor.js", + "./sort.js", + "./errors.js" + ], + "type": "module" + }, + "src/modules/public-stats/routes/pagination/cursor.ts": { + "exports": [ + "encodeCursor", + "decodeCursor" + ], + "imports": [], + "type": "module" + }, + "src/modules/public-stats/routes/slug.ts": { + "exports": [ + "slugify", + "shortSuffix", + "looksLikeUuid" + ], + "imports": [], + "type": "module" + }, + "src/modules/public-stats/repository.ts": { + "exports": [ + "PgPublicStatsReadModel", + "mapBounty" + ], + "imports": [ + "pg", + "./routes/models.js", + "./replay-mapper.js" + ], + "type": "module" + }, + "src/modules/public-stats/replay-mapper.ts": { + "exports": [ + "extractMapName", + "scrubPayload", + "scrubActor", + "mapReplayDetail", + "mapReplayEvent" + ], + "imports": [ + "./routes/models.js" + ], + "type": "module" + }, + "src/modules/statistics/service/recalculation.ts": { + "exports": [ + "ParserResultRecalculationService" + ], + "imports": [ + "../repository/repository.js", + "../parser-artifact.js", + "../parity-formulas.js" + ], + "type": "module" + }, + "src/modules/statistics/service/service.ts": { + "exports": [ + "calculatePlayerAndSquadAggregates", + "artifactCounterDeaths", + "ParserArtifactPersistenceService" + ], + "imports": [ + "../repository/repository.js" + ], + "type": "module" + }, + "src/modules/statistics/service/commander.ts": { + "exports": [ + "calculateCommanderSideAggregates" + ], + "imports": [ + "../repository/repository.js" + ], + "type": "module" + }, + "src/modules/statistics/service/full-run-recalculation.ts": { + "exports": [ + "FullRunRecalculationService" + ], + "imports": [ + "../repository/full-run.js" + ], + "type": "module" + }, + "src/modules/statistics/repository/repository.ts": { + "exports": [ + "PgStatisticsRepository" + ], + "imports": [ + "pg", + "./parity-sql.js" + ], + "type": "module" + }, + "src/modules/statistics/parser-artifact.ts": { + "exports": [ + "mapParserArtifact" + ], + "imports": [], + "type": "module" + }, + "src/modules/statistics/parity-formulas.ts": { + "exports": [ + "kdRatio", + "killsFromVehicleCoef", + "totalScore", + "weeklyScore", + "round" + ], + "imports": [], + "type": "module" + }, + "src/modules/statistics/bounty/bounty.ts": { + "exports": [ + "calculateBountyPoints" + ], + "imports": [], + "type": "module" + }, + "src/modules/statistics/game-type/classify-game-type.ts": { + "exports": [ + "extractMissionName", + "classifyGameType" + ], + "imports": [ + "./game-type-config.js" + ], + "type": "module" + }, + "src/modules/statistics/exclude-players/exclude-players.ts": { + "exports": [ + "normalizeExcludeName", + "isWithinInterval", + "isPlayerExcluded" + ], + "imports": [ + "./exclude-players-config.js" + ], + "type": "module" + }, + "src/modules/statistics/export/legacy-public-export.ts": { + "exports": [ + "LegacyPublicStatsExportService", + "LEGACY_PUBLIC_EXPORT_CONTRACT_VERSION", + "weekExport", + "sortRelationships", + "sortWeapons", + "sortWeeks" + ], + "imports": [ + "../repository/legacy-export.js" + ], + "type": "module" + }, + "src/modules/statistics/readiness/readiness.ts": { + "exports": [ + "StatisticsReadinessService" + ], + "imports": [ + "../repository/readiness.js" + ], + "type": "module" + }, + "src/operations/recalculate-statistics.ts": { + "exports": [], + "imports": [ + "../modules/statistics/service/recalculation.js", + "../infra/db/client.js" + ], + "type": "script" + }, + "src/operations/statistics-readiness.ts": { + "exports": [], + "imports": [ + "../modules/statistics/readiness/readiness.js" + ], + "type": "script" + }, + "src/operations/check-app-boundary-guards.ts": { + "exports": [], + "imports": [], + "type": "script" + }, + "src/operations/check-backup-runbook.ts": { + "exports": [], + "imports": [], + "type": "script" + }, + "src/operations/export-legacy-public-stats.ts": { + "exports": [], + "imports": [ + "../modules/statistics/export/legacy-public-export.js" + ], + "type": "script" + } + } +} diff --git a/.planning/intel/stack.json b/.planning/intel/stack.json new file mode 100644 index 0000000..d5f75ae --- /dev/null +++ b/.planning/intel/stack.json @@ -0,0 +1,44 @@ +{ + "_meta": { + "updated_at": "2026-06-16T11:57:39.736Z", + "version": 2 + }, + "languages": [ + "TypeScript" + ], + "frameworks": [ + "Fastify 5" + ], + "tools": [ + "ESLint 10 (typescript-eslint, import-x, unicorn)", + "Prettier 3", + "Vitest 4", + "V8 coverage", + "tsx", + "openapi-typescript", + "@fastify/swagger", + "@fastify/swagger-ui" + ], + "build_system": "tsc (tsconfig.build.json) via pnpm scripts", + "test_framework": "Vitest 4 (unit + testcontainers integration)", + "package_manager": "pnpm@11.5.3 (Node >=25 <26)", + "datastores": [ + "PostgreSQL (pg driver)", + "RabbitMQ (amqplib)", + "S3-compatible (@aws-sdk/client-s3)" + ], + "observability": [ + "pino logging", + "prom-client metrics", + "@sentry/node" + ], + "validation": [ + "@sinclair/typebox + @fastify/type-provider-typebox", + "envalid (env config)" + ], + "content_formats": [ + "OpenAPI 1.0.0 JSON contract (openapi/server-2.openapi.json, FROZEN, consumed by web)", + "SQL migrations (src/infra/db/migrations)", + "Markdown (.planning docs, skills)" + ] +}