Skip to content

Latest commit

 

History

1,596 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WordSparrow

CI CodeQL Deploy Frontend Deploy API Lighthouse License: FSL-1.1-MIT

A mots fléchés (French crossword variant) puzzle game for web, tablet, and mobile, with future Discord-Activity support. Brand identity is recorded in ADR-0005; "Bliss" is the working codename used throughout the repo.

Live: https://wordsparrow.io

Status

Sandbox / pre-alpha. Daily puzzles generate and play end-to-end; the multiplayer game context and player identity (OIDC) are in active development. Operated by a single maintainer with a fleet of AI agents working in parallel (ADR-0001).

Application architecture

Bounded contexts, each hexagonally layered (domain/application/infrastructure/api/):

  • grid/ — Kotlin/JVM. Puzzle generation, validation, word corpus. Includes a daily pre-generation worker (ADR-0042) and the bitmask-CSP grid generator (ADR-0039).
  • game/ — Kotlin/JVM. Multiplayer lobbies and realtime play over REST + WebSocket (ADR-0018).
  • identity/ — Kotlin/JVM. Player OIDC and session tokens (ADR-0044, ADR-0047).
  • billing/ — Kotlin/JVM. Subscription entitlement and checkout via Mollie as the payment provider (ADR-0078).
  • frontend/ — Vite + React 19 + TypeScript + Panda CSS + TanStack Router. Player UI, deployed as a static bundle to Cloudflare Workers static assets (ADR-0002, ADR-0090).

JVM is Kotlin 2.x on JDK 21 with Ktor for HTTP/WS (ADR-0006) and Postgres via CNPG + Flyway. Cross-context imports are forbidden; communication is schema-first via OpenAPI for HTTP and AsyncAPI 2.6 for WebSocket (ADR-0003, ADR-0019), with cross-context events flowing over NATS JetStream (ADR-0049). Generated TypeScript types are checked in and gated by drift CI.

Infrastructure (IaC)

All infrastructure is declarative and version-controlled. Nothing is clicked in a console.

flowchart LR
  subgraph Edge
    ingress["ingress-nginx"]
    certmanager["cert-manager"]
  end
  subgraph Messaging
    nats["NATS JetStream"]
  end
  subgraph ctx_grid["grid"]
    grid["grid-api"]
    gridDB[("grid pg")]
    grid --> gridDB
  end
  subgraph ctx_game["game"]
    game["game-api"]
    gameDB[("game pg")]
    game --> gameDB
  end
  subgraph ctx_identity["identity"]
    identity["identity-api"]
    identityDB[("identity pg")]
    identity --> identityDB
  end
  subgraph ctx_survey["survey"]
    survey["survey-api"]
    surveyDB[("survey pg")]
    survey --> surveyDB
  end
  subgraph ctx_billing["billing"]
    billing["billing-api"]
    billingDB[("billing pg")]
    billing --> billingDB
  end
  cluepipeline["clue AI pipeline (Modal)"]
  mollie["Mollie (payment provider)"]
  certmanager -->|TLS certs| ingress
  ingress --> grid
  ingress --> game
  ingress --> identity
  ingress --> survey
  ingress --> billing
  grid -->|publishes| nats
  identity -->|publishes| nats
  billing -->|publishes| nats
  nats -->|consumed by| game
  survey -. manual export .-> cluepipeline
  billing -->|hosted checkout / webhooks| mollie
  classDef data fill:#c8945633,stroke:#a87538;
  classDef external fill:#b8554022,stroke:#b85540;
  class gridDB,gameDB,identityDB,surveyDB,billingDB data;
  class cluepipeline,mollie external;
  style Edge fill:#5a655a1f,stroke:#8b9488;
  style Messaging fill:#a875381f,stroke:#c89456;
  style ctx_grid fill:#6a93581f,stroke:#6a9358;
  style ctx_game fill:#6a93581f,stroke:#6a9358;
  style ctx_identity fill:#6a93581f,stroke:#6a9358;
  style ctx_survey fill:#6a93581f,stroke:#6a9358;
  style ctx_billing fill:#6a93581f,stroke:#6a9358;
  classDef default stroke:#6b7fd7,stroke-width:1.5px;
Loading

Figure 1. In-cluster topology grouped by bounded context — each box is one context's API and database. Dashed edges are manual or leave the cluster.

flowchart LR
  subgraph CI
    deploy_api_k8s["deploy-api-k8s.yml"]
    deploy_frontend["deploy-frontend.yml"]
  end
  subgraph Cloud
    workers["Cloudflare Worker (frontend)"]
    pages["Cloudflare Pages (legacy 301)"]
    dns["Cloudflare DNS"]
    cacherules["Cloudflare cache rules"]
    httpsredirect["Cloudflare Always Use HTTPS"]
    k3s["Hetzner k3s"]
  end
  deploy_frontend -->|wrangler deploy| workers
  deploy_api_k8s -->|helm upgrade| k3s
  pages -. 301 redirect .-> workers
  dns -->|service subdomains via external-dns| k3s
  dns -->|301 upgrade| httpsredirect
  dns -->|edge cache| cacherules
  cacherules -->|cache miss / bypass| k3s
  style CI fill:#5a655a1f,stroke:#8b9488;
  style Cloud fill:#b8554020,stroke:#b85540;
  classDef default stroke:#6b7fd7,stroke-width:1.5px;
Loading

Figure 2. Where the frontend and cluster are hosted, and the CI workflows that deploy them.

flowchart LR
  browser["Browser"]
  ingress["ingress-nginx"]
  grid["grid-api"]
  game["game-api"]
  nats["NATS JetStream"]
  browser -->|HTTPS| ingress
  browser -->|WSS| ingress
  ingress --> grid
  ingress --> game
  grid -->|PuzzleReady event| nats
  nats -->|consumed by| game
  classDef context fill:#6a935826,stroke:#6a9358;
  classDef messaging fill:#a8753826,stroke:#c89456;
  class grid,game context;
  class nats messaging;
  classDef default stroke:#6b7fd7,stroke-width:1.5px;
Loading

Figure 3. How a request and the daily-puzzle event move through the system at runtime.

  • Cloud + DNS — OpenTofu manages a self-hosted Hetzner k3s cluster (ADR-0009, ADR-0011), Cloudflare DNS records, and the Cloudflare Workers static-assets project for the frontend (ADR-0004, ADR-0090). The retired Cloudflare Pages project survives as a 301 grace stub until its 2026-08-04 deletion. Roots in terraform/ (Cloudflare) and terraform/k8s/ (provider-agnostic cluster module). State is remote (ADR-0010); versions are pinned via versions.tf and .terraform.lock.hcl.
  • Cluster apps — every in-cluster app ships as a Helm chart under infra/: infra/platform/ (ingress-nginx, cert-manager, ClusterIssuers), infra/observability/ (SigNoz + alerts + oauth2-proxy), infra/nats/ (JetStream streams bootstrapped via in-cluster Job), infra/matomo/ (RGPD-compliant product analytics, ADR-0025). App charts and Postgres CNPG clusters live alongside each bounded context.
  • Configure-in-cluster, not push-from-CI — when an app needs config bootstrapped (alert rules, JetStream streams, feature-flag seeds), it ships as a Helm post-install,post-upgrade Job inside the chart rather than kubectl port-forward from a GitHub Action.
  • Deploy pipelines — frontend via .github/workflows/deploy-frontend.yml to Cloudflare Workers static assets; APIs + workers via .github/workflows/deploy-api-k8s.yml using helm upgrade --install. Container images are pinned by digest.

Operational guides: docs/local-development.md, docs/deploy.md, docs/secrets.md.

Observability, alerting & analytics

OpenTelemetry from day 1, both ends of the stack. The diagram below is the target topology — telemetry and symptom alerts on every module (plus the RGPD-compliant Matomo analytics path). A module without a source edge here is a gap to address, not a documented exception.

flowchart LR
  subgraph Sources
    frontend["frontend (browser SDK)"]
    grid["grid-api"]
    game["game-api"]
    identity["identity-api"]
    survey["survey-api"]
    billing["billing-api"]
    nats["NATS JetStream"]
    k8smetrics["k8s pod / node metrics"]
  end
  subgraph Ingest
    otlpingress["otlp.wordsparrow.io"]
    collector["OTel collector"]
  end
  subgraph Backend
    signoz["SigNoz"]
    clickhouse["ClickHouse"]
  end
  subgraph Analytics
    matomo["Matomo"]
  end
  subgraph Alerting
    alerts["symptom alert rules"]
    gmail["Gmail SMTP"]
    oauth2["oauth2-proxy"]
  end
  frontend -->|analytics| matomo
  frontend -->|OTLP traces| otlpingress
  otlpingress -->|forward| collector
  grid -->|otel| collector
  game -->|otel| collector
  identity -->|otel| collector
  survey -->|otel| collector
  billing -->|otel| collector
  nats -->|metrics| collector
  k8smetrics -->|metrics| collector
  collector -->|ingest| signoz
  signoz -->|store| clickhouse
  signoz -->|evaluate| alerts
  alerts -->|5xx / errors / staleness| gmail
  oauth2 -->|gates admin UI| signoz
  classDef data fill:#c8945633,stroke:#a87538;
  class clickhouse data;
  style Sources fill:#6a93581f,stroke:#6a9358;
  style Ingest fill:#5a655a1f,stroke:#8b9488;
  style Backend fill:#5a655a1f,stroke:#8b9488;
  style Analytics fill:#a875381f,stroke:#c89456;
  style Alerting fill:#b8554020,stroke:#b85540;
  classDef default stroke:#6b7fd7,stroke-width:1.5px;
Loading

Figure 4. Target telemetry, alerting and analytics topology — a module without a source edge here is a gap to address, not an exception.

  • Frontend traces ship to a public OTLP ingest fronted by ingress (ADR-0033).
  • Backend telemetry lands in SigNoz on ClickHouse (ADR-0027, ADR-0041), with a dedicated worker topology to isolate the observability data plane (ADR-0040). Cluster + node metrics flow via the k8s infra collector (ADR-0038).
  • Logs are structured JSON with correlation IDs. No println, no console.log, no string concatenation in log messages.
  • Alerts target symptoms, not causes — API 5xx rate, frontend error rate, daily-puzzle staleness. Alert rules are markdown files in infra/observability/alerts/ and applied via an in-cluster Helm Job; routing is Gmail SMTP (ADR-0032). The admin UI is gated by oauth2-proxy (ADR-0030).

Clue generation pipeline

French crossword clues need a French model that respects domain rules (no stem leak, right register, exact length, valid morphology). Off-the-shelf APIs don't clear that bar, so the project trains its own French clue model on Modal GPU (ADR-0057) through human-in-the-loop rounds. The lane lives in scripts/clue_generation/pipeline_v2/.

flowchart LR
  gen["Modal GPU generate (model n-1)"]
  judge["learned judge — pre-filter"]
  human["human rates · /contribuer"]
  winners["winners (qualité=5)"]
  sft["SFT → model n"]
  grid["grid corpus"]
  gen --> judge
  judge --> human
  human --> winners
  winners --> sft
  sft -. next round .-> gen
  human -. not yet wired .-> grid
  classDef context fill:#6a935826,stroke:#6a9358;
  classDef data fill:#c8945633,stroke:#a87538;
  classDef messaging fill:#a8753826,stroke:#c89456;
  class gen,sft context;
  class grid data;
  class human messaging;
  classDef default stroke:#6b7fd7,stroke-width:1.5px;
Loading

Figure 5. The Modal clue-generation training loop. Dashed edges are the round restart and the not-yet-wired grid corpus.

  • Generator — successive fine-tuned model iterations on Modal GPU; each round's model generates candidate clues for the sampled lemmas.
  • Structural filters — a deterministic chain in pipeline_v2 gates candidates before human review: typography, length, French-language detection (lingua), self-reference, tautology, stem-leak, and pleonasm.
  • Judge — a learned judge (filter_8) scores semantic quality as a pre-filter ahead of human rating (currently shadow-scored). The human rater on /contribuer is the reward signal — never the judge — so the generator cannot reward-hack it.
  • Rounds — maintainer-rated winners (qualité=5) become the SFT training set for the next model iteration; the loop stays human-anchored.
  • Grid corpus — not yet fed from this lane. The in-cluster words-clues-worker ingestion (ADR-0013) hasn't been rewired from the old CSV path to the Modal pipeline.

The human stays the reward by design: the judge only triages obvious-bad to cut rating load, never grading the data that becomes training winners. Data-licence posture (e.g. DBnary CC BY-SA) is governed by ADR-0058; evaluation logbooks live in docs/eval/.

Claude Code agent orchestration

The repo is built to be worked on by many Claude Code agents in parallel, with the maintainer as the human-in-the-loop reviewer and arbiter (ADR-0001). The operational rules are in CLAUDE.md; the rationale is in MANIFESTO.md. Mechanics:

  • One workstream per PR, hard-capped at 400 lines of diff (excluding generated code). Branches follow <type>/<short-description> and are enforced by branch-name.yml. Implementer ≠ reviewer (§6a).
  • Skill library in .claude/skills/dispatch (orchestrator playbook), reviewer (§6a reviewer agent), clue-ai, jvm-backend, frontend, schemas. Skills load via the Claude Code Skill tool and encode repo conventions so each agent starts with the same context.
  • Worktree isolation — agents run in .claude/worktrees/agent-<id>/ via the Agent tool with isolation: "worktree", so parallel work never collides on the working tree.
  • Wave-based rollouts — large features (multiplayer, custom mobile keyboard) are decomposed into a plan under docs/superpowers/plans/, then dispatched in waves of disjoint PRs. The dispatcher orchestrates implementer + reviewer + fixer loops.
  • Autonomous cron mode (/orchestrate) — a 2-minute cron tick picks up the plan, dispatches the next phase, runs the auto-fixer loop, and merges PRs when CI is green and the §6a reviewer LGTMs. Maintainer remains the escalation backstop via the log file.
  • CI gates that keep the fleet honest — Spotless, Konsist architecture tests, openapi-lint, openapi-typescript-drift, helm-lint, CodeQL, dependency-review, gitleaks, DCO sign-off, conventional commits via commitlint, and claude-code-review for the §6a review/fix cycle. No --no-verify, no force-push to shared branches.

Getting started

Local development runs against a k3d cluster that mirrors the prod k3s topology. See docs/local-development.md for the full walkthrough; the short version:

make cluster-up         # create k3d cluster (idempotent)
make cluster-bootstrap  # ingress-nginx, cert-manager, CNPG
make deploy-local       # build images, helm install
make dev                # API hot reload + Vite HMR

JVM build:

./gradlew build --parallel --build-cache   # what CI runs
./gradlew spotlessApply                    # fix formatting in place

Frontend (from frontend/):

pnpm dev          # Vite + Panda codegen
pnpm test         # vitest
pnpm e2e          # Playwright
pnpm a11y         # axe-core via Playwright (WCAG AA baseline)
pnpm api:check    # regenerate OpenAPI types; fails on drift

Contributing

See CONTRIBUTING.md for branch naming, commit conventions, DCO sign-off, and local hook setup. Every non-trivial change starts with an ADR in docs/adr/.

License

FSL-1.1-MIT — Functional Source License 1.1, MIT Future License.

In plain English:

  • Free for any non-competing use — personal, internal-business, educational, research, professional services around the Software.
  • Commercial competition is restricted — you may not host or sell a product or service that substitutes for, or substantially duplicates, WordSparrow.
  • Becomes MIT after two years — every release auto-converts to a full MIT license on the second anniversary of its publication. The Software is genuinely open in the long run; the restriction applies only to the current frontier.

The full text and edge cases are in LICENSE. For commercial-use licensing inquiries that fall outside the Permitted Purpose, contact ISHO IT EURL.

About

No description, website, or topics provided.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages