mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 22:35:42 +00:00
ceed22d6de09e95bfa003035c57b4665eec90e22
10894
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ceed22d6de | Prioritize remaining small Eagle alpha work (#6795) | ||
|
|
75f35e981e | Unpin Bazel workflows from specific runners (#6792) | ||
|
|
76d8f07827 | Keep command factory test flatbuffers alive (#6794) | ||
|
|
705a36e901 | Update Unity 6000.4.9 project files (#6793) | ||
|
|
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 |
||
|
|
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> |
||
|
|
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 |
||
|
|
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>
|
||
|
|
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> |
||
|
|
646c47259c | Upgrade Unity project to 6000.4.9f1 (#6788) | ||
|
|
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>
|
||
|
|
237efb9c0c | Temporarily pin self-hosted CI to halfdan (#6786) | ||
|
|
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>
|
||
|
|
39b8c3360e | Tidy Bazel rc marker and Mac deploy runner label (#6782) | ||
|
|
c2b04c2109 | Pass iOS TestFlight build between jobs (#6781) | ||
|
|
875f34a624 | Harden GitHub Actions workflows (#6780) | ||
|
|
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>
|
||
|
|
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>
|
||
|
|
40dd82e638 | Print Shardok binary hash during deploy (#6775) | ||
|
|
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>
|
||
|
|
0b70e6e9bf | Load Shardok image tarball on Hetzner deploy (#6774) | ||
|
|
0ad09f4067 | Delete first unused Unity asset batch (#6770) | ||
|
|
3c23afecfa | Downscale oversized Unity texture sources (#6769) | ||
|
|
596143f54d | Make DragonEffect tolerate child animators (#6768) | ||
|
|
6cbc99ae73 | Disable checkout credential persistence (#6767) | ||
|
|
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> |
||
|
|
fbe7f9606b | Update Unity Rider package (#6771) | ||
|
|
1573e73419 | Add Unity LFS asset audit tooling (#6766) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
9112e60378 | Queue client update notifications until UI is ready (#6762) | ||
|
|
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>
|
||
|
|
9eca1f4a91 | Clarify expired OAuth session login state (#6759) | ||
|
|
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>
|
||
|
|
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>
|
||
|
|
f2c3772304 | Preserve Shardok setup placements on reconnect (#6757) | ||
|
|
7ab1b44989 | Apply faction name diffs on client (#6755) | ||
|
|
e584e85642 | Handle factionless profession gain notifications (#6754) | ||
|
|
e543d7b53f | Run scalafmt hook through Bazel (#6753) | ||
|
|
c08ed8c170 | Suppress ready-to-join notification after province ownership change (#6751) | ||
|
|
86d396c5f5 | Suppress ready-to-join notification after quest fulfillment (#6752) | ||
|
|
e635e2d5cf | Preserve tutorial generated text requests (#6750) | ||
|
|
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>
|
||
|
|
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> |
||
|
|
76d3d25d05 | Account for incoming food in low food warnings (#6748) | ||
|
|
6672683289 |
Display ready-to-join hero notifications (#6745)
* Show ready-to-join hero notifications * Update Unity project file |
||
|
|
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> |
||
|
|
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> |
||
|
|
8aedd945cf | Notify when heroes are ready to join (#6743) |