Commit Graph
10894 Commits
Author SHA1 Message Date
adminandGitHub ceed22d6de Prioritize remaining small Eagle alpha work (#6795) 2026-05-29 08:31:41 -07:00
adminandGitHub 75f35e981e Unpin Bazel workflows from specific runners (#6792) 2026-05-29 08:29:29 -07:00
adminandGitHub 76d8f07827 Keep command factory test flatbuffers alive (#6794) 2026-05-29 08:23:27 -07:00
adminandGitHub 705a36e901 Update Unity 6000.4.9 project files (#6793) 2026-05-29 08:21:19 -07:00
adminandGitHub 758ae6d44f Ensure Bazel is available in self-hosted workflows (#6785)
* Ensure Bazel is available in self-hosted workflows

* Allow Bazel CI to use Command Line Tools

* Set macOS C++ deployment target for Bazel

* Use Bazel macOS minimum OS flags

* Raise macOS Bazel deployment target for filesystem

* Ensure git-lfs is available for CI fetches

* Authenticate CI LFS fetches with workflow token

* Disable stale LFS hooks during CI checkout

* Disable LFS filters during CI checkout

* Prepare Git LFS before persistent checkout

* Harden generated Bazel rc and LFS auth

* Let Xcode sync skip without full Xcode

* Harden Unity build cache markers

* Require full Xcode for mactools sync
2026-05-29 07:52:17 -07:00
899162bbc5 Phase 4.5i: delete v15-v20 payload-parsing backfills (#6791)
After #6787 (upload-on-load) deployed and the admin-load pass pushed
every persisted game.db to v ≥ 21 in S3, the v15-v20 migration
backfills that re-derived their new columns/tables from
`action_results.payload` are unreachable on any healthy game. This PR
deletes that code so the proto dependency surface shrinks and the
"only used by backfill" file headers stop misleading future readers.

Removed:
- `backfill(Connection)` (and supporting `bindOpt*` helpers / `select`
  loops) from each of the 6 writers that populated the 4.5h.1–4
  scalars/vectors:
    - ActionResultEntityRefColumnsWriter (v15)
    - ActionResultEnumColumnsWriter (v16)
    - ActionResultNumericColumnsWriter (v17)
    - ActionResultDateBattleColumnsWriter (v18)
    - ActionResultStructuredBlobColumnsWriter (v19)
    - ActionResultVectorTablesWriter (v20)
- `applyV15`–`applyV20` collapse to a single line each — just
  `execEach(connection, ...schemaStatements)`. Doc note added that
  schema v < 15 is no longer supported (if `applyV15` ever runs on a
  non-empty `action_results` table, the new columns are left NULL and
  state diverges silently; the migration runner's `schema_version`
  gate is the only protection).
- 6 corresponding `"action_results vN backfill"` tests in
  `SqliteHistoryTest` (one per migration). The matching `vN live
  write` tests stay — those still exercise the live-write column
  binding.

BUILD dep cleanup falls out: each of those writers no longer needs
`internal:action_result_scala_proto` or
`proto_converters:action_result_proto_converter` (the
payload-parse + full-converter round-trip is gone). The remaining
narrow deps (`tutorial_phase_scala_proto`, `game_type_scala_proto`,
etc.) cover the proto-enum `.value` reads that `bindColumns` still
needs.

Verification: `sqlite_history_test` 90/90 green (96 - 6 backfill
tests deleted); full Scala suite 226/226 green.

The per-entity table backfills (v3-v14) still parse `payload` and are
out of scope here — they kept ChangedHero/Faction/Battalion games
alive across the .5c cutover, and removing them needs the same
"prove all games are at v ≥ 21 in S3" gate. Future cleanup.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 07:51:34 -07:00
adminandGitHub d174c06ddb Reduce Unity LFS asset footprint (#6789)
* Delete unused Animal pack deluxe v2 assets

* Delete unused preview and demo LFS assets

* Normalize bridge FBX assets into LFS
2026-05-29 07:51:00 -07:00
4935b81037 Upload migrated game.db to persister on load (red+green) (#6787)
* Upload migrated game.db to persister on load (red)

Prep for 4.5i (deleting v15-v20 backfill payload-parsing). Before we
can delete migration code, every persisted `game.db` needs to be at the
current schema in S3 — otherwise a server restart re-downloads a stale
blob and re-walks the old migrations.

Today `SqliteHistory.loaded` runs migrations on the local file but
nothing pushes the migrated form back to the persister until an action
is actually committed (`withNewResults` is the only upload trigger).
Result: admin-loading every game upgrades the local server's copy but
leaves S3 at the old schema, so the upgrade unwinds on restart.

This commit adds two red tests:

  1. Loading a v1 DB out of the persister with a wiped local copy must
     bump the persister's `saveCount` — i.e., the migrated bytes get
     pushed back. Today this fails because nothing in the load path
     uploads.

  2. Loading an already-current DB must NOT bump `saveCount`. Today
     this passes vacuously (the load path never uploads), so it's a
     forward-guarantee that the fix won't spam uploads on every restart
     of a healthy server.

GREEN will thread an "any pending migrations applied?" boolean out of
`Migrations.applyPending` and, when true, do a one-shot
checkpoint+upload after constructing the `SqliteHistory`.

Holding for review of the test design before I land GREEN on the same
branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Upload migrated game.db to persister on load (green)

Thread an "any pending migrations applied?" boolean out of
`Migrations.applyPending` and through `initializeSchema`. In `loaded`,
when migrations actually ran and a persister is set, do a one-shot
checkpoint+upload so the migrated bytes propagate to S3.

Factored the WAL-checkpoint + readAllBytes + persister.save block out
of `maybeUploadToPersister` into a shared
`uploadCurrentBytesToPersister(p, nowMillis)` helper; `loaded` calls
the public no-arg variant. The `lastUploadAtMillis` field is anchored
inside the upload so the subsequent withNewResults time-based trigger
stays correct.

`create` discards the migration boolean explicitly: a fresh game has
no stale S3 form to compete with, and the first `withNewResults`
upload will be the v-current bytes anyway.

Verification: sqlite_history_test 95/95 green; full Scala suite
226/226 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 06:58:50 -07:00
503801e7c4 Add PerEntityWriteDispatcher with compile-time + runtime coverage guards (#6790)
Background. We just hit a production state-loss bug on a saved game:
`Province 40 has an army that should have already arrived`. Root cause:
`ChangedProvinceTableWriter` was added in 4.5b (#6725, 5/17) but its call
into `withNewResults` wasn't wired until 4.5h.5c (#6779, 5/28). For the
11 days between, every action committed had `payload` populated but
zero rows in `action_changed_provinces`. When 4.5h.5d (#6784) dropped
`payload`, those actions' changedProvinces became permanently lost.
On the affected game that's ~24k actions out of 28k.

ChangedHero and ChangedFaction had similar (hours-long) wiring gaps, but
v6 (#6741) included a re-backfill that rescued them. Every other
decomposition (4.5e/f/g/h.4) shipped writer + migration in a single PR,
so no orphan window. Only ChangedProvince bled data in production.

This PR adds two complementary guards so the same shape of bug can't
ship again:

1) New `PerEntityWriteDispatcher` object owns the fan-out to every
   per-entity decomposition writer. Its `writeAll` matches `ar` against
   the full `ActionResultC` case-class constructor pattern, binding
   every field (Vector ones by name, scalars as `_`). Adding any new
   field to `ActionResultC` breaks the constructor arity at compile
   time, forcing whoever adds the field to come here and decide
   "scalar → new column on `action_results`" or "vector → new
   per-entity table + writer + wiring".

2) New test ("withNewResults populates every per-entity decomposition
   table when given an ActionResult with one of each vector")
   constructs an ActionResult with a non-empty entry for every 17
   `Vector[_]` field on `ActionResultC`, runs `withNewResults`, and
   asserts every per-entity table has >= 1 row. Catches "wrote a
   writer but didn't call it" — which is the second variant of the
   same class of bug.

`SqliteHistory.withNewResults` now delegates to
`PerEntityWriteDispatcher.writeAll`. The previous inline foreach blocks
plus the `ChangedProvinceConverter` import move to the dispatcher.
Visibility of `model/action_result/changed_province/concrete` widened to
`//src/test/scala/net/eagle0/eagle/service:__pkg__` so the new test
can build the source `ChangedProvinceC` directly.

Memory updated: `changed_entity_decomposition_pitfalls.md` now lists
this as a third recurring pitfall with the guard documented.

Verification: `sqlite_history_test` 94/94 green; full Scala suite
226/226 green.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 06:53:56 -07:00
adminandGitHub 646c47259c Upgrade Unity project to 6000.4.9f1 (#6788) 2026-05-29 06:52:18 -07:00
4e411695f2 Phase 4.5h.5d (red+green): drop action_results.payload column (#6784)
* Phase 4.5h.5d (red): assert action_results.payload column is gone

Final 4.5h chunk: stop writing the `payload` blob and drop the column from
the SQLite schema. After .5c the read path no longer touches `payload`, so
it's now dead weight on every write. .5d closes that loop.

Two red tests:
  1. Fresh DB: `PRAGMA table_info(action_results)` must not include `payload`.
  2. v20 → v21 upgrade: a DB that has a populated `payload` column gets the
     column dropped on `SqliteHistory.loaded`, and a subsequent read still
     returns the action via `ActionResultDbReader`.

Both fail RED because the column is still present (v20 schema). GREEN will:
  - Stop binding `payload` in the `withNewResults` INSERT (and drop
    `ActionResultProtoConverter.toProto` from the write path).
  - Add migration v21 to `ALTER TABLE action_results DROP COLUMN payload`.

Holding for review of the test design before I land GREEN on the same branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5h.5d (green): stop writing payload and drop the column

Final step of the 4.5x decomposition. After this commit every
ActionResultC field on disk lives in a dedicated column or per-entity
table; `action_results.payload` is gone.

Changes:
- Add migration v21: ALTER TABLE action_results DROP COLUMN payload.
  SQLite >= 3.35.0 supports DROP COLUMN; the xerial sqlite-jdbc driver
  bundles a new-enough engine.
- Remove `payload` from the withNewResults INSERT (drops the bind at
  position 6 and the ActionResultProtoConverter.toProto call) and
  renumber the trailing column positions.
- Drop the ActionResultProtoConverter import from SqliteHistory.scala
  and the corresponding sqlite_history Bazel deps
  (action_result_proto_converter, action_result_scala_proto); add
  changed_province_scala_proto, which had been pulled in transitively
  via the action_result proto.
- Delete the .5b parity test and the .5c "read path doesn't touch
  payload" test. Both queried `payload` directly to make their
  assertions; with the column gone, the assertions are moot. Their
  intent — that ActionResultDbReader assembles a faithful
  ActionResultC from the decomposed tables — is now covered implicitly
  by every test that round-trips through withNewResults + last/all.

Verification: `sqlite_history_test` 93/93 green; full Scala suite
226/226 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 22:09:38 -07:00
adminandGitHub 237efb9c0c Temporarily pin self-hosted CI to halfdan (#6786) 2026-05-28 22:05:35 -07:00
0368a9f24a Phase 4.5h.5c (red+green): swap read path off action_results.payload (#6779)
* Phase 4.5h.5c (red): assert read path no longer depends on payload

Continues 4.5h.5 after .5b (the ActionResultDbReader orchestrator).
This sub-PR is the read-path cutover: swap replayFrom + readActionAt +
stateAfter to use ActionResultDbReader instead of
ActionResultProtoConverter.fromProto(payload), and wire
ChangedProvinceTableWriter into withNewResults so the cutover is
correctness-complete for changedProvinces.

The cleanest single test for "read path doesn't touch payload" is to
persist an action, then corrupt its payload column (overwrite with a
1-byte garbage blob), then re-load and read. If anything in the read
path still calls parseFrom on payload, that parse throws
InvalidProtocolBufferException; the test fails. If the read path uses
ActionResultDbReader (which doesn't touch payload), the test sees the
populated fields and passes.

The test exercises both stateAfter (which replays actions to advance
state) and readActionAt (which returns the last action) via a single
SqliteHistory.last call — both must avoid touching the corrupted
payload.

This commit (red):
- No code changes to SqliteHistory; the read path still parses payload.
- One red test: persist an action with 4 populated scalar/Option fields,
  wipe payload column to x'00', reload, call last, assert the fields
  come back populated.

The GREEN commit will:
  - Swap replayFrom + readActionAt + the stateAfter inner loop to
    ActionResultDbReader.read (no more parseFrom + fromProto).
  - Wire ChangedProvinceTableWriter into withNewResults (so live actions
    populate action_changed_provinces — needed for the read path to
    return changedProvinces correctly).
  - Note: payload is still written by withNewResults (kept around as a
    safety net for one PR); .5d drops the column entirely.

Verification: sqlite_history_test 92/93 pass, 1 RED for the right
reason (parseFrom throws "Protocol message contained an invalid tag
(zero)" on the corrupted bytes).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5h.5c (green): swap read path to ActionResultDbReader, wire ChangedProvinceTableWriter

stateAfter / replayFrom / readActionAt now go through ActionResultDbReader.read,
which assembles ActionResultC from the decomposed columns + per-entity tables
(4.5b–4.5h.4) instead of `parseFrom(payload)`. Also wires
ChangedProvinceTableWriter into withNewResults so the action_changed_provinces
table is populated by live writes (it was already populated by the 4.5b
backfill; this closes the gap for new actions). The `payload` column is still
written; it gets dropped in 4.5h.5d.

The RED test added in 6b6134afef passes now that the corrupted payload is
no longer touched on read; gave it a `newDate` so the applier (which runs
during stateAfter's replay) has a date.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:49:06 -07:00
adminandGitHub 39b8c3360e Tidy Bazel rc marker and Mac deploy runner label (#6782) 2026-05-28 20:05:11 -07:00
adminandGitHub c2b04c2109 Pass iOS TestFlight build between jobs (#6781) 2026-05-28 06:25:48 -07:00
adminandGitHub 875f34a624 Harden GitHub Actions workflows (#6780) 2026-05-27 22:04:56 -07:00
7f2cdbd111 Phase 4.5h.5b: ActionResultDbReader (#6777)
* Phase 4.5h.5b (red): ActionResultDbReader stub + parity test

Continues 4.5h.5 after .5a (the two missing per-entity readers). This
sub-PR adds the top-level orchestrator that will assemble a domain
ActionResultC from the 24 action_results columns + the eleven per-entity
table readers — without touching the legacy payload BLOB.

The 24 columns now on action_results (added by 4.5h.1, .2a, .2b, .2c,
.3) cover every top-level scalar/Option field of ActionResultC plus the
three structured-Option blobs (new_battle_proto / new_chronicle_entry_proto
/ new_eagle_map_proto). The eleven per-entity vectors are read via the
existing per-entity table readers:

  - ChangedBattalionTableReader, ChangedFactionTableReader,
    ChangedHeroTableReader, ChangedProvinceTableReader (last from .5a)
  - NewBattalionTableReader, NewFactionTableReader, NewHeroTableReader,
    NewProvinceTableReader
  - NotificationTableReader (readNew + readRemoved)
  - GeneratedTextRequestTableReader, ClientTextVisibilityExtensionTableReader
  - ActionResultVectorTablesReader (the five v20 vector tables, from .5a)

Parity contract with the converter-based read:
  - For every field both paths handle, this reader produces an
    ActionResultC equal to ActionResultProtoConverter.fromProto(payload).
  - newGeneratedTextRequests is the one intentional asymmetry: the
    converter drops it ("LLM text requests are not used by the
    applier"), this reader returns the populated vector from the
    decomposed table. Strictly better.
  - changedProvinces is excluded from this PR's parity test:
    ChangedProvinceTableWriter is not yet wired into withNewResults
    (that's a .5c concern). Adding it back to parity is straightforward
    once the wiring lands.

Reader is unused by production — replayFrom still goes through
ActionResultProtoConverter.fromProto(payload). The cutover lands in
.5c; the payload column gets dropped in .5d.

This commit (red):
- ActionResultDbReader stub: read returns a near-empty ActionResultC
  (only actionResultType = UnknownActionUnspecified). GREEN lands the
  real read.
- One parity test in SqliteHistoryTest: builds a rich source
  ActionResultC (one of each shape — scalar/Option columns, structured
  blobs, v20 vector tables, per-entity vectors except changedProvinces
  and newGeneratedTextRequests), persists via withNewResults, then
  asserts ActionResultDbReader.read produces the same ActionResultC as
  the converter-based read of the payload.
- Visibility widening:
  model/action_result/concrete:action_result_concrete adds
  service:__pkg__; model/proto_converters:action_result_proto_converter
  adds test/.../service:__pkg__.

Verification: sqlite_history_test 91/92 pass, 1 RED for the right reason
(stub returns near-empty AR; parity assertion fails the long structural
comparison).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5h.5b (green): implement ActionResultDbReader

Fills in the ActionResultDbReader stub from the red commit with the
real read implementation. The parity test added in red now passes:
ActionResultDbReader.read produces an ActionResultC equal to
ActionResultProtoConverter.fromProto(payload) for every field both
paths handle.

Implementation:
- Single action_results SELECT for the 24 column values (action_result_type,
  the 4 entity-ref columns, 4 enum columns, 4 numeric Option columns,
  date pair + resolved_battle, 3 blob columns).
- Eleven per-entity reader calls (ChangedBattalion/Faction/Hero/Province,
  NewBattalion/Faction/Hero/Province, NotificationTableReader.readNew +
  readRemoved, GeneratedTextRequest, ClientTextVisibilityExtension,
  ActionResultVectorTables — readDestroyedBattalionIds/etc).
- All thirty fields plug straight into ActionResultC's case-class
  constructor — single allocation, no .copy() (the custom copy method
  uses different parameter names like `actingFaction` vs `actingFactionId`
  and `removedHeroes` vs `removedHeroIds`).

Decoding helpers:
- getOptInt / getOptLong: nullable INTEGER via JDBC's wasNull() pattern
  (getInt returns 0 for SQL NULL, indistinguishable without wasNull).
- decodeGameType: handles the (new_game_type, new_tutorial_phase) pair.
  Writer never stores UNKNOWN_GAME_TYPE (stores NULL instead), so
  fromProto's UNKNOWN→throw path is unreachable; defensive throw on
  None from CommandTypeConverter.fromProto for the same reason.
- decodeDate: handles the (new_date_year, new_date_month) pair.
  Inconsistent (one NULL) would throw IllegalArgumentException from
  Date.Month.fromInt(0), surfacing writer bugs loudly.
- Blobs use Option(rs.getBytes(...)).map(parseFrom + Converter.fromProto).

BUILD additions (green-only):
- action_result_db_reader deps: all 11 per-entity readers + 6 enum/blob
  converters + their proto packages (chronicle_entry, command_type,
  eagle_map_info, game_type, round_phase, tutorial_phase, shardok_battle)
  + Date domain type + eagle_internal_exception.

The read path is NOT swapped here — replayFrom still parses payload.
The reader is unused by production until .5c.

Verification: sqlite_history_test 92/92 pass; full Scala test suite
226/226.

Next: 4.5h.5c — swap replayFrom to use ActionResultDbReader, stop
writing payload, wire ChangedProvinceTableWriter into withNewResults.
Then .5d drops the payload column.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:14:40 -07:00
a466c42cd8 Phase 4.5h.5a: missing per-entity readers (#6776)
* Phase 4.5h.5a (red): missing per-entity readers (stub) + round-trip tests

Begins Phase 4.5h.5 (the final read-path-swap chunk). Per the agreed
four-slice plan:
  .5a (this PR): the two missing per-entity readers + round-trip tests
                 with their writers. Readers exist but are unused by
                 production.
  .5b:           the top-level ActionResultDbReader orchestrator +
                 parity tests asserting it produces the same ActionResultC
                 as the converter-based read path.
  .5c:           swap replayFrom to use the new reader, stop writing
                 payload.
  .5d:           drop the payload column.

The 4.5b–g per-entity readers were created alongside their writers
(paired RED+GREEN PRs). Two gaps remain:

- ChangedProvinceTableReader: ChangedProvince was the first option-B
  entity (4.5b) and its reader was deferred because nothing needed it
  until 4.5h.
- ActionResultVectorTablesReader: the v20 vector tables (4.5h.4) were
  built with only a writer because the read path hadn't started yet.

Both readers are needed by .5b's top-level orchestrator. Adding them
now closes the gap.

This commit (red):
- ChangedProvinceTableReader stub: read returns Vector.empty. Reads
  from action_changed_provinces' changed_province_proto BLOB (the
  scalar denorm columns are ignored on read; the blob is authoritative
  per the writer's contract).
- ActionResultVectorTablesReader stub: five public read methods (one
  per v20 table), all returning Vector.empty. The four primitive-ID
  methods will share an internal readIds helper in GREEN;
  readNewBattalionTypes will parse battalion_type_proto via
  BattalionTypeConverter.fromProto.
- Two red tests in SqliteHistoryTest: round-trip ChangedProvinces via
  the writer + new reader (two protos, exercises n ordering); round-trip
  all five vectors via withNewResults + new reader.
- Visibility widening: model/action_result/changed_province:changed_province
  and model/proto_converters:changed_province_converter to add the
  service main + test service packages (GREEN reader will need the
  converter).

Verification: sqlite_history_test 89/91 pass, 2 RED for the right reason
(stub readers return Vector.empty → "Vector() was not equal to Vector(17,
23)").

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5h.5a (green): implement missing per-entity readers

Fills in the two reader stubs from the red commit with real
implementations. No new tests — the round-trip tests added in the red
commit now turn green.

ChangedProvinceTableReader:
- read(connection, actionSeq): SELECT changed_province_proto ORDER BY n,
  parse each blob via ChangedProvinceProto.parseFrom, convert to domain
  via ChangedProvinceConverter.fromProto.

ActionResultVectorTablesReader:
- readDestroyedBattalionIds / readRemovedHeroIds / readRemovedFactionIds
  / readAffectedFactionIds: all delegate to the shared private readIds
  helper which does SELECT $idColumn FROM $table WHERE action_seq = ?
  ORDER BY n. Table and column names are internal constants, never user
  input.
- readNewBattalionTypes: SELECT battalion_type_proto ORDER BY n, parse
  each blob via BattalionTypeProto.parseFrom, convert via
  BattalionTypeConverter.fromProto. The type_id column is ignored on
  read — the blob is authoritative.

BUILD additions (green-only):
- changed_province_table_reader deps:
  internal:changed_province_scala_proto (for parseFrom),
  proto_converters:changed_province_converter (for fromProto).
- action_result_vector_tables_reader deps:
  common:battalion_type_scala_proto (for parseFrom),
  proto_converters:battalion_type_converter (for fromProto).

Verification: sqlite_history_test 91/91 pass; full Scala test suite
226/226.

Next: 4.5h.5b — top-level ActionResultDbReader orchestrator that uses
all the per-entity readers (plus the action_results column reads) to
assemble an ActionResultC. Parity tests assert it matches the
converter-based read.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:30:36 -07:00
adminandGitHub 40dd82e638 Print Shardok binary hash during deploy (#6775) 2026-05-27 07:12:51 -07:00
824748d88f Phase 4.5h.4: action_results vector tables (#6773)
* Phase 4.5h.4 (red): action_results vector tables schema + tests

Continues 4.5h after .3 (structured-Option blob columns). Five remaining
top-level ActionResultC vectors decomposed into child tables (one row
per element), matching the per-entity table precedent from 4.5e–g:

  - action_destroyed_battalion_ids (destroyedBattalionIds: Vector[BattalionId])
  - action_removed_hero_ids        (removedHeroIds: Vector[HeroId])
  - action_removed_faction_ids     (removedFactionIds: Vector[FactionId])
  - action_affected_faction_ids    (affectedFactionIds: Vector[FactionId])
  - action_new_battalion_types     (newBattalionTypes: Vector[BattalionType]
                                    — quest-hybrid: type_id queryable +
                                    whole battalion_type_proto BLOB)

All five share the (action_seq, n) PK pattern (n = position in the
source vector — the #6735/#6737/#6738 disambiguator). The four
primitive-ID tables index the id column for the obvious "actions
affecting entity X" lookups. BattalionType is a flat 12-field struct
that's only ever looked up by type_id, so it gets the quest-hybrid
shape (type_id column + whole battalion_type_proto BLOB) rather than
fully columnizing — consistent with how flat-but-deep entities are
handled elsewhere in this package.

All FK back to action_results(action_seq) ON DELETE CASCADE.

Unified writer (ActionResultVectorTablesWriter) with a facade
write(connection, actionSeq, ar: ActionResultT) that iterates all five
vectors internally. SqliteHistory.withNewResults calls it once per
action; backfill calls the same facade per row after fromProto. Single
source of truth, no risk of drift between live + backfill.

Read path is NOT swapped here — replayFrom still parses payload. The
rows are "shadow" until 4.5h.5 drops payload and switches the read
path.

This commit (red):
- ActionResultVectorTablesWriter stub: real schemaStatements (5 CREATE
  TABLE + 5 CREATE INDEX), no-op write + no-op backfill.
- Migration v20 registered.
- Two red tests in SqliteHistoryTest, each asserting all 5 tables: live-
  write via withNewResults, and backfill from an existing payload.
  Shared assertVectorTablesPopulated helper checks every row of every
  table.

After this lands, the only remaining 4.5h work is .5 — drop payload +
read-path swap.

Verification: sqlite_history_test 87/89 pass, 2 RED for the right reason
(tables exist per migration, no rows because stubs are no-ops →
rs.next() returns false in withRow).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5h.4 (green): implement action_results vector tables

Fills in the ActionResultVectorTablesWriter stub from the red commit
and wires SqliteHistory.withNewResults to call the writer's facade
once per action. v20 backfill turns from a no-op into a real per-row
decomposition of every existing payload.

Writer (ActionResultVectorTablesWriter):
- write(connection, actionSeq, ar): facade that dispatches to four
  insertIdVector calls (one per primitive-ID vector, each batched) plus
  insertNewBattalionTypes. Single source of truth for "what gets
  inserted from a domain ActionResultT" across live + backfill.
- insertIdVector(connection, table, idColumn, actionSeq, ids): batched
  INSERT into one of the four primitive-ID join tables. No-op when ids
  is empty. (table/idColumn are internal constants, never user input.)
- insertNewBattalionTypes: batched INSERT into action_new_battalion_types
  binding type_id + BattalionTypeConverter.toProto(bt).toByteArray.
  Domain → proto happens here at the storage edge.
- backfill(connection): drains every action_results.payload (closing
  the SELECT first per the xerial quirk), then per row goes through
  ActionResultProtoConverter.fromProto and delegates to write.

SqliteHistory.withNewResults:
- Single new line after the existing per-vector loops:
  ActionResultVectorTablesWriter.write(connection, seq, awrs.actionResult).
  The facade iterates all five vectors internally.

BUILD additions (green-only):
- action_result_vector_tables_writer deps:
  action_result_scala_proto (for parseFrom),
  action_result_proto_converter (for fromProto in backfill),
  battalion_type_converter (for toProto + transitively pulls in the
  BattalionType proto for .toByteArray).

The read path is NOT touched here — replayFrom still parses payload.
The rows are "shadow" until 4.5h.5 drops payload and switches the
read path.

Verification: sqlite_history_test 89/89 pass; full Scala test suite
226/226.

Next: 4.5h.5 — drop payload BLOB, swap replayFrom to assemble from
the columns + per-entity blobs. The last 4.5h chunk.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:08:41 -07:00
adminandGitHub 0b70e6e9bf Load Shardok image tarball on Hetzner deploy (#6774) 2026-05-27 07:02:20 -07:00
adminandGitHub 0ad09f4067 Delete first unused Unity asset batch (#6770) 2026-05-27 06:56:53 -07:00
adminandGitHub 3c23afecfa Downscale oversized Unity texture sources (#6769) 2026-05-27 06:53:29 -07:00
adminandGitHub 596143f54d Make DragonEffect tolerate child animators (#6768) 2026-05-27 06:46:58 -07:00
adminandGitHub 6cbc99ae73 Disable checkout credential persistence (#6767) 2026-05-27 06:46:18 -07:00
2497e7114d Phase 4.5h.3: action_results structured-Option blob columns (#6772)
* Phase 4.5h.3 (red): action_results structured-Option blob columns schema + tests

Continues 4.5h after .2 (the nine scalar/Option columns split across .2a/.2b/.2c).
Three structured-Option top-level ActionResultC fields promoted as
nullable BLOB columns on action_results — small nested aggregates that
aren't analytics-queried at this level, so per the design doc's stopping
rule (same as ChangedProvince / newProvinces / etc.) each becomes a
single whole-entity BLOB column rather than being further decomposed:

  - new_battle_proto         (newBattle: Option[ShardokBattle])
  - new_chronicle_entry_proto (newChronicleEntry: Option[ChronicleEntry])
  - new_eagle_map_proto      (newEagleMap: Option[EagleMapInfo])

No indexes — these are BLOBs whose internals aren't queried at the SQL
level; the analytical handles for "which actions started a battle /
advanced chronicle / loaded a map" come from per-entity tables and
scalar columns elsewhere on action_results.

Same shared-bindColumns-helper pattern as .2a / .2b / .2c — backfill
will go through ActionResultProtoConverter.fromProto and delegate to
bindColumns; live INSERT in withNewResults calls the same helper. Single
source of truth, no risk of drift between live + backfill.

Read path is NOT swapped here — replayFrom still parses payload. The
columns are "shadow" until the final 4.5h sub-PR drops payload and
switches the read path.

This commit (red):
- ActionResultStructuredBlobColumnsWriter stub: real schemaStatements
  (3 ALTER TABLE ADD COLUMN ... BLOB), no-op backfill.
- Migration v19 registered.
- Two red tests in SqliteHistoryTest: live-write asserts the three BLOB
  columns populated from the ActionResultC fields (assertions parse the
  stored bytes via the per-entity converter and compare to the domain
  instance — robust to serialization-order differences); backfill asserts
  the same from an existing payload.
- Visibility widening: model/state/shardok_battle:shardok_battle,
  model/state/chronicle_entry:chronicle_entry,
  model/proto_converters/shardok_battle:shardok_battle_converter,
  model/proto_converters/chronicle_entry:chronicle_entry_converter,
  model/proto_converters:eagle_map_info_converter — all to add the
  test (and where relevant, service) packages.

After this lands, the remaining 4.5h work is the primitive-ID vectors +
the small newBattalionTypes vector, then the payload drop + read-path
swap.

Verification: sqlite_history_test 85/87 pass, 2 RED for the right reason
(columns exist per migration, default NULL → getBytes returns null and
parseFrom(null) throws NPE).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5h.3 (green): implement action_results structured-Option blob columns

Fills in the ActionResultStructuredBlobColumnsWriter stub from the red
commit and wires the live INSERT in SqliteHistory.withNewResults to call
the writer's bindColumns helper. v19 backfill turns from a no-op into a
real per-row UPDATE through the full ActionResultProtoConverter.fromProto.

Writer (ActionResultStructuredBlobColumnsWriter):
- bindColumns(ps, startPosition, ar): single source of truth for the
  three blob column bindings — each is the per-entity converter's
  toProto.toByteArray (or SQL NULL for None).
    * new_battle_proto         ← ShardokBattleConverter.toProto(...).toByteArray
    * new_chronicle_entry_proto ← ChronicleEntryConverter.toProto(...).toByteArray
    * new_eagle_map_proto      ← EagleMapInfoConverter.toProto(...).toByteArray
- backfill(connection): drains every action_results.payload (closing
  the SELECT first per the xerial quirk), then per row goes through
  ActionResultProtoConverter.fromProto and delegates to bindColumns.
- bindOptBytes helper via .fold (no match-on-Option).

SqliteHistory.withNewResults:
- INSERT extended from 21 to 24 columns. The three new bindings are a
  single call:
  ActionResultStructuredBlobColumnsWriter.bindColumns(insertStmt,
  startPosition = 22, awrs.actionResult).

BUILD additions (green-only):
- action_result_structured_blob_columns_writer deps:
  action_result_scala_proto, eagle_pkg, action_result_trait,
  action_result_proto_converter, eagle_map_info_converter,
  chronicle_entry_converter, shardok_battle_converter.
- Widened eagle_map_info_converter visibility to include service:__pkg__
  (chronicle_entry_converter and shardok_battle_converter already had
  service visibility from the red commit / prior phases).

The read path is NOT touched here — replayFrom still parses payload.
These columns are "shadow" until the final 4.5h sub-PR drops payload
and switches the read path.

Verification: sqlite_history_test 87/87 pass; full Scala test suite
226/226.

Next: 4.5h.4 — primitive-ID vectors (destroyedBattalionIds,
removedHeroIds, removedFactionIds, affectedFactionIds) + the small
newBattalionTypes vector. Then 4.5h.5 = drop payload + read-path swap.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 06:45:39 -07:00
adminandGitHub fbe7f9606b Update Unity Rider package (#6771) 2026-05-27 06:44:48 -07:00
adminandGitHub 1573e73419 Add Unity LFS asset audit tooling (#6766) 2026-05-26 21:42:31 -07:00
5ea7908db0 Phase 4.5h.2c: action_results date + resolvedBattle columns (#6765)
* Phase 4.5h.2c (red): action_results date + resolvedBattle columns schema + tests

Closes the 4.5h.2 family (enums → numerics → date+text). The last two
scalar/Option top-level ActionResultC fields:

Three nullable columns + two indexes:
  - new_date_year   (newDate.year)
  - new_date_month  (newDate.month.value, 1..12)
  - resolved_battle (resolvedBattle: Option[String] — shardok game id)

The year/month pair mirrors the v1 date_year/date_month pattern already
used for the resulting state's currentDate — newDate is a distinct field
(the change-of-date event), but storing it identically keeps the table
shape uniform. A composite (new_date_year, new_date_month) index matches
the v1 composite on the resulting-state date columns; resolved_battle
is independently indexed.

Same shared-bindColumns-helper pattern as .2a / .2b — backfill goes
through ActionResultProtoConverter.fromProto, then delegates to
bindColumns; live write calls the same bindColumns from the existing
INSERT in withNewResults. Single source of truth, no risk of drift.

Read path is NOT swapped here — replayFrom still parses payload. The
columns are "shadow" until the final 4.5h sub-PR drops payload and
switches the read path.

This commit (red):
- ActionResultDateBattleColumnsWriter stub: real schemaStatements
  (3 ALTER TABLE ADD COLUMN + 2 CREATE INDEX, including the composite
  on (year, month)), no-op backfill.
- Migration v18 registered.
- Two red tests: live-write asserts all three columns populated; backfill
  asserts the same from an existing payload.

After this lands, 4.5h.2 is complete. The remaining 4.5h work is
structured Options (newBattle / newChronicleEntry / newEagleMap),
primitive-ID vectors, the small newBattalionTypes vector, and finally
the payload drop + read-path swap.

Verification: sqlite_history_test 83/85 pass, 2 RED for the right reason
(columns exist per migration, default NULL → getInt returns 0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5h.2c (green): implement action_results date + resolvedBattle columns

Fills in the ActionResultDateBattleColumnsWriter stub from the red
commit and wires the live INSERT in SqliteHistory.withNewResults to call
the writer's bindColumns helper. v18 backfill turns from a no-op into a
real per-row UPDATE through the full ActionResultProtoConverter.fromProto.

This commit closes the 4.5h.2 family — all 9 scalar/Option top-level
ActionResultC fields are now promoted onto action_results columns. The
remaining 4.5h work is structured Options, primitive-ID vectors, and the
payload drop + read-path swap.

Writer (ActionResultDateBattleColumnsWriter):
- bindColumns(ps, startPosition, ar): single source of truth for the
  three column bindings — delegates to bindOptDate (year/month pair)
  and bindOptString (resolved_battle).
- backfill(connection): drains every action_results.payload, then per
  row goes through ActionResultProtoConverter.fromProto and delegates to
  bindColumns. Same pattern as .2a / .2b.
- bindOptDate(ps, yearIdx, monthIdx, opt): Option[Date] → (setInt year,
  setInt month.value) or (setNull, setNull), via .fold.
- bindOptString(ps, idx, opt): Option[String] → setString or setNull,
  via .fold.

SqliteHistory.withNewResults:
- INSERT extended from 18 to 21 columns. The three new bindings are a
  single call:
  ActionResultDateBattleColumnsWriter.bindColumns(insertStmt,
  startPosition = 19, awrs.actionResult).

BUILD additions (green-only):
- action_result_date_battle_columns_writer deps:
  action_result_scala_proto, eagle_pkg (for type aliases referenced
  through ActionResultT), action_result_trait,
  action_result_proto_converter, model/state/date.

The read path is NOT touched here — replayFrom still parses payload.
These columns are "shadow" until the final 4.5h sub-PR drops payload
and switches the read path.

Verification: sqlite_history_test 85/85 pass; full Scala test suite
226/226.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 21:16:26 -07:00
090ce2b1f6 Phase 4.5h.2b: action_results numeric columns (#6763)
* Phase 4.5h.2b (red): action_results numeric columns schema + tests

Continues 4.5h after .2a (enum columns). The second of the three 4.5h.2
sub-PRs (enums → numerics → date+text).

Four numeric-primitive top-level ActionResultC fields promoted as
nullable INTEGER columns + indexes:

  - new_round_id           (newRoundId: Option[RoundId]   — RoundId = Int)
  - new_victor_faction_id  (newVictorFactionId: Option[FactionId] — FactionId = Int)
  - game_ended             (gameEnded: Option[Boolean] — stored as 0/1)
  - new_random_seed        (newRandomSeed: Option[Long]   — SQLite INTEGER takes int64)

Note new_round_id is distinct from the v1 round_id column: the latter is
the resulting state's current round id (always set), this is the action's
change-of-round event (sparse, only set when an action advances the round).

Unlike .2a's enums, these four are direct Option-unwrap from the proto
(no UNKNOWN→None or back-compat semantics), so the GREEN backfill is
simpler — though it'll still go through ActionResultProtoConverter.fromProto
to keep the bindColumns helper as the single source of truth for "what
gets bound and from what" across live INSERT + backfill UPDATE.

The shared-bindColumns-helper pattern from .2a continues. Live-write
path is the existing INSERT INTO action_results (...) in withNewResults,
extended in GREEN to bind these four additional columns positionally via
ActionResultNumericColumnsWriter.bindColumns(insertStmt, position, ar).

Read path is NOT swapped here — replayFrom still parses payload. The
columns are "shadow" until the final 4.5h sub-PR drops payload and
switches the read path.

This commit (red):
- ActionResultNumericColumnsWriter stub: real schemaStatements
  (4 ALTER TABLE ADD COLUMN + 4 CREATE INDEX), no-op backfill (no
  bindColumns yet — that lands in green alongside the impl).
- Migration v17 registered.
- Two red tests in SqliteHistoryTest: live-write asserts the four
  columns populated from the ActionResultC fields; backfill asserts
  the four columns populated from an existing payload.

Verification: sqlite_history_test 81/83 pass, 2 RED for the right reason
(columns exist per migration, default NULL → getInt returns 0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5h.2b (green): implement action_results numeric columns

Fills in the ActionResultNumericColumnsWriter stub from the red commit
and wires the live INSERT in SqliteHistory.withNewResults to call the
writer's bindColumns helper. v17 backfill turns from a no-op into a
real per-row UPDATE through the full ActionResultProtoConverter.fromProto.

Writer (ActionResultNumericColumnsWriter):
- bindColumns(ps, startPosition, ar): single source of truth for the
  four numeric-column bindings.
    * new_round_id          ← ar.newRoundId             (Option[Int])
    * new_victor_faction_id ← ar.newVictorFactionId     (Option[Int])
    * game_ended            ← ar.gameEnded.map(_→0/1)   (Option[Boolean])
    * new_random_seed       ← ar.newRandomSeed          (Option[Long], setLong)
- backfill(connection): drains every action_results.payload (closing
  the SELECT first per the xerial quirk), then issues a batched UPDATE
  per row. Each row goes through ActionResultProtoConverter.fromProto
  to get the domain ActionResultC, then delegates to bindColumns at
  position 1. Same pattern as .2a — keeps live + backfill on a single
  source of truth.
- bindOptInt / bindOptLong helpers via .fold (no match-on-Option).

SqliteHistory.withNewResults:
- INSERT extended from 14 to 18 columns. The four new bindings are a
  single call: ActionResultNumericColumnsWriter.bindColumns(insertStmt,
  startPosition = 15, awrs.actionResult).

BUILD additions (green-only):
- action_result_numeric_columns_writer deps: action_result_scala_proto,
  eagle_pkg (for type aliases referenced through ActionResultT),
  action_result_trait, action_result_proto_converter.

The read path is NOT touched here — replayFrom still parses payload.
These columns are "shadow" until the final 4.5h sub-PR drops payload
and switches the read path.

Verification: sqlite_history_test 83/83 pass; full Scala test suite
226/226.

Next in 4.5h.2: .2c — newDate (year+month columns) + resolvedBattle
(text column) — the last scalar/Option chunk before the structured
Options + primitive-ID vectors + the payload drop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:52:32 -07:00
9d114b89a7 Phase 4.5h.2a: action_results enum columns (#6761)
* Phase 4.5h.2a (red): action_results enum columns schema + tests

Continues 4.5h after .1 (entity-ref columns). The 4.5h.2 family is the
remaining 9 scalar/Option ActionResultC fields, split per the three-group
plan: .2a = enums (this PR), .2b = numeric primitives, .2c = Date +
String.

The three enum-typed fields become four columns — GameType is a sum type
(Normal vs Tutorial(phase: TutorialPhase)) so it needs a kind discriminator
plus a wrapped TutorialPhase column. All nullable INTEGER, all indexed:

  - new_round_phase     (newRoundPhase: Option[RoundPhase])
  - last_command_type   (lastCommandTypeForActingProvince: Option[CommandType])
  - new_game_type       (newGameType: Option[GameType]) — kind discriminator
  - new_tutorial_phase  (TutorialPhase wrapped inside GameType.Tutorial;
                         NULL when Normal or None)

Storage value is the **proto enum number** (Converter.toProto(domain).value)
for all three — the proto enum's wire-format number is intentionally
stable across schema evolutions, unlike e.g. Scala enum .ordinal. The
domain RoundPhase.value happens to match the proto enum number, but going
through the converter for all three keeps the pattern uniform (and forces
any drift into one place — the converter).

GREEN backfill will go through the full ActionResultProtoConverter.fromProto
rather than reading proto fields directly, because the domain
lastCommandTypeForActingProvince has a back-compat fallback path to a
deprecated SelectedCommand field. The converter is the single source of
truth for "domain Option-value from this payload"; reusing it eliminates
drift risk for the cost of a one-time per-row conversion on the migration
path.

Live-write path is the existing INSERT INTO action_results (...) in
withNewResults, extended in GREEN to bind these four additional columns
positionally. Read path is NOT swapped here — replayFrom still goes
through the full converter on the payload blob; the columns are "shadow"
until the final 4.5h sub-PR drops payload and switches the read path.

This commit (red):
- ActionResultEnumColumnsWriter stub: real schemaStatements
  (4 ALTER TABLE ADD COLUMN + 4 CREATE INDEX), no-op backfill.
- Migration v16 registered.
- Two red tests in SqliteHistoryTest: live-write exercises the Tutorial
  branch (both new_game_type AND new_tutorial_phase populated); backfill
  exercises the Normal branch (new_game_type set, new_tutorial_phase
  NULL) so the two tests together cover both GameType branches.

Verification: sqlite_history_test 79/81 pass, 2 RED for the right reason
(columns exist per migration, default NULL → getInt returns 0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5h.2a (green): implement action_results enum columns

Fills in the ActionResultEnumColumnsWriter stub from the red commit and
wires the live INSERT in SqliteHistory.withNewResults to call the writer's
new bindColumns helper. v16 backfill turns from a no-op into a real
per-row UPDATE through the full ActionResultProtoConverter.fromProto.

Writer (ActionResultEnumColumnsWriter):
- bindColumns(ps, startPosition, ar): single source of truth for "what
  the four enum columns get bound to from a domain ActionResultT".
  Returns nothing; binds 4 positional parameters via the shared
  bindOptInt helper. Used by both the live INSERT and the migration
  UPDATE — eliminates drift risk between the two paths.
- backfill(connection): drains every action_results.payload (closing the
  SELECT first per the xerial quirk), then issues a batched UPDATE per
  row. Each row goes through the full ActionResultProtoConverter.fromProto
  to get the domain ActionResultC (which handles the deprecated
  SelectedCommand fallback for lastCommandTypeForActingProvince), then
  delegates to bindColumns at position 1.
- GameType handling: GameType.Tutorial(phase) populates BOTH
  new_game_type AND new_tutorial_phase; GameType.Normal populates only
  new_game_type, leaving new_tutorial_phase NULL (rather than the proto
  sentinel TUTORIAL_PHASE_UNKNOWN = 0).

SqliteHistory.withNewResults:
- INSERT extended from 10 to 14 columns. The four new bindings are a
  single call: ActionResultEnumColumnsWriter.bindColumns(insertStmt,
  startPosition = 11, awrs.actionResult). The 4.5h.1 entity-ref columns
  stay inline (their inline-vs-helper choice can be revisited later).

BUILD additions (green-only):
- action_result_enum_columns_writer deps: tutorial_phase_scala_proto,
  action_result_scala_proto, eagle_pkg (for the BattalionId etc. types
  referenced through ActionResultT), action_result_trait,
  action_result_proto_converter, command_type_converter,
  game_type_converter, round_phase_converter.
- Widened visibility of command_type_converter and round_phase_converter
  to include service:__pkg__.

The read path is NOT touched here — replayFrom still parses payload and
goes through ActionResultProtoConverter.fromProto. These columns are
"shadow" until the final 4.5h sub-PR drops payload and switches the
read path.

Verification: sqlite_history_test 81/81 pass; full Scala test suite
226/226.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 17:01:56 -07:00
adminandGitHub 9112e60378 Queue client update notifications until UI is ready (#6762) 2026-05-26 16:46:19 -07:00
37fa64bdad Phase 4.5h.1: action_results entity-ref columns (#6760)
* Phase 4.5h.1 (red): action_results entity-ref columns schema + tests

Begins Phase 4.5h. The first chunk: four top-level entity-ref Option fields
from ActionResultC promoted onto the existing action_results table as
nullable INTEGER columns, indexed for the obvious query shapes
("show all actions by hero X"):

  - acting_hero_id      (domain actingHeroId     / proto leader)
  - acting_faction_id   (domain actingFactionId  / proto player)
  - province_id         (domain provinceId       / proto province)
  - province_id_acted   (domain provinceIdActed  / proto province_acted)

Proto field names don't match the domain — the column names follow the
domain since that's what the read path will ultimately expose.

Distinct from the per-entity table writers: there's no new table here,
and no per-row write helper. The live-write path is the existing
INSERT INTO action_results (...) statement in withNewResults, which the
green commit extends to bind these four extra columns positionally
(RED: INSERT unchanged, new columns default to NULL, so getInt returns 0
and the test fails 0 != 9).

The read path is NOT swapped here — replayFrom still parses the payload
blob and goes through ActionResultProtoConverter.fromProto. The columns
are "shadow" until the final 4.5h sub-PR drops payload and switches the
read path.

This commit (red):
- ActionResultEntityRefColumnsWriter stub: real schemaStatements
  (4 ALTER TABLE ADD COLUMN + 4 CREATE INDEX), no-op backfill.
- Migration v15 registered (runs the schema; backfill is the stub no-op).
- Two red tests in SqliteHistoryTest: live-write asserts the four
  columns are populated from the ActionResultC fields, and backfill
  asserts the four columns are populated from an existing payload.

Verification: sqlite_history_test 77/79 pass, 2 RED for the right reason
(columns exist per migration, default NULL → getInt returns 0). The impl
that turns them green is the next commit on this branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5h.1 (green): implement action_results entity-ref columns

Fills in the ActionResultEntityRefColumnsWriter.backfill stub from the
red commit and extends the existing INSERT in SqliteHistory.withNewResults
to populate the four new columns. v15 backfill turns from a no-op into
a real one-row-per-existing-payload UPDATE.

Writer (ActionResultEntityRefColumnsWriter):
- backfill(connection): drains every action_results.payload (fully
  closing the SELECT ResultSet before any UPDATE — the xerial
  sqlite-jdbc quirk), then issues one batched UPDATE per row binding
  leader/player/province/provinceActed from the parsed proto. The
  UPDATE is unconditional: rows whose four proto fields are all unset
  bind NULL × 4, identical to the post-ALTER default — keeps the logic
  uniform with no extra branching.
- bindOptInt helper: Option[Int] → setInt or setNull, via .fold (no
  match-on-Option).

SqliteHistory.withNewResults:
- The INSERT INTO action_results (...) statement is extended from 6
  columns to 10 — adding acting_hero_id, acting_faction_id, province_id,
  province_id_acted at positions 7–10. The four new bindings each use
  .fold to handle the Option[Int] domain field. The existing
  date_year/date_month pattern-match stays as-is (local convention).

BUILD addition (green-only):
- action_result_entity_ref_columns_writer: action_result_scala_proto
  (for ActionResultProto.parseFrom in backfill).

The read path is NOT touched here — replayFrom still parses payload and
goes through ActionResultProtoConverter.fromProto. These columns are
"shadow" until the final 4.5h sub-PR drops payload and switches the
read path.

Verification: sqlite_history_test 79/79 pass; full Scala test suite
226/226.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 15:53:08 -07:00
adminandGitHub 9eca1f4a91 Clarify expired OAuth session login state (#6759) 2026-05-26 15:09:36 -07:00
5d807ef268 Phase 4.5g.3: client text visibility extensions decomposition (#6758)
* Phase 4.5g.3 (red): client text visibility extensions decomposition tests + schema

Final 4.5g chunk. ClientTextVisibilityExtensionT is small and all-scalar
(textId: ClientTextId + recipientFactionIds: Vector[FactionId]) with no
sealed-hierarchy variants and no nested aggregates — so it's a **refined**
entity, fully columnized with no *_proto BLOB. Two tables, mirroring the
ChangedFaction primitive-ID-join precedent:

  - action_client_text_visibility_extensions (parent): one row per
    extension, (action_seq, n) PK, text_id queryable column (indexed).
  - action_client_text_visibility_extension_recipients (child): one row
    per (extension, faction_id), PK (action_seq, n, faction_id),
    faction_id indexed. Set semantics: the reader sorts by faction_id on
    read, matching the existing ChangedFaction _*_ids children.

Both FK back to action_results(action_seq) ON DELETE CASCADE directly
(matching the existing convention) — cascading from the parent action
keeps both tables in sync without an explicit parent-to-child FK.

There is no ClientTextVisibilityExtensionConverter file — conversion is
inlined in ActionResultProtoConverter. Backfill will build a
ClientTextVisibilityExtensionC directly from the proto's nested
ClientTextVisibilityExtension fields.

This commit (red):
- ClientTextVisibilityExtensionTableWriter stub: real schemaStatements
  (both tables + indexes), no-op write/backfill.
- ClientTextVisibilityExtensionTableReader stub: read returns
  Vector.empty.
- Migration v14 registered (runs the schema; backfill is the stub
  no-op).
- Two red tests in SqliteHistoryTest: live-write/reader round-trip
  preserving text_id and recipients (including an extension with empty
  recipients, to exercise the no-child-rows case), and backfill from an
  existing payload (asserts parent rows + recipients child rows for both
  the populated and empty-recipients case).
- client_text_visibility_extension_trait visibility widened to the
  service main + test packages; client_text_visibility_extension_concrete
  visibility widened to the service main + test service package.

Verification: sqlite_history_test 75/77 pass, 2 RED for the right reason
(schema exists, rows absent). The impl that turns them green is the next
commit on this branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5g.3 (green): implement client text visibility extensions decomposition

Fills in the ClientTextVisibilityExtensionTableWriter / Reader stubs from
the red commit and wires the write into withNewResults. v14 backfill
turns from a no-op into the real per-extension decomposition of every
existing action_results.payload.

Writer (ClientTextVisibilityExtensionTableWriter):
- write(connection, actionSeq, n, extension): one INSERT into
  action_client_text_visibility_extensions (parent row with text_id),
  then a batched INSERT into action_client_text_visibility_extension_recipients
  (one row per faction_id). Skips the child INSERT entirely when
  recipientFactionIds is empty.
- backfill(connection): reads every action_results.payload (fully
  draining the SELECT ResultSet before any INSERT), parses ActionResult
  proto, iterates clientTextVisibilityExtensions and builds a
  ClientTextVisibilityExtensionC directly from the nested proto fields
  (the same inline conversion ActionResultProtoConverter does), then
  delegates to write.

Reader (ClientTextVisibilityExtensionTableReader):
- read(connection, actionSeq): two-pass — first drain parent rows
  (n, text_id) ordered by n, then per-parent SELECT of the recipient
  faction_ids ordered ascending (set semantics, matching
  ChangedFactionTableReader's _*_ids children).

SqliteHistory:
- withNewResults: after the action_results executeBatch (so the FK
  target rows exist), for each (awrs, i) iterate
  awrs.actionResult.clientTextVisibilityExtensions.zipWithIndex and call
  ClientTextVisibilityExtensionTableWriter.write — same loop shape as
  the other per-vector writes already in place.

BUILD additions (green-only):
- client_text_visibility_extension_table_writer: action_result_scala_proto
  (for ActionResultProto.parseFrom) + client_text_visibility_extension_concrete
  (for the ClientTextVisibilityExtensionC constructor in backfill).
- client_text_visibility_extension_table_reader:
  client_text_visibility_extension_concrete (same — assembling each
  read row).

Verification: sqlite_history_test 77/77 pass; full Scala test suite
226/226.

This closes Phase 4.5g. Next is 4.5h: top-level ActionResultC scalar
columns on action_results + drop the payload BLOB, swapping the read
path to scalar columns + per-entity blobs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 15:06:34 -07:00
c9b045cd78 Phase 4.5g.2: generated text requests decomposition (#6756)
* Phase 4.5g.2 (red): generated text requests decomposition tests + schema

Continues Phase 4.5g. After notifications (4.5g.1), this is the second
chunk: the newGeneratedTextRequests "new entity" vector.

GeneratedTextRequestT is a sealed trait (FixedHeroName, GeneratedHeroName,
and the ~45-case LlmRequestT enum), so per the quest-hybrid stopping rule
each request is stored as a whole-entity generated_text_request_proto BLOB
plus a queryable `kind` discriminator column — the same shape as
notifications, minus the notification-only `deferred` column (a
GeneratedTextRequest carries no field beside the proto).

One table, action_new_generated_text_requests (there is no "removed"
counterpart): (action_seq, n, kind, generated_text_request_proto), PK
(action_seq, n), kind indexed.

A wrinkle worth noting: the production read path
(ActionResultProtoConverter.fromProto) deliberately drops
newGeneratedTextRequests ("not used by the applier"), so the proto blob in
action_results.payload is the only place old requests survive. Backfill
therefore reads the proto's repeated new_generated_text_requests field
directly, and the decomposed table becomes the first relational home for
this data.

This commit (red):
- GeneratedTextRequestTableWriter stub: real schemaStatements (table +
  kind index), no-op write/backfill.
- GeneratedTextRequestTableReader stub: read returns Vector.empty.
- Migration v13 registered (runs the schema; backfill is the stub no-op).
- Two red tests in SqliteHistoryTest: newGeneratedTextRequests round-trip
  preserving kind + order via withNewResults, and backfill from an
  existing payload (asserting the kind discriminator in n order).

The converter and domain-type targets were already visible to the service
and test packages, so no visibility widening was needed.

Verification: sqlite_history_test 73/75 pass, 2 RED for the right reason
(schema exists, rows absent). The impl that turns them green is the next
commit on this branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5g.2 (green): implement generated text requests decomposition

Fills in the GeneratedTextRequestTableWriter / Reader stubs from the red
commit and wires the write into withNewResults. v13 backfill turns from a
no-op into the real per-request decomposition of every existing
action_results.payload.

Writer (GeneratedTextRequestTableWriter):
- write(connection, actionSeq, n, request): INSERT one row with
  (action_seq, n, kind, generated_text_request_proto). Converts the
  domain request to proto exactly at the storage edge via
  GeneratedTextRequestConverter.toProto.
- kindOf(request): productPrefix-based discriminator. Every subtype —
  FixedHeroName, GeneratedHeroName, and each LlmRequestT enum case — is
  a Product, so the compiler proves the match exhaustive (no fallback,
  which would be unreachable under -Werror).
- backfill(connection): reads every action_results.payload (fully
  draining the SELECT ResultSet before any INSERT, the xerial
  sqlite-jdbc quirk), parses ActionResult proto, then iterates the proto's
  repeated new_generated_text_requests field directly — because
  ActionResultProtoConverter.fromProto deliberately drops this vector
  ("not used by the applier"), the blob is the only place these
  requests survive. Each is converted to the domain type via
  GeneratedTextRequestConverter.fromProto and delegated to write.

Reader (GeneratedTextRequestTableReader):
- read(connection, actionSeq): SELECT generated_text_request_proto in n
  order, parse each blob, convert via fromProto.

SqliteHistory:
- withNewResults: after the action_results executeBatch (so the FK target
  rows exist), for each (awrs, i) iterate
  awrs.actionResult.newGeneratedTextRequests.zipWithIndex and call
  GeneratedTextRequestTableWriter.write — same loop shape as the other
  per-vector writes already in place.

BUILD additions (green-only):
- generated_text_request_table_writer: action_result_scala_proto (for
  ActionResultProto.parseFrom) + generated_text_request_converter (for
  toProto / fromProto at the storage edge).
- generated_text_request_table_reader: llm_request_scala_proto (for
  GeneratedTextRequestProto.parseFrom — the target name is historical
  baggage; the proto is generated_text_request.proto) +
  generated_text_request_converter.

Verification: sqlite_history_test 75/75 pass; full Scala test suite
225/225.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 13:49:48 -07:00
adminandGitHub f2c3772304 Preserve Shardok setup placements on reconnect (#6757) 2026-05-26 13:37:54 -07:00
adminandGitHub 7ab1b44989 Apply faction name diffs on client (#6755) 2026-05-25 07:26:23 -07:00
adminandGitHub e584e85642 Handle factionless profession gain notifications (#6754) 2026-05-24 13:18:53 -07:00
adminandGitHub e543d7b53f Run scalafmt hook through Bazel (#6753) 2026-05-24 13:01:56 -07:00
adminandGitHub c08ed8c170 Suppress ready-to-join notification after province ownership change (#6751) 2026-05-24 14:56:58 -04:00
adminandGitHub 86d396c5f5 Suppress ready-to-join notification after quest fulfillment (#6752) 2026-05-24 11:14:08 -04:00
adminandGitHub e635e2d5cf Preserve tutorial generated text requests (#6750) 2026-05-23 22:42:24 -04:00
266039df60 Phase 4.5g.1: notifications decomposition (#6749)
* Phase 4.5g.1 (red): notifications decomposition tests + schema

Starts Phase 4.5g — notifications + generated text requests. This is the
first chunk: the newNotifications / removedNotifications vectors.

NotificationT's details is a ~45-subtype sealed hierarchy, so per the
quest-hybrid stopping rule each notification is stored as a whole-entity
notification_proto BLOB plus a queryable `kind` discriminator column.
Two wrinkles unique to notifications:

  - deferred is NOT in the proto. NotificationConverter.toProto returns
    (NotificationProto, Boolean) — the Boolean is deferred, carried
    beside the message. So deferred is its own column.
  - the proto representation of newNotifications is lossy: it splits into
    a single new_deferred_notification + a repeated notifications_to_deliver,
    so multiple deferred new notifications and original order across the
    split don't survive a proto round-trip. The decomposed tables store
    the domain vector faithfully — one row per element, in n order, each
    with its own deferred — which is strictly better than the proto.

Both action_new_notifications and action_removed_notifications share the
shape (action_seq, n, kind, deferred, notification_proto), PK
(action_seq, n), kind indexed.

This commit (red):
- NotificationTableWriter stub: real schemaStatements (both tables +
  kind indexes), no-op writeNew/writeRemoved/backfill.
- NotificationTableReader stub: readNew/readRemoved return Vector.empty.
- Migration v12 registered (runs the schema; backfill is the stub no-op).
- Three red tests in SqliteHistoryTest: newNotifications round-trip
  preserving deferred + order (the proto-can't-do-this case),
  removedNotifications round-trip, and backfill from an existing payload
  (asserting the kind discriminator).
- notification_trait / notification_concrete / notification_converter
  visibility widened to the service main and/or test packages.

Verification: sqlite_history_test 70/73 pass, 3 RED for the right
reason (schema exists, rows absent). The impl that turns them green is
the next commit on this branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5g.1 (green): implement notifications decomposition

Turns the three RED tests from the previous commit green by filling in
the writer, reader, and live write-path wiring.

- NotificationTableWriter.writeNew / writeRemoved: INSERT one row per
  notification at (action_seq, n) — kind discriminator + deferred column
  + the Notification proto bytes. kind = details.productPrefix (clean
  subtype name; the compiler proves the Product case exhaustive over the
  sealed NotificationDetails hierarchy, so no reflection and no
  45-case map). deferred comes from NotificationConverter.toProto's
  second tuple element (it lives beside the proto, not in it).
- NotificationTableWriter.backfill: for every existing payload,
  reconstruct newNotifications exactly as ActionResultProtoConverter
  does (single new_deferred_notification ++ notifications_to_deliver),
  plus removedNotifications; write each. Reads all payloads before any
  INSERT (closes the SELECT ResultSet first).
- NotificationTableReader.readNew / readRemoved: inverse — parse each
  blob and combine with the deferred column via
  NotificationConverter.fromProto(proto, deferred), ordered by n.
- SqliteHistory.withNewResults: live-write loop now also writes
  newNotifications + removedNotifications.
- BUILD: writer/reader gain action_result_scala_proto /
  action_result_notification_details_scala_proto + notification_converter
  deps.

Verification: sqlite_history_test 73/73 pass; full //src/test/scala/...
225/225 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 07:51:55 -07:00
ec8223fb4f Phase 4.5f.4: newProvinces decomposition (#6747)
* Phase 4.5f.4 (red): newProvinces decomposition tests + schema

Last and deepest of the "new entity" vectors (after newBattalions
4.5f.1, newFactions 4.5f.2, newHeroes 4.5f.3): newProvinces. A
ProvinceT creation snapshot transitively embeds armies, units, heroes,
battalions, events, deferred changes — exactly the nesting 4.5b's
ChangedProvince kept as a proto blob by the same stopping rule. So it
takes the whole-entity province_proto BLOB shape + a queryable
province_id index.

action_new_provinces is a brand-new table, distinct from 4.5b's
action_changed_provinces (deltas). PK is (action_seq, n) from the
start — the disambiguator the #6735/#6737/#6738 outages taught us — so
multiple new provinces in one action all land; province_id is indexed
but non-unique.

This commit (red):
- NewProvinceTableWriter stub: real schemaStatements (CREATE the
  (action_seq, n) table + index), no-op write and backfill.
- NewProvinceTableReader stub: returns Vector.empty.
- Migration v11 registered (runs the schema; backfill is the stub no-op).
- Three red tests in SqliteHistoryTest: live write + reader round-trip,
  two new provinces preserved in n order, and backfill from an existing
  payload.
- ProvinceC concrete visibility widened to the service test package
  (ProvinceT and ProvinceConverter are already //visibility:public).

Verification: sqlite_history_test 67/70 pass, 3 RED for the right
reason (schema exists, rows absent). The impl that turns them green is
the next commit on this branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5f.4 (green): implement newProvinces decomposition

Turns the three RED tests from the previous commit green by filling in
the writer, reader, and live write-path wiring. Completes Phase 4.5f
(all four "new entity" vectors decomposed).

- NewProvinceTableWriter.write: INSERT one row per new province at
  (action_seq, n) — province_id column (denormalized, indexed) plus the
  Province proto bytes in province_proto. Domain type in;
  ProvinceConverter.toProto only at the blob storage edge.
- NewProvinceTableWriter.backfill: decompose every newProvinces entry in
  every existing action_results.payload. Reads all payloads before any
  INSERT (closes the SELECT ResultSet first); the proto field is a
  Province per entry, converted via fromProto before write.
- NewProvinceTableReader.read: inverse — parse each blob back to a
  ProvinceT, ordered by n. (Round-trip test confirms ProvinceConverter
  is lossless for the test province.)
- SqliteHistory.withNewResults: live-write loop now also writes
  newProvinces (same n-disambiguator pattern as the other new-entity
  vectors).
- BUILD: writer/reader gain action_result_scala_proto /
  province_scala_proto + province converter deps.

Verification: sqlite_history_test 70/70 pass; full //src/test/scala/...
225/225 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 16:40:06 -07:00
adminandGitHub 76d3d25d05 Account for incoming food in low food warnings (#6748) 2026-05-22 08:30:44 -07:00
adminandGitHub 6672683289 Display ready-to-join hero notifications (#6745)
* Show ready-to-join hero notifications

* Update Unity project file
2026-05-22 08:16:15 -07:00
91ee65f7d0 Phase 4.5f.3: newHeroes decomposition (#6746)
* Phase 4.5f.3 (red): newHeroes decomposition tests + schema

Third of the "new entity" vectors (after newBattalions 4.5f.1 and
newFactions 4.5f.2): newHeroes. Full HeroT creation snapshots (no
Changed* wrapper), embedding backstory versions, backstory events, and
faction biases — a deep entity, so it takes the whole-entity hero_proto
BLOB shape + a queryable hero_id index.

action_new_heroes is a brand-new table (the "new entity" vectors had no
v2 scaffolding). PK is (action_seq, n) from the start — the
disambiguator the #6735/#6737/#6738 outages taught us — so multiple new
heroes in one action all land; hero_id is indexed but non-unique.

This commit (red):
- NewHeroTableWriter stub: real schemaStatements (CREATE the
  (action_seq, n) table + index), no-op write and backfill.
- NewHeroTableReader stub: returns Vector.empty.
- Migration v10 registered (runs the schema; backfill is the stub no-op).
- Three red tests in SqliteHistoryTest: live write + reader round-trip,
  two new heroes preserved in n order, and backfill from an existing
  payload.
- hero converter target visibility widened to the service main and test
  packages (HeroT and HeroC are already visible to service).

Verification: sqlite_history_test 64/67 pass, 3 RED for the right
reason (schema exists, rows absent). The impl that turns them green is
the next commit on this branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5f.3 (green): implement newHeroes decomposition

Turns the three RED tests from the previous commit green by filling in
the writer, reader, and live write-path wiring.

- NewHeroTableWriter.write: INSERT one row per new hero at (action_seq,
  n) — hero_id column (denormalized, indexed) plus the Hero proto bytes
  in hero_proto. Domain type in; HeroConverter.toProto only at the blob
  storage edge.
- NewHeroTableWriter.backfill: decompose every newHeroes entry in every
  existing action_results.payload. Reads all payloads before any INSERT
  (closes the SELECT ResultSet first); the proto field is a Hero per
  entry, converted via fromProto before write.
- NewHeroTableReader.read: inverse — parse each blob back to a HeroT,
  ordered by n. (Round-trip test confirms HeroConverter is lossless over
  all HeroC fields.)
- SqliteHistory.withNewResults: live-write loop now also writes
  newHeroes (same n-disambiguator pattern as newBattalions/newFactions).
- BUILD: writer/reader gain action_result_scala_proto / hero_scala_proto
  + hero converter deps.

Verification: sqlite_history_test 67/67 pass; full //src/test/scala/...
225/225 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 08:14:18 -07:00
7d3dee125a Phase 4.5f.2: newFactions decomposition (#6744)
* Phase 4.5f.2 (red): newFactions decomposition tests + schema

Second of the "new entity" vectors (after newBattalions, 4.5f.1):
newFactions. Full FactionT creation snapshots (no Changed* wrapper),
which transitively embed diplomacy offers, faction relationships, and
reconned province views — a deep entity, so it takes the whole-entity
faction_proto BLOB shape + a queryable faction_id index.

action_new_factions is a brand-new table (the "new entity" vectors had
no v2 scaffolding). PK is (action_seq, n) from the start — the
disambiguator the #6735/#6737/#6738 outages taught us — so multiple new
factions in one action all land; faction_id is indexed but non-unique.

This commit (red):
- NewFactionTableWriter stub: real schemaStatements (CREATE the
  (action_seq, n) table + index), no-op write and backfill.
- NewFactionTableReader stub: returns Vector.empty.
- Migration v9 registered (runs the schema; backfill is the stub no-op).
- Three red tests in SqliteHistoryTest: live write + reader round-trip,
  two new factions preserved in n order, and backfill from an existing
  payload.
- faction (FactionT) / faction/concrete (FactionC) / faction converter
  target visibility widened to the service main and/or test packages.

Verification: sqlite_history_test 61/64 pass, 3 RED for the right
reason (schema exists, rows absent). The impl that turns them green is
the next commit on this branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Phase 4.5f.2 (green): implement newFactions decomposition

Turns the three RED tests from the previous commit green by filling in
the writer, reader, and live write-path wiring.

- NewFactionTableWriter.write: INSERT one row per new faction at
  (action_seq, n) — faction_id column (denormalized, indexed) plus the
  Faction proto bytes in faction_proto. Domain type in;
  FactionConverter.toProto only at the blob storage edge.
- NewFactionTableWriter.backfill: decompose every newFactions entry in
  every existing action_results.payload. Reads all payloads before any
  INSERT (closes the SELECT ResultSet first); the proto field is a
  Faction per entry, converted via fromProto before write.
- NewFactionTableReader.read: inverse — parse each blob back to a
  FactionT, ordered by n.
- SqliteHistory.withNewResults: live-write loop now also writes
  newFactions (same n-disambiguator pattern as newBattalions).
- BUILD: writer/reader gain action_result_scala_proto /
  faction_scala_proto + faction converter deps.

Verification: sqlite_history_test 64/64 pass; full //src/test/scala/...
225/225 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 07:32:49 -07:00
adminandGitHub 8aedd945cf Notify when heroes are ready to join (#6743) 2026-05-22 07:25:21 -07:00