Commit Graph
151 Commits
Author SHA1 Message Date
tiennm99 e5cee5c08f feat(deploy): AWS SAM template + Makefile + GitHub Actions
- AWS SAM CloudFormation template for Lambda + DynamoDB + EventBridge
- SAM config for us-east-1 deployment with guided parameters
- Unified Makefile: build-lambda, dynamodb-local, sam-* targets
- GitHub Actions: OIDC trust + SAM deploy on push to main
- CI job: add iac stage (sam validate)
- .gitignore: build/, bin/, .aws-sam/, samconfig.local.toml
2026-05-10 02:29:49 +07:00
tiennm99 27a54977d3 feat(server): wire KV_PROVIDER env selection (memory|firestore|dynamodb)
- Add environment-based provider selection in main.go
- Support memory (test), Firestore (GCP), and DynamoDB (AWS) backends
2026-05-10 02:29:43 +07:00
tiennm99 b061ab78df feat(storage): add DynamoDB KV provider sibling to Firestore
- Implement DynamoDB client wrapper and KV provider
- Full CRUD test coverage for both local and AWS modes
- AWS SDK v2 dependency added to go.mod/go.sum
2026-05-10 02:29:39 +07:00
tiennm99 5bcb643222 docs(plans): research AWS vs GCP free tier + create AWS port plan
- Research reports comparing AWS/GCP free tiers and greenfield trade-offs
- New AWS port plan with 7 phases (infrastructure, deployment, integration)
- Mark GCP plan deploy phases as superseded by new AWS plan
2026-05-10 02:29:35 +07:00
tiennm99 2b9bbf27ae feat(observability): request log middleware + in-memory metrics
Phase 11 partial of the go-port-cloud-run plan. Code-side
observability hooks ready ahead of Phase 01 GCP rollout.

- internal/server/log_middleware.go: HTTP middleware that wraps the
  router and emits {msg:"req", method, path, status, ms} per
  request. statusRecorder defaults to 200 when the inner handler
  doesn't call WriteHeader (Go writes 200 implicitly on first body
  write). Wired into server.New so /, /webhook, /cron/* all log.
- internal/metrics/counters.go: in-memory Registry with
  IncCommand/IncError/IncAI. Atomic Int64 per name + RWMutex on the
  map; steady-state increments are mutex-free. Periodic Run flushes
  via the project logger every 60s and one final flush on ctx done.
  Empty flush is silent (no-noise default).
- Dispatcher instrumented: every command invocation calls
  metrics.IncCommand; every handler error calls
  metrics.IncError("handler-error"). Logger keeps the full error
  detail; counters keep the rate.
- cmd/server/main.go: go metrics.Run(rootCtx) so the flush loop
  cancels with SIGTERM and emits the trailing window before exit.

Test coverage: 12 new tests (7 metrics, 3 middleware, 2 default-
registry round-trip). go test -race -count=1 ./... clean
(20 packages); golangci-lint clean.

Soak / cold-start measurement / log-based metrics setup deferred to
post-deployment (Phase 01 prerequisite).
2026-05-09 17:30:15 +07:00
tiennm99 4bcd8ed7cf ci: bump golangci-lint v2.2.2 -> v2.12.2 for Go 1.25 support
v2.2.2 was built with go1.24; .golangci.yml's 'go: 1.25' target
fails its config-load check. v2.12.x is built against go1.25.
2026-05-09 17:19:49 +07:00
tiennm99 3cfb0366c3 ci: bump golangci-lint-action to v7; drop firestore emulator
- golangci-lint-action@v6 doesn't support golangci-lint v2.x; v7 does.
  This was the only thing breaking CI; subsequent steps were skipped.
- Firestore emulator step removed for now — storage_test.go skips when
  FIRESTORE_EMULATOR_HOST is unset, and the gcloud emulator install
  adds 30-60s of CI time for tests not yet on the merge-gating path.
  Re-add when storage-layer changes need emulator coverage.
2026-05-09 17:18:36 +07:00
tiennm99 be13a597c7 feat(modules): port lolschedule (5 commands; daily-push cron deferred)
Phase 6e (final sub-phase of port-plan Phase 06). LoL esports match
schedule via lolesports.com's persisted API.

- internal/modules/lolschedule:
  - api_client.go: HTTP client with cache-first lookup (120s fresh
    window, 60-min stale fallback). Cache record shape matches JS so
    cross-runtime KV migration round-trips.
  - parse_date.go: ICT-anchored date parser. Accepts dd-mm-yyyy,
    dd/mm/yyyy, ddmmyyyy; trailing month/year may be omitted (default
    to current ICT month/year). Rejects impossible dates (Apr 31, Feb
    29 in non-leap, etc.).
  - format.go: Today (grouped by league) and Week (grouped by league
    -> day) renderers. Major-league filter (LCK/LPL/LEC/LCS/Worlds/
    MSI/etc.) keeps replies under Telegram's 4096-char limit. All
    user-influenced strings HTML-escaped.
  - subscribers.go: Idempotent add/remove/list keyed by chat id.
  - handlers.go: 5 commands (`/lolschedule [date]`,
    `/lolschedule_today`, `/lolschedule_week`,
    `/lolschedule_subscribe`, `/lolschedule_unsubscribe`).
- 22 tests across api-client (cache hit / miss / stale fallback /
  hard fail / show filter / non-JSON), parse-date (full and
  short formats, defaults, rejections, ICT anchor), format (event
  line states, league ordering, week grouping, HTML escape, major
  filter), subscribers (idempotent add/remove), handlers (HTML
  reply, error path, subscribe/unsubscribe round-trip).

Daily-push cron deferred to Phase 09 (Cloud Scheduler). Subscribers
are still collected so the push lights up the moment the cron infra
lands. Deps doesn't currently expose a *bot.Bot reference; that is
the prerequisite that Phase 09 will solve.

go test -race -count=1 ./... clean (19 packages); golangci-lint clean.
2026-05-09 17:14:01 +07:00
tiennm99 d7ccafafb3 feat(modules): port loldle-splash
Phase 6d of the go-port-cloud-run plan. Adds the fourth loldle
variant — guess the champion from a splash art (any skin, including
non-Default).

- internal/modules/loldlesplash: champions.go (embed splashes.json,
  10557-line DDragon-sourced pool with SplashChampion + Skin types),
  state.go (gameState gains a `skinId` field so the same splash
  shows across guesses; default 4 guesses — splash is harder than
  ability since non-Default skins are in rotation), handlers.go
  (sendPhoto path uses the DDragon CDN splash URL via
  models.InputFileString), loldlesplash.go (Module Factory).
- Reuses internal/modules/util/chathelper and internal/champname.
- 4 commands wired: loldle_splash (public), loldle_splash_giveup
  (public), loldle_splash_stats (public), loldle_splash_setmax
  (private).
- 13 tests: lookup (embed shape + DDragon URL prefix + Default skin
  invariant), state (skinId round-trip + JS-wire-format decode +
  default 4), handlers (sendPhoto with correct URL, win, unknown,
  giveup with skin label, stats, setmax owner + non-owner).

go test -race -count=1 ./... clean (18 packages); golangci-lint
clean.
2026-05-09 17:00:24 +07:00
tiennm99 08e5a6d7fa feat(modules): port loldle-ability
Phase 6c of the go-port-cloud-run plan. Adds the third loldle variant
— guess the champion from a single ability icon (passive or Q/W/E/R).

- internal/modules/loldleability: champions.go (embed
  abilities.json, 5334-line DDragon-sourced pool with
  AbilityChampion + Ability types), state.go (gameState gains a
  `slot` field so the same icon shows across guesses in a round),
  render-free handlers.go (sendPhoto path uses
  models.InputFileString with the DDragon CDN URL directly — no
  binary upload), loldleability.go (Module Factory).
- Reuses internal/modules/util/chathelper and internal/champname
  (same shared layer the other variants use).
- 4 commands wired: loldle_ability (public), loldle_ability_giveup
  (public), loldle_ability_stats (public), loldle_ability_setmax
  (private).
- 14 tests: lookup (embed shape + DDragon URL prefix + slot
  coverage), state (slot round-trip + JS-wire-format decode +
  streak), handlers (sendPhoto with correct URL, win, unknown
  champion, duplicate, giveup with slot label, stats, setmax owner
  + non-owner).
- gocyclo cap nudged 20 -> 22 to accommodate handleAbility's
  pre-flight validation branch.

go test -race -count=1 ./... clean (17 packages); golangci-lint
clean.
2026-05-09 16:53:26 +07:00
tiennm99 ec890ef0f4 feat(modules): port loldle-quote
Phase 6b of the go-port-cloud-run plan. Same shape as loldle-emoji but
with quote-pool data and default 6 guesses (vs emoji's 5).

- internal/modules/loldlequote: champions.go (embed quotes.json),
  state.go (game/stats/config persistence), render.go (italic clue +
  guess list, HTML-escaped), handlers.go (handleQuote / handleGiveup /
  handleStats / handleSetMax), loldlequote.go (Module Factory).
- Reuses internal/modules/util/chathelper and internal/champname so
  the new module adds no helper duplication.
- 4 commands wired in cmd/server/main.go: loldle_quote (public),
  loldle_quote_giveup (public), loldle_quote_stats (public),
  loldle_quote_setmax (private).
- 17 tests: lookup (embed + redaction-marker check), render
  (escapes), state (round-trip + JS-wire-format decode + streak
  sequence), handlers (no-arg / win / unknown / duplicate / giveup /
  stats / setmax owner+nonowner).

go test -race -count=1 ./... clean across all 16 packages;
golangci-lint clean.
2026-05-09 16:45:51 +07:00
tiennm99 4046d6af42 chore(plans): mark fix-all-review-findings plan completed 2026-05-09 16:33:43 +07:00
tiennm99 bdbcc43f85 chore(tooling): golangci-lint + govulncheck + defensive guards
Phase 6 of the 2026-05-09 review remediation plan. Bundle of small
hygiene fixes — none individually urgent but better folded together
than scattered across follow-ups.

- .golangci.yml: enable errcheck/govet/gosec/staticcheck/unused/
  ineffassign/gocyclo/misspell/revive. Tuned to the codebase style
  (no universal exported-doc requirement, gocyclo cap at 20 to
  accommodate handler dispatch). 0 issues across the tree.
- ci.yml: add golangci-lint job + govulncheck (informational).
- Defensive guards:
  - registry.go: Module.Name mismatch now errors at Build instead of
    silently overwriting (TestBuild_RejectsFactoryNameMismatch).
  - cmd/server/main.go: PORT env validated numerically + 0..65535.
  - firestore_provider.go: For() re-validates module name; invalid
    names return an invalidStore whose every op errors with
    ErrInvalidModuleName.
- Dead code removal:
  - wordle: gameTTLSeconds const + pickDaily/hashDJB2/todayUTC
    helpers + their tests deleted (pickDaily was unused;
    daily.go renamed pick_random.go).
- Dependency: golang.org/x/net v0.52.0 -> v0.54.0 (resolves
  GO-2026-4918 HTTP/2 infinite-loop CVE).
- Deferred from the original phase plan: Docker digest pinning
  (Dependabot handles), per-handler file splits (largest file 279 LOC;
  splits would churn for marginal gain).

go test -race -count=1 ./... clean (15 packages); golangci-lint run
clean (0 issues).
2026-05-09 16:33:21 +07:00
tiennm99 765cce549d test(handlers): integration tests + recording bot + emulator on CI
Phase 5 of the 2026-05-09 review remediation plan. Closes the
handler-layer test gap (5 modules at 0% coverage in the audit) and
ends the storage-package's CI t.Skip on Firestore emulator tests.

- internal/testutil: Update fixture builders (NewPrivateMessage,
  NewGroupMessage, NewSupergroupMessage, NewChannelMessage) plus a
  RecordingBot that wraps the real go-telegram/bot.Bot with an
  httptest server. The bot library hits the test server instead of
  Telegram; multipart form fields are captured per call. Tests assert
  on Sent() / LastSent() / AssertSentText().
- Handler tests added: misc (4), util (7), wordle (10), loldle (9),
  loldleemoji (8). Cover happy paths, error paths, auth gates,
  group-vs-private subject keying, KV side effects.
- Coverage 44.7% -> 69.8% (verified via -coverprofile). All packages
  now report coverage in CI output.
- CI: ci.yml installs cloud-firestore-emulator beta component and
  starts it on localhost:8090 before go test. Sets
  FIRESTORE_EMULATOR_HOST + GOOGLE_CLOUD_PROJECT env so the storage
  package's emulator-gated tests execute instead of skipping.

go test -race -count=1 ./... clean across all 15 packages locally.
2026-05-09 16:19:38 +07:00
tiennm99 9abade4ce0 feat(log): structured slog.JSONHandler + 22-site rewire
Phase 4 of the 2026-05-09 review remediation plan.

- internal/log: thin facade over log/slog.NewJSONHandler writing to
  stdout. Cloud Run's Cloud Logging integration auto-parses level,
  time, msg fields. Honours LOG_LEVEL env (debug|info|warn|error).
  Re-exports Info/Warn/Error/Fatal/Debug/With ergonomics.
- Migrated all 22 stdlib log call sites: cmd/server/main.go (17),
  internal/server/router.go (2), internal/modules/dispatcher.go (1),
  internal/telegram/webhook.go (1), internal/modules/misc/misc.go (1).
  Format-string args replaced with structured key/value attrs.
- Closes log-injection class (J3 from security audit) — slog escapes
  newlines and quotes inside field values, so attacker-controlled
  strings cannot synthesise fake log records (test:
  TestNewlineEscaping_NoLogInjection).

go test -race -count=1 ./... clean across all 13 packages. Zero
stdlib log imports remain outside internal/log.
2026-05-09 16:01:00 +07:00
tiennm99 80ef664473 refactor(modules): extract shared chathelper + champname packages
Phase 3 of the 2026-05-09 review remediation plan. Eliminates the
helper drift (subjectFor / argAfterCommand / nowMillis / reply /
replyHTML / winRate) that previously lived in 3-4 modules, plus the
loldle-specific normalize / findChampion / findByExactName.

- internal/modules/util/chathelper: SubjectFor, ArgAfterCommand,
  NowMillis, Reply, ReplyHTML, WinRate. Single canonical SubjectFor
  shape (group/supergroup -> chat ID, else user ID); WinRate uses
  math.Round to match JS Math.round (the truncation drift caught in
  Phase 5b/5c).
- internal/champname: Normalize + generic Find[T] / FindByExactName[T]
  with name-extractor closure. Loldle and loldle-emoji both consume
  via Champion / EmojiChampion.

Migrations: wordle, loldle, loldle-emoji, misc, util/info,
util/stickerid. Module-local lookup.go + normalize.go in loldle and
loldle-emoji deleted.

go test -race -count=1 ./... clean across all 12 packages. Net ~290
lines removed across handler files.
2026-05-09 15:52:40 +07:00
tiennm99 939c5fedab feat(server): high-priority hardening + critical blockers
Phase 1+2 of the 2026-05-09 review remediation plan:

- Go-version alignment (Dockerfile/go.mod) + 4 nil-deref guards + CI
  docker-build step (Phase 1, 63482c4 carried over).
- Env allowlist: secretEnvKeys denylist replaced; modules opt-in via
  RequiredEnv. Future API keys do not auto-leak.
- Visibility enforcement: dispatcher gates Private/Protected commands
  via BOT_OWNER_ID / ADMIN_USER_IDS; non-permitted callers are silently
  denied.
- Panic recovery in webhook handler; logs runtime/debug.Stack and
  returns 200 to prevent Telegram retry storm.
- Cron timeout reduced 5m -> 60s.
- MaxBytesError handled separately from generic decode errors so 413
  from MaxBytesReader is not shadowed by a 400.
- Emoji clue HTML-escaped defensively in loldle-emoji renderer.
- Tests added for dispatcher Auth.Permits + webhook panic recovery.
2026-05-09 15:52:15 +07:00
tiennm99 980126ea28 chore(plans): mark Phase 6a partial; log code-review findings
Update phase status to reflect loldle-emoji port completion. Document
code-review findings in dedicated report, noting per-subject keylock
strategy and JS-wire-format decode verification as key validation gates.
2026-05-09 12:19:27 +07:00
tiennm99 0b2914fe34 feat(modules): port loldle-emoji
Phase 6a of go-port-cloud-run; first of 5 sub-cooks for Phase 6 loldle
variants. Implements binary right/wrong scoring (no attribute compare).
Per-subject keylock and math/rand.Intn applied from the start, lessons
from prior phase reviews. JS-wire-format decode test added per
code-review concern F#1, locking the migration contract. Helpers
(normalize/subjectFor/argAfterCommand) duplicated from classic loldle;
extraction earmarked for 6b prep.
2026-05-09 12:19:22 +07:00
tiennm99 79fc690ed3 refactor(modules): allow hyphens in module names
Relax module name regex to accept hyphens, preparing for hyphenated
loldle variants (loldle-emoji, loldle-quote, etc.) ported from upstream
JS sources. Storage prefix delimiter ':' remains rejected. Telegram
command names use separate stricter regex (commandNameRe) and are
unaffected.
2026-05-09 12:19:16 +07:00
tiennm99 e99253fc2b chore(plans): mark Phase 5 done; log review findings
Completed Phase 5c: loldle module ported with full comparison engine,
keylock extracted for module reuse, and winRate rounding aligned with JS
behavior. Code review flagged and fixed render-alignment golden tests.
Updated phase status and plan progress tracking.
2026-05-09 09:57:00 +07:00
tiennm99 479a6ea9ea feat(modules): port loldle classic + fix winRate truncation
Ported loldle game module with full classic-mode comparison engine:
4 commands (3 public + /loldle_setmax private), 7-attribute comparison
(gender/species/range_type/resource/regions/positions/release_date) with
exact/multi/year scoring, 172-champion dictionary, and sticker pools by
outcome.

Fixed winRate display discrepancy: JS uses Math.round but Go was using
int(...) truncation. Applied math.Round in both loldle and wordle
handlers. Rendered output now matches expected percentages (e.g. 67%
instead of 66%).

Includes comparison/lookup/flavor/state/render golden tests, keylock
fan-out tests, and strict render-alignment validation.
2026-05-09 09:56:54 +07:00
tiennm99 676c481def refactor(modules): extract per-subject lock to internal/keylock
Extracted per-subject RWMutex primitive into dedicated internal/keylock
package to allow reuse across game modules. Wordle previously held local
subjectLocks; loldle module (Phase 5c) would duplicate this type without
the extraction. Placing keylock as a peer to storage/telegram (not nested
under modules/) matches its cross-module scope.
2026-05-09 09:56:47 +07:00
tiennm99 8c62d73895 build: claude-code project settings + ignore agent runtime state
Added .claude/settings.json with git push authorization.
Updated .gitignore to exclude .claude/agent-memory/ from tracking.
2026-05-09 09:35:32 +07:00
tiennm99 fda1510e6e chore(plans): mark Phase 5b done, log review findings
Updated phase-05-port-simple-modules.md with completion status and
linked code-reviewer report documenting the two bugs fixed during
implementation (defaultRNG race, Get→mutate→Put logical race).
Updated plan.md to reflect Phase 5b completion in roadmap.
2026-05-09 09:35:28 +07:00
tiennm99 5980897475 feat(modules): port wordle module + per-subject locking
Phase 5b of go-port-cloud-run plan. Port 14855-word dictionary
(89 KB, byte-identical to JS source) and four wordle commands
(/wordle, /wordle_new, /wordle_giveup, /wordle_stats).

KV wire-format parity: GameState/Stats JSON match JS shape;
*int64 LastResultAt for null-value compatibility. Two real bugs
caught and fixed: (1) defaultRNG data race in handlers — switched
to math/rand.Intn (mutex-protected package-level); (2) Get→mutate→Put
logical race in groups — added per-subject sync.Mutex map to serialize
access. TTL deferred (Firestore has no expirationTtl equiv — Phase 11 GC).
2026-05-09 09:35:23 +07:00
tiennm99 25f06eaaf2 chore(plans): mark Phase 5a partial, log review findings
Update phase-05 status to reflect partial completion: util and misc modules
ported with tests passing, code-review feedback incorporated. Add code-reviewer
report documenting nil-deref and KV wire-format fixes. Update plan.md to track
progress: Phase 5a done (4/4 todos), Phase 5b (wordle/loldle) pending.
2026-05-09 08:25:01 +07:00
tiennm99 45a57e1573 feat(modules): port util + misc; expose Registry to handlers
Phase 5a of go-port-cloud-run plan: port first 2 of 4 modules (wordle/loldle
deferred to later phase). Port util.go, info.go, help.go, stickerid.go and
misc.go with tests. /help renders registry view; /info exposes chat/thread/
sender ids; /stickerid (private) returns bot-scoped file_ids; /ping writes
last_ping KV ms-epoch JSON for byte-parity, /mstats reads it, /fortytwo is
easter egg.

Registry-pointer-in-Deps required for /help to access module registry—pointer
captured at factory time, stable post-Build. Static factory catalog moved from
modules pkg to cmd/server to break import cycle. Code-review fixes applied in
same session: /info nil-deref guard, KV wire-format parity.
2026-05-09 08:24:52 +07:00
tiennm99 c7a631bffd chore(plans): mark Phase 04 done + record review findings
Updated phase-04-firestore-kv.md to reflect completion with all success
criteria validated. Updated plan.md to mark Phase 04 status as complete.
Recorded code-reviewer findings documenting security hardening, abstraction
quality, and emulator test coverage from Phase 04 implementation review.
2026-05-08 23:51:32 +07:00
tiennm99 1f86f3df12 feat(storage): Firestore KVStore + KVProvider abstraction
Phase 04 of go-port-cloud-run plan. Introduces KVProvider abstraction
with memory backend (via Prefixed wrapper) and Firestore backend (via
collection-per-module isolation). Backend selection gated by env vars:
GOOGLE_CLOUD_PROJECT or FIRESTORE_EMULATOR_HOST → Firestore, else memory.
Emulator-gated tests via `make test-emulator`. Security hardened: emulator
fallback project ID, prefix validation on List, length-in-bytes docs.
2026-05-08 23:51:24 +07:00
tiennm99 067abb72a1 chore(plans): update phase 02-03 status and record code review findings
Phase 02 (repo bootstrap, partial) and Phase 03 (module framework)
completed and reviewed. Cloud Run deployment deferred to Phase 01.
Updates plan progress, phase completion notes, and code review report
documenting security hardening and test coverage decisions.
2026-05-08 23:27:21 +07:00
tiennm99 35ad4cb997 feat(server,modules): bootstrap server and module framework
Implements Phases 02 (partial) and 03 of the go-port-cloud-run plan.
Introduces module framework with per-module KV prefix isolation,
health check endpoint, request timeout protection, and comprehensive
test coverage. Cloud Run deployment deferred to Phase 01.

Security hardening: constant-time secret comparison, cron auth bridge,
and secrets stripped from dependency environment exports. Includes
Dockerfile, GitHub CI workflow (vet + race + build), and integration
tests for module lifecycle.
2026-05-08 23:27:12 +07:00
tiennm99 5d27cd8356 chore: switch to Apache-2.0 license; import plans from upstream miti99bot 2026-05-08 22:48:34 +07:00
tiennm99andGitHub ab617ade63 Initial commit 2026-05-08 22:44:45 +07:00
tiennm99 9eb1c527af Clear and rewrite
Retire the JavaScript/Cloudflare implementation; history continues from the Go rewrite (miti99bot).
2026-08-04 23:21:19 +07:00
tiennm99 46e073d607 chore(plans): migrate to miti99bot-go repo 2026-05-08 22:51:36 +07:00
tiennm99 b2616155dd feat(lolschedule): add /lolschedule [date] command
Accepts dd-mm-yyyy, dd/mm/yyyy, ddmmyyyy with trailing month/year
optional. Empty defaults to today's ICT day. Rejects month/year
without day, invalid ranges, and impossible dates.
2026-05-08 14:42:59 +07:00
tiennm99 d6af89efe7 revert(loldle): restore default max guesses to pre-tuning values
Roll classic loldle back to 8 (from 6) and emoji to 5 (from 4). The
/<module>_setmax override command stays — chats that want tighter limits
can opt in instead of having defaults forced on them.
2026-05-07 15:06:58 +07:00
tiennm99 293c6adfde chore(repo): ignore .claude/ local agent state
The directory holds machine-local memory and session-scoped permission
overrides; never belongs in the tree.
2026-05-07 15:06:58 +07:00
tiennm99 b52fa7094e feat(loldle): tunable max guesses per subject + retuned defaults
Drop classic loldle from 8 → 6 (7-axis grid leaks too much per guess for 8
to feel earned) and emoji from 5 → 4 (3 emojis are usually unmistakable).
Add a hidden /<module>_setmax <n> command per loldle module so a chat can
override its own round length (1-10). Override stored at config:<subject>
in each module's KV; getMaxGuesses() falls back to the default when unset.
2026-05-07 14:10:01 +07:00
tiennm99 5782e4779e revert(loldle): restore default max guesses to pre-tuning values
Roll classic loldle back to 8 (from 6) and emoji to 5 (from 4). The
/<module>_setmax override command stays — chats that want tighter limits
can opt in instead of having defaults forced on them.
2026-05-07 15:06:25 +07:00
tiennm99 14e5668c96 chore(repo): ignore .claude/ local agent state
The directory holds machine-local memory and session-scoped permission
overrides; never belongs in the tree.
2026-05-07 15:06:23 +07:00
tiennm99 e03d2a937c feat(loldle): tunable max guesses per subject + retuned defaults
Drop classic loldle from 8 → 6 (7-axis grid leaks too much per guess for 8
to feel earned) and emoji from 5 → 4 (3 emojis are usually unmistakable).
Add a hidden /<module>_setmax <n> command per loldle module so a chat can
override its own round length (1-10). Override stored at config:<subject>
in each module's KV; getMaxGuesses() falls back to the default when unset.
2026-05-07 13:52:09 +07:00
tiennm99 59a0b76f91 chore(plans): drop orphan reports tied to no active plan
Removed reports that documented work already shipped and not tied to
any archived plan dir:

- docs-manager-260420-2151-documentation-audit.md (one-off doc audit)
- researcher-260421-0845-leaguepedia-api-verification.md (lolschedule research)
- researcher-260421-0909-leaguepedia-auth-token.md (lolschedule research)

The lolschedule module is in src/modules/lolschedule/ but never had a
discrete plan dir. Findings from the leaguepedia reports are reflected
in the live module code; the markdown is no longer load-bearing.

plans/reports/ now contains only the 6 Atlas migration reports for the
active plan.
2026-04-26 10:24:44 +07:00
tiennm99 3a310dbf06 chore(plans): co-locate reports with their archived plans
Six research reports were sitting in plans/reports/ but tied to
already-archived plans. Move each under the matching archived plan
dir so the archive is self-contained and plans/reports/ only holds
reports for in-flight or unarchived work.

Moves
- researcher-260422-2329-semantle-api-alternatives.md
- researcher-260423-0025-bge-m3-cosine-calibration.md
- researcher-260423-1110-vietnamese-embeddings-semantle.md
  → plans/archive/260422-2128-semantle-module/reports/

- researcher-260424-2215-loldle-ability-splash-modes.md
- researcher-260424-2215-loldle-emoji-and-modes-overview.md
- researcher-260424-2215-loldle-quote-mode.md
  → plans/archive/260424-2215-loldle-new-modes/reports/ (new dir)

Stays in plans/reports/
- 6 Atlas migration reports (active plan)
- docs-manager-260420-2151-documentation-audit.md (general audit, no
  discrete plan home)
- researcher-260421-0845-leaguepedia-api-verification.md
- researcher-260421-0909-leaguepedia-auth-token.md
  (lolschedule research; no archived lolschedule plan exists)

No code changes.
2026-04-26 10:13:43 +07:00
tiennm99 5cb1a812d4 chore(plans): archive completed plans + mark atlas migration code-complete
Three feature plans (semantle, twentyq, loldle-new-modes) are
status:completed in their frontmatter and the corresponding modules
exist in src/modules/. Move them to plans/archive/ to keep the active
plans/ dir focused on in-flight work.

Atlas migration (260425-1945-mongodb-atlas-migration/plan.md): bump
status from `planning` to `code-complete` and annotate each phase row
with its commit SHA + whether operator action is still pending. Plan
stays in active plans/ until cutover lands or the Upstash standby
(phase-07-alt-pivot.md) executes.

No code changes. Tests, lint, register:dry unaffected (733 passing).

Operator-facing summary in the plan.md status note: 8 phases of
implementation are committed on dev (6f0b5ff..e2e3112). Outstanding
operator work: Atlas provisioning, real-cluster smoke tests,
backfill runs, soak, cutover stages, Stage 3 code cleanup.
2026-04-26 09:56:38 +07:00
tiennm99 b660692eb2 feat(db): finalize MongoDB Atlas migration documentation
Phase 08: Complete documentation pass for MongoDB Atlas migration.

- Create docs/cost-tracking.md: Cost monitoring, upgrade triggers, monthly checklist
- Create docs/project-changelog.md: Full migration summary with phase breakdown
- Update docs/architecture.md section 8: Describe dual-write era, MongoDB store layers
- Update docs/code-standards.md: Add Persistence section for storage factory patterns
- Update docs/codebase-summary.md: Reflect MongoDB as primary, update test count (733)
- Update README.md: Storage section now describes MongoDB + dual-write during migration
- Update CLAUDE.md: Architecture section references MongoDB instead of KV/D1
- Update tests/fakes/fake-mongo.js: Document frozen surface (Phase 02-08 API)

Verified:
- All 733 tests passing
- Lint + secret-leak check pass
- npm run register:dry succeeds
- Auto-pause concern satisfied: trading (17:00), lolschedule (01:00), drift-verifier (hourly) all write to Mongo
- Roadmap verified migration NOT listed (future-only per user feedback)

Post-Phase-07 cutover: dual-write collapses, KV/D1 deleted, MongoDB becomes sole backend.
2026-04-26 09:34:25 +07:00
tiennm99 54f4c28807 feat(scripts): phase 07 — reverse-backfill scripts + delete guard
Pre-execution prerequisites for the Phase 07 cutover. Stage 2 of the
cutover keeps DUAL_WRITE=0 for ~6 days; if anything regresses during
that window the operator MUST be able to roll back to KV/D1 with the
last N days of Mongo-only writes recovered. Pre-building these scripts
(per code-reviewer #4) eliminates "draft a backfill under outage
pressure" — the anti-pattern of writing untested code at 4am.

Reverse-backfill
- scripts/backfill-mongo-to-kv.js: full-scan Mongo collection per module,
  PUT each doc back to CF KV via REST. expiresAt → expirationTtl (clamped
  to 60s minimum per CF KV); already-expired docs are skipped (won't
  resurrect dead state). 50 ops/sec throttle. --dry-run + --module flags.
- scripts/backfill-mongo-to-d1.js: full-scan trading_trades, build INSERT
  SQL preserving legacy_id where present (round-trips D1 autoincrement IDs
  preserved by phase-05 forward backfill). Sequential int generation for
  any docs without legacy_id. Pipes through wrangler d1 execute.
- scripts/lib/migration-helpers.js: cfKvPut helper added.

Delete guard (debugger #12)
- scripts/wrangler-delete-guard.sh: interactive CONFIRM wrapper around
  wrangler kv namespace delete + wrangler d1 delete. Exits 3 when stdin
  is not a tty so it cannot run in CI. Documented: never run in CI.

package.json: backfill:mongo:kv[:dry] + backfill:mongo:d1[:dry] scripts
wired.

Tests: 697 → 733 (+36).
- 7 cfKvPut tests (REST URL, querystring, body, expiration_ttl param).
- 10 reverse-KV TTL math tests (expired sentinel, future seconds, no-TTL,
  CF 60s minimum clamp).
- 9 reverse-D1 SQL construction tests (escaping, legacy_id preservation,
  sequential generation).

Lint clean. No Worker code touched. Stage 1 cutover, 7-day soak,
snapshots, and Stage 3 cleanup (delete CFKVStore + simplify factories +
edit package.json deploy chain) remain operator-driven and will be
committed separately after binding deletion.
2026-04-26 09:29:14 +07:00
tiennm99 439e874a11 feat(observability): phase 06 — timing telemetry + soak analyzer + burst tester
Code prerequisites for the Phase 06 cold-start soak gate. The 24-72h soak
itself is operator-run; this commit ships the instrumentation + analysis
tools needed to make the PROCEED-or-PIVOT decision.

Telemetry
- src/util/timing.js: startTiming(cmd) returns {mark, end} that emits a
  structured cmd_timing log. takeColdFlag() returns {cold, isolateAgeMs}
  using a module-scoped boolean — first request in an isolate is cold,
  subsequent are warm. This replaces the originally-planned
  isolate_age_ms < 200ms classifier (broken because Mongo cold-connect
  itself is ~1500ms; cold requests would always bucket as warm —
  code-reviewer #11).
- src/util/request-context.js: setLastCold/getLastCold shared state
  bridges fetch-level cold detection into the dispatcher middleware
  without a circular import.
- src/index.js: takeColdFlag at the top of fetch() emits a request log
  and primes the request context for the dispatcher.
- src/modules/dispatcher.js: bot.use() middleware times every command.
  Chosen over per-handler wrapping to preserve the existing identity
  assertion in tests (handler === reg.allCommands.get(name).cmd.handler)
  — single instrumentation point, no contract change.

Soak tools (operator-run)
- scripts/analyze-soak.js: parses CF Logs export (NDJSON or CSV), filters
  cmd_timing events, computes p50/p95/p99 per (cmd, cold/warm). Counts
  dual-write secondary failures, mongo connection errors, CPU-time
  exceeded events. Writes markdown report.
- scripts/synthetic-burst.js: fires N parallel synthetic Telegram updates
  at the deployed Worker URL with cache-busting tokens. Used for the
  pre-deploy connection-cap stress test (debugger #2 — 20 parallel cold
  requests, abort if Atlas peak > 60% of 500-conn cap).
- package.json: analyze:soak + burst:synthetic scripts wired.

Tests
- tests/util/timing.test.js: 8 tests — timing semantics, cold flag flip.
- tests/scripts/analyze-soak.test.js: 22 tests — percentile math, NDJSON
  + CSV parse, aggregation, markdown formatting.

Tests: 667 → 697 (+30). Lint clean.

Operator runbook for Phase 06 (NOT executed by this commit):
1. Verify telemetry live via wrangler tail.
2. Run synthetic burst test: npm run burst:synthetic -- --url <prod>
3. Configure Atlas + CF Observability email alerts.
4. 24h soak (extend to 72h on stop-conditions per phase plan).
5. Daily npm run verify:mongo.
6. npm run analyze:soak -- --input <cf-logs.json> → soak-decision.md.
7. PROCEED to Phase 07 if cold-start P95 ≤ 2.5 × BASELINE_COLD_PING_MS;
   else execute phase-07-alt-pivot.md (Upstash standby).
2026-04-26 09:22:04 +07:00
tiennm99 afe7d768b7 feat(scripts): phase 05 — backfill + verify + wipe (local node, no admin routes)
Operator-run migration scripts for KV→Mongo and D1→trading_trades, plus a
parity verifier and a rollback wiper. Pure local Node — no Worker code,
no /__admin/* routes, no new Worker secrets. Complies with
docs/architecture.md §10.

Scripts
- backfill-kv-to-mongo.js: paginates CF KV REST API per module, fetches
  values, $setOnInsert upsert into per-module Mongo collection. Resumes
  from .backfill-cursor-<module>.json on restart. Throttles 50 ops/sec.
  expiresAt derived from KV metadata.expiration (debugger #10). --dry-run
  and --module flags for incremental work.
- backfill-d1-to-mongo.js: wrangler d1 execute --remote --json → parse →
  insertMany batches into trading_trades, preserving original integer id
  as legacy_id (code-reviewer #13). Pre-flight aborts if collection
  non-empty unless --force.
- verify-mongo-parity.js: count parity ±1%, SHA256 value compare,
  expiresAt ±5min bucket. Full-scan when <10K docs, sqrt-sample
  capped at 500 otherwise (code-reviewer #21). Trading: full-scan
  on legacy_id/ts/user_id/symbol/qty.
- wipe-mongo.js: rollback helper. deleteMany across all collections
  with readline confirm. --yes for CI.
- lib/migration-helpers.js: shared sleep, sha256, checkpoint I/O,
  cfKvList/cfKvGet, MongoClient singleton, sample strategy.

Surface updates
- .env.deploy.example: CF account/token/namespace placeholders.
- package.json: backfill:kv[:dry], backfill:d1[:dry], verify:mongo,
  wipe:mongo scripts.
- check-secret-leaks.js: SECRETS array gains CLOUDFLARE_API_TOKEN +
  CLOUDFLARE_ACCOUNT_ID for defense-in-depth.
- .gitignore: .backfill-cursor-*.json excluded.

Tests: 638 → 667 (+29 pure-logic tests for sha256, checkpoint round-trip,
count-diff, sample-size, fetch-mocked CF REST). Lint clean.

Operator-run sequence (after Phase 06 deploy):
  npm run backfill:kv:dry   # preview
  npm run backfill:kv
  npm run backfill:d1:dry
  npm run backfill:d1
  npm run verify:mongo      # exit 0 = parity ok
2026-04-26 09:13:00 +07:00