Skip to content

fix(sim): transfer subject/vassal bonds during empire merges - #39

Merged
csheldrick merged 2 commits into
mainfrom
claude/galimulator-noet-workflow-ngfamm
Jul 15, 2026
Merged

csheldrick merged 2 commits into
mainfrom
claude/galimulator-noet-workflow-ngfamm

Conversation

@csheldrick

Copy link
Copy Markdown
Owner

Summary

  • mergeEmpires transferred fleets, artifacts, factions, alliances, and diplomatic relationships from the absorbed empire to the dominant one, but never touched state.subjects. A merged empire that was a vassal/tributary (or an overlord of others) left dangling subject records — the next stepSubjects pass would silently drop the relation or fire a spurious liberation event, instead of correctly carrying the bond forward.
  • Subject/vassal ties now transfer to the dominant empire on merge, mirroring the cleanup removeEmpireFromGalaxy already performs on empire death. A tie directly between the absorbed and dominant empire collapses (both sides are now the same empire); a transfer that would violate the one-overlord/no-cycle invariant falls back to liberation, same as elsewhere in the codebase.
  • Declared npm run build / npm run lint / npm run report as checks in .noet/VERIFICATION.md and recorded passing evidence via noet verify run + noet verify check (includes the determinism replay guard).
  • Recorded one constraint in .noet/CONSTRAINTS.md: new per-empire relational state needs transfer logic in both mergeEmpires and removeEmpireFromGalaxy, since this bug is exactly a case of the former being added without touching the latter's counterpart.

Test plan

  • npm run build (typecheck + Vite build)
  • npm run lint
  • npm run report -- --milestones 1000,3000,10000 — 10k-tick run with 20 empire merges and ongoing subject churn, no crashes; determinism replay check passed
  • noet verify run / noet verify check — all declared checks green and fresh

🤖 Generated with Claude Code


Generated by Claude Code

mergeEmpires transferred fleets, artifacts, factions, alliances, and
relationships from the absorbed empire to the dominant one, but never
touched state.subjects. A merged empire that was a vassal/tributary
(or an overlord of others) left dangling subject records, which the
next stepSubjects pass would either silently drop or resolve into a
spurious liberation event instead of correctly transferring the bond.

Subject ties now transfer to the dominant empire, mirroring the
cleanup removeEmpireFromGalaxy already does on empire death. A tie
directly between the absorbed and dominant empire collapses (both
sides are now one empire); a transfer that would violate the
one-overlord/no-cycle invariant falls back to liberation.

Declared build/lint/headless-report checks in .noet/VERIFICATION.md
and recorded evidence via noet verify. Also recorded a constraint:
new per-empire relational state needs transfer logic in both
mergeEmpires and removeEmpireFromGalaxy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018o55CaUYZM5bNccWjsctnc

@csheldrick csheldrick left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two changes are needed before I would merge this.

  1. Avoid the new Merge.ts ↔ Subjects.ts circular dependency. Subjects.ts already imports mergeEmpires; this PR makes Merge.ts import breakSubjectRelation, subjectOf, and subjectsOf back from Subjects.ts. Even if the current ESM cycle happens to initialize successfully, it creates a fragile dependency between core simulation modules. Move the shared subject-query/transfer primitives into a neutral module, or implement the merge-side state transformation without importing the module that calls mergeEmpires.

  2. Add focused regression coverage for the subject-transfer branches. The 10k-tick report is useful integration evidence, but it does not prove the specific cases introduced here. Cover at least: absorbed subject transfers to dominant; absorbed overlord's subjects transfer to dominant; a direct dominant↔absorbed subject tie collapses; conflicting transfer liberates instead of creating a second overlord or chain; no surviving relation references the absorbed ID.

The Noet record added by this PR is genuinely useful and appropriately captures the cross-cutting invariant. The implementation direction also looks correct; these requests are about making the fix structurally safe and checkable.

…er tests

Address review feedback on PR #39:

- Extract subject-relation primitives (subjectOf, subjectsOf, isSubjectPair,
  createSubjectRelation, breakSubjectRelation, SUBJECT_STATUS_LABEL,
  STATUS_PROFILE) into a new SubjectRelations.ts with no dependency on
  Merge.ts. Merge.ts now imports from there instead of from Subjects.ts,
  breaking the Merge <-> Subjects cycle (Subjects.ts still imports
  mergeEmpires for peaceful integration). Subjects.ts re-exports the
  primitives so existing call sites (Diplomacy, Tick, Simulation, UI panels)
  are unaffected.
- Add src/sim/Merge.test.ts using Node's built-in test runner, covering the
  five subject-transfer branches introduced by the merge fix: absorbed's
  subject status transfers, absorbed's own subjects transfer, a direct
  dominant<->absorbed tie collapses, and conflicting transfers on both the
  subject and overlord side liberate instead of violating the one-overlord/
  no-cycle invariant. Verified these tests fail against the pre-fix code and
  pass against the fix.
- New `npm run test:unit` script and CI step; exclude *.test.ts from the
  browser tsconfig project (it has no Node types) and note the new unit
  tests in CLAUDE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018o55CaUYZM5bNccWjsctnc

Copy link
Copy Markdown
Owner Author

Addressed both points:

  1. Extracted the subject-relation primitives (subjectOf, subjectsOf, isSubjectPair, createSubjectRelation, breakSubjectRelation, plus the status label/profile tables) into a new src/sim/SubjectRelations.ts, which has no dependency on Merge.ts. Merge.ts now imports from there instead of Subjects.ts, breaking the cycle (Subjects.ts still imports mergeEmpires for peaceful integration, and re-exports the primitives so existing call sites are unaffected).
  2. Added src/sim/Merge.test.ts (Node's built-in test runner, no new dependency) covering exactly the branches you listed: absorbed's subject status transfers to dominant, absorbed's own subjects transfer to dominant, a direct dominant↔absorbed tie collapses, and conflicting transfers on both the subject-side and overlord-side liberate instead of creating a chain/second overlord — each asserts no surviving relation references the absorbed empire id. I confirmed these tests fail against the pre-fix code and pass against the fix. Wired up npm run test:unit + a CI step for it.

All declared noet verify checks (build, lint, test:unit, headless report) are green.


Generated by Claude Code

@csheldrick
csheldrick merged commit f498391 into main Jul 15, 2026
2 checks passed
@csheldrick
csheldrick deleted the claude/galimulator-noet-workflow-ngfamm branch July 15, 2026 01:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants