From 35ecf36178ee31a8806802a4644674365d628f9e Mon Sep 17 00:00:00 2001 From: Chris Sheldrick Date: Wed, 15 Jul 2026 14:41:41 -0400 Subject: [PATCH 1/2] fix(sim): keep sandbox empire ids unique --- .noet/CONSTRAINTS.md | 1 + .noet/verification/VER-12.json | 15 +++++++++++++++ .noet/verification/VER-13.json | 15 +++++++++++++++ .noet/verification/VER-14.json | 15 +++++++++++++++ .noet/verification/VER-15.json | 16 ++++++++++++++++ src/sim/Simulation.test.ts | 32 ++++++++++++++++++++++++++++++++ src/sim/Simulation.ts | 5 ++++- 7 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 .noet/verification/VER-12.json create mode 100644 .noet/verification/VER-13.json create mode 100644 .noet/verification/VER-14.json create mode 100644 .noet/verification/VER-15.json create mode 100644 src/sim/Simulation.test.ts diff --git a/.noet/CONSTRAINTS.md b/.noet/CONSTRAINTS.md index c9bacc1..418b167 100644 --- a/.noet/CONSTRAINTS.md +++ b/.noet/CONSTRAINTS.md @@ -10,6 +10,7 @@ - Build must pass with `npm run build`. - New per-empire relational state (subjects, alliances, factions) needs transfer logic in both mergeEmpires and removeEmpireFromGalaxy. — mergeEmpires silently dropped subject/vassal bonds on merge; only removeEmpireFromGalaxy handled subject cleanup, so merges left stale/dangling ids. - Any new Id reference into state.events (modifiers, factions, subjects) must be added to gcEvents()'s referenced set in Events.ts. — gcEvents missed sourceEventId/historicalEventIds on modifiers, factions, subjects, silently corrupting grievance-age and legitimacy read-outs. +- Sandbox empire IDs must be checked against state.empires before insertion. — Same-tick merges reduce map cardinality, so count-derived IDs can overwrite a live empire and corrupt ownership links. ## Must-nots - Do not add a backend. diff --git a/.noet/verification/VER-12.json b/.noet/verification/VER-12.json new file mode 100644 index 0000000..b812d92 --- /dev/null +++ b/.noet/verification/VER-12.json @@ -0,0 +1,15 @@ +{ + "id": "VER-12", + "command": "npm run build", + "exitCode": 0, + "source": "recorded", + "startedAt": "2026-07-15T18:36:40.218Z", + "finishedAt": "2026-07-15T18:36:47.117Z", + "durationMs": 6898, + "summary": "exit 0", + "stdoutTail": "\n> galimulator-ng@0.0.0 build\n> tsc -b && vite build\n\n\u001b[36mvite v8.0.16 \u001b[32mbuilding client environment for production...\u001b[36m\u001b[39m\n\u001b[2K\rtransforming...✓ 48 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.47 kB │ gzip: 0.30 kB\ndist/assets/index-Hx8UZdve.css 15.95 kB │ gzip: 3.40 kB\ndist/assets/index-BKfrphMh.js 471.33 kB │ gzip: 138.68 kB\n\n\u001b[32m✓ built in 369ms\u001b[39m\n", + "codeState": { + "head": "0f261096cc597cf1847cb4b4550a8192b47516be", + "diffHash": "88ee1f25723278604d39b60ddc94bb16c66c0c303723954ca377f47eb977708b" + } +} diff --git a/.noet/verification/VER-13.json b/.noet/verification/VER-13.json new file mode 100644 index 0000000..6a16410 --- /dev/null +++ b/.noet/verification/VER-13.json @@ -0,0 +1,15 @@ +{ + "id": "VER-13", + "command": "npm run lint", + "exitCode": 0, + "source": "recorded", + "startedAt": "2026-07-15T18:36:47.476Z", + "finishedAt": "2026-07-15T18:37:00.411Z", + "durationMs": 12935, + "summary": "exit 0", + "stdoutTail": "\n> galimulator-ng@0.0.0 lint\n> eslint .\n\n", + "codeState": { + "head": "0f261096cc597cf1847cb4b4550a8192b47516be", + "diffHash": "88ee1f25723278604d39b60ddc94bb16c66c0c303723954ca377f47eb977708b" + } +} diff --git a/.noet/verification/VER-14.json b/.noet/verification/VER-14.json new file mode 100644 index 0000000..8f710e7 --- /dev/null +++ b/.noet/verification/VER-14.json @@ -0,0 +1,15 @@ +{ + "id": "VER-14", + "command": "npm run test:unit", + "exitCode": 0, + "source": "recorded", + "startedAt": "2026-07-15T18:37:00.776Z", + "finishedAt": "2026-07-15T18:37:02.234Z", + "durationMs": 1458, + "summary": "exit 0", + "stdoutTail": "\n> galimulator-ng@0.0.0 test:unit\n> node --import tsx --test src/sim/*.test.ts\n\n✔ gcEvents keeps an event still pointed at by a relationship modifier's sourceEventId (15.3925ms)\n✔ gcEvents keeps an event still pointed at by a faction's historicalEventIds (8.3924ms)\n✔ gcEvents keeps an event still pointed at by a subject relation's historicalEventIds (4.5457ms)\n✔ gcEvents still drops events with no surviving reference at all (3.0577ms)\n✔ absorbed empire's subject status transfers to the dominant empire (12.3859ms)\n✔ absorbed empire's own subjects transfer to the dominant empire as their new overlord (3.8125ms)\n✔ a direct subject tie between the dominant and absorbed empire collapses (3.4433ms)\n✔ a conflicting subject transfer is liberated instead of creating a second overlord (2.8264ms)\n✔ a conflicting overlord transfer is liberated instead of creating a subject chain (1.9716ms)\n✔ sandbox empire creation keeps IDs unique after a same-tick merge (21.5425ms)\nℹ tests 10\nℹ suites 0\nℹ pass 10\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 551.6987\n", + "codeState": { + "head": "0f261096cc597cf1847cb4b4550a8192b47516be", + "diffHash": "88ee1f25723278604d39b60ddc94bb16c66c0c303723954ca377f47eb977708b" + } +} diff --git a/.noet/verification/VER-15.json b/.noet/verification/VER-15.json new file mode 100644 index 0000000..5b69783 --- /dev/null +++ b/.noet/verification/VER-15.json @@ -0,0 +1,16 @@ +{ + "id": "VER-15", + "command": "npm run report", + "exitCode": 0, + "source": "recorded", + "startedAt": "2026-07-15T18:37:02.596Z", + "finishedAt": "2026-07-15T18:38:42.841Z", + "durationMs": 100245, + "summary": "exit 0", + "stdoutTail": "\n> galimulator-ng@0.0.0 report\n> tsx scripts/headless-report.ts\n\n# galimulator-ng headless report\n\nSeed: 42 · Stars: 400 · Starting empires: 12\nShape: spiral · Lanes: standard · Layout: classic\nInitial graph: avg degree 3.84 · max degree 7 · avg lane length 18.8\nMilestones: 1000, 3000 ticks\n\n## Tick 1000\n\n- Living empires: 36 (8/12 original founders survive — 67% survival)\n- Owned systems: 346/400\n- Largest power: Dark Dominion (36 systems, 9.0% of map)\n- Active wars now: 10 · cumulative wars declared: 105\n- Alliances: 2 blocs · Trade routes: 25\n- Artifacts: 21 · Markers/scars: 425\n- Dynasties: 56 (53 living) · People: 241 · Deepest ruler chain: 4 — Solsorlar Order (House of Belcer)\n- Grievances: 8 active across 6 feuds (5 empires) · avg +30 tension, peak +30 · 5% of historical modifiers · oldest 373t\n- Graph now: avg degree 3.84 · max degree 7 · avg lane length 18.8\n- Churn since last milestone: 31 empire births+deaths\n- Cumulative: 5 founded, 26 collapsed, 1 merged, 43 rebellions, 22 coups, 0 transcended\n- Religion: 6 faiths · 1 founded · 18 state conversions\n - Spread: Loraz Mysteries (51), Elcerdor Communion (50), Disciples of Solsol (36)\n- Monsters: 1 at large · 2 spawned · 1 slain · Oddities roaming: 0 · Crises/oddity events: 1\n- Factions: 10 active (33 worlds, 2 near uprising) · 25 formed · 4 uprisings\n- Subjects: 2 active (1 protectorate, 1 tributary) · 4 created · 0 rebellions · 0 integrations · 0 liberations\n- Quests: 5 launched · 2 completed\n- Specialist ships: 1 missionary, 2 dropship, 1 gunstation active\n- Careers: 17 promotions · 11 falls · top house: Jarax (prestige 51, legitimacy +1.00)\n\n## Tick 3000\n\n- Living empires: 42 (0/12 original founders survive — 0% survival)\n- Owned systems: 292/400\n- Largest power: Lost Independence Movement (39 systems, 9.8% of map)\n- Active wars now: 14 · cumulative wars declared: 780\n- Alliances: 3 blocs · Trade routes: 37\n- Artifacts: 22 · Markers/scars: 1412\n- Dynasties: 176 (169 living) · People: 687 · Deepest ruler chain: 6 — Rogue Resistance (House of Zanpri)\n- Grievances: 14 active across 9 feuds (8 empires) · avg +30 tension, peak +30 · 6% of historical modifiers · oldest 637t\n- Graph now: avg degree 3.84 · max degree 7 · avg lane length 18.8\n- Churn since last milestone: 211 empire births+deaths\n- Cumulative: 34 founded, 208 collapsed, 9 merged, 145 rebellions, 88 coups, 0 transcended\n- Religion: 10 faiths · 5 founded · 74 state conversions\n - Spread: Elcerdor Communion (151), Loraz Mysteries (87), Song of Xardenvor (79)\n- Monsters: 2 at large · 9 spawned · 7 slain · Oddities roaming: 0 · Crises/oddity events: 8\n- Factions: 11 active (24 worlds, 2 near uprising) · 173 formed · 72 uprisings\n- Subjects: 5 active (2 vassal, 2 tributary, 1 protectorate) · 28 created · 4 rebellions · 0 integrations · 9 liberations\n- Quests: 23 launched · 7 completed\n- Specialist ships: 2 science, 2 gunstation, 2 dropship, 1 disruptor, 1 missionary active\n- Careers: 25 promotions · 48 falls · top house: Varar (prestige 49, legitimacy +0.20)\n\n", + "stderrTail": "\n✓ Determinism check passed — identical replay from the same seed.\n", + "codeState": { + "head": "0f261096cc597cf1847cb4b4550a8192b47516be", + "diffHash": "88ee1f25723278604d39b60ddc94bb16c66c0c303723954ca377f47eb977708b" + } +} diff --git a/src/sim/Simulation.test.ts b/src/sim/Simulation.test.ts new file mode 100644 index 0000000..32b5d5a --- /dev/null +++ b/src/sim/Simulation.test.ts @@ -0,0 +1,32 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { Simulation } from "./Simulation"; + +test("sandbox empire creation keeps IDs unique after a same-tick merge", () => { + const sim = new Simulation({ seed: 42, numStars: 80, numEmpires: 6, ticksPerSecond: 4 }); + const initial = sim.getLiveState(); + const neutralSystems = Object.values(initial.systems).filter(sys => sys.ownerEmpireId === null); + const originalEmpireIds = Object.keys(initial.empires); + assert.ok(neutralSystems.length >= 2, "fixture needs two neutral systems"); + assert.ok(originalEmpireIds.length >= 2, "fixture needs two original empires to merge"); + + const firstSystem = neutralSystems[0]; + const secondSystem = neutralSystems[1]; + const firstId = sim.foundEmpireAtSystem(firstSystem.id); + assert.ok(firstId, "first sandbox empire should be founded"); + const firstDynastyId = sim.getLiveState().empires[firstId].dynastyId; + assert.ok(firstDynastyId, "first sandbox empire should have a dynasty"); + + sim.forceMerge(originalEmpireIds[0], originalEmpireIds[1]); + const secondId = sim.foundEmpireAtSystem(secondSystem.id); + assert.ok(secondId, "second sandbox empire should be founded"); + + const state = sim.getLiveState(); + assert.notEqual(secondId, firstId, "a reduced empire count must not reuse a live empire ID"); + assert.equal(state.systems[firstSystem.id].ownerEmpireId, firstId); + assert.ok(state.empires[firstId].ownedSystemIds.includes(firstSystem.id)); + assert.equal(state.empires[firstId].dynastyId, firstDynastyId); + assert.ok(state.dynasties?.[firstDynastyId].rulingEmpireIds.includes(firstId)); + assert.equal(state.systems[secondSystem.id].ownerEmpireId, secondId); + assert.ok(state.empires[secondId].ownedSystemIds.includes(secondSystem.id)); +}); diff --git a/src/sim/Simulation.ts b/src/sim/Simulation.ts index 4982784..8ef8b55 100644 --- a/src/sim/Simulation.ts +++ b/src/sim/Simulation.ts @@ -305,7 +305,10 @@ export class Simulation { const old = this.state.empires[oldOwnerId]; if (old) old.ownedSystemIds = old.ownedSystemIds.filter(id => id !== systemId); } - const id = `god-emp-${this.state.tick}-${Object.keys(this.state.empires).length}`; + const idPrefix = `god-emp-${this.state.tick}`; + let idSequence = Object.keys(this.state.empires).length; + let id = `${idPrefix}-${idSequence}`; + while (this.state.empires[id]) id = `${idPrefix}-${++idSequence}`; const cultureId = `culture-${id}`; const ideology = this.rng.pick(IDEOLOGIES); const govType = pickGovernmentType(this.rng, ideology); From bce6f3e638271c9e77fda5cacc6e859a03cbe5e3 Mon Sep 17 00:00:00 2001 From: Chris Sheldrick Date: Wed, 15 Jul 2026 14:51:32 -0400 Subject: [PATCH 2/2] chore: ignore Noet verification evidence --- .gitignore | 1 + .noet/verification/VER-1.json | 15 --------------- .noet/verification/VER-10.json | 15 --------------- .noet/verification/VER-11.json | 16 ---------------- .noet/verification/VER-12.json | 15 --------------- .noet/verification/VER-13.json | 15 --------------- .noet/verification/VER-14.json | 15 --------------- .noet/verification/VER-15.json | 16 ---------------- .noet/verification/VER-2.json | 15 --------------- .noet/verification/VER-3.json | 16 ---------------- .noet/verification/VER-4.json | 15 --------------- .noet/verification/VER-5.json | 15 --------------- .noet/verification/VER-6.json | 15 --------------- .noet/verification/VER-7.json | 16 ---------------- .noet/verification/VER-8.json | 15 --------------- .noet/verification/VER-9.json | 15 --------------- 16 files changed, 1 insertion(+), 229 deletions(-) delete mode 100644 .noet/verification/VER-1.json delete mode 100644 .noet/verification/VER-10.json delete mode 100644 .noet/verification/VER-11.json delete mode 100644 .noet/verification/VER-12.json delete mode 100644 .noet/verification/VER-13.json delete mode 100644 .noet/verification/VER-14.json delete mode 100644 .noet/verification/VER-15.json delete mode 100644 .noet/verification/VER-2.json delete mode 100644 .noet/verification/VER-3.json delete mode 100644 .noet/verification/VER-4.json delete mode 100644 .noet/verification/VER-5.json delete mode 100644 .noet/verification/VER-6.json delete mode 100644 .noet/verification/VER-7.json delete mode 100644 .noet/verification/VER-8.json delete mode 100644 .noet/verification/VER-9.json diff --git a/.gitignore b/.gitignore index 6a5efa4..b77c580 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ dist-ssr # Noet ephemeral session state (local only — not project artifacts) .noet/session/ +.noet/verification/ # Editor directories and files .vscode/* diff --git a/.noet/verification/VER-1.json b/.noet/verification/VER-1.json deleted file mode 100644 index c67d456..0000000 --- a/.noet/verification/VER-1.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "VER-1", - "command": "npm run build", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T00:41:07.257Z", - "finishedAt": "2026-07-15T00:41:13.081Z", - "durationMs": 5823, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 build\n> tsc -b && vite build\n\nvite v8.0.16 building client environment for production...\n\u001b[2K\rtransforming...✓ 47 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.46 kB │ gzip: 0.29 kB\ndist/assets/index-Hx8UZdve.css 15.95 kB │ gzip: 3.40 kB\ndist/assets/index-Puboqa6e.js 470.97 kB │ gzip: 138.61 kB\n\n✓ built in 304ms\n", - "codeState": { - "head": "d3a915b3e9449a13e1e9ee0754c54989091d0c71", - "diffHash": "c696deb40b9fb1ca139373d3da1d53610a0403074f4af89b196bce333b0fe644" - } -} diff --git a/.noet/verification/VER-10.json b/.noet/verification/VER-10.json deleted file mode 100644 index 4a47853..0000000 --- a/.noet/verification/VER-10.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "VER-10", - "command": "npm run test:unit", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T14:59:24.410Z", - "finishedAt": "2026-07-15T14:59:25.139Z", - "durationMs": 729, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 test:unit\n> node --import tsx --test src/sim/*.test.ts\n\nTAP version 13\n# Subtest: gcEvents keeps an event still pointed at by a relationship modifier's sourceEventId\nok 1 - gcEvents keeps an event still pointed at by a relationship modifier's sourceEventId\n ---\n duration_ms: 9.907447\n type: 'test'\n ...\n# Subtest: gcEvents keeps an event still pointed at by a faction's historicalEventIds\nok 2 - gcEvents keeps an event still pointed at by a faction's historicalEventIds\n ---\n duration_ms: 15.200138\n type: 'test'\n ...\n# Subtest: gcEvents keeps an event still pointed at by a subject relation's historicalEventIds\nok 3 - gcEvents keeps an event still pointed at by a subject relation's historicalEventIds\n ---\n duration_ms: 3.427037\n type: 'test'\n ...\n# Subtest: gcEvents still drops events with no surviving reference at all\nok 4 - gcEvents still drops events with no surviving reference at all\n ---\n duration_ms: 3.126353\n type: 'test'\n ...\n# Subtest: absorbed empire's subject status transfers to the dominant empire\nok 5 - absorbed empire's subject status transfers to the dominant empire\n ---\n duration_ms: 12.717783\n type: 'test'\n ...\n# Subtest: absorbed empire's own subjects transfer to the dominant empire as their new overlord\nok 6 - absorbed empire's own subjects transfer to the dominant empire as their new overlord\n ---\n duration_ms: 5.005493\n type: 'test'\n ...\n# Subtest: a direct subject tie between the dominant and absorbed empire collapses\nok 7 - a direct subject tie between the dominant and absorbed empire collapses\n ---\n duration_ms: 5.138644\n type: 'test'\n ...\n# Subtest: a conflicting subject transfer is liberated instead of creating a second overlord\nok 8 - a conflicting subject transfer is liberated instead of creating a second overlord\n ---\n duration_ms: 4.709811\n type: 'test'\n ...\n# Subtest: a conflicting overlord transfer is liberated instead of creating a subject chain\nok 9 - a conflicting overlord transfer is liberated instead of creating a subject chain\n ---\n duration_ms: 2.806086\n type: 'test'\n ...\n1..9\n# tests 9\n# suites 0\n# pass 9\n# fail 0\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 438.890456\n", - "codeState": { - "head": "f498391857766766a1ab9f4fb4bfdf9f5c7f6b0a", - "diffHash": "8adb2e39dd2f0354a3134159aa17a4298b84a8afe695bab4929c0f8738984ef9" - } -} diff --git a/.noet/verification/VER-11.json b/.noet/verification/VER-11.json deleted file mode 100644 index 9c7da85..0000000 --- a/.noet/verification/VER-11.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "VER-11", - "command": "npm run report", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T14:59:25.164Z", - "finishedAt": "2026-07-15T15:01:09.011Z", - "durationMs": 103847, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 report\n> tsx scripts/headless-report.ts\n\n# galimulator-ng headless report\n\nSeed: 42 · Stars: 400 · Starting empires: 12\nShape: spiral · Lanes: standard · Layout: classic\nInitial graph: avg degree 3.84 · max degree 7 · avg lane length 18.8\nMilestones: 1000, 3000 ticks\n\n## Tick 1000\n\n- Living empires: 36 (8/12 original founders survive — 67% survival)\n- Owned systems: 346/400\n- Largest power: Dark Dominion (36 systems, 9.0% of map)\n- Active wars now: 10 · cumulative wars declared: 105\n- Alliances: 2 blocs · Trade routes: 25\n- Artifacts: 21 · Markers/scars: 425\n- Dynasties: 56 (53 living) · People: 241 · Deepest ruler chain: 4 — Solsorlar Order (House of Belcer)\n- Grievances: 8 active across 6 feuds (5 empires) · avg +30 tension, peak +30 · 5% of historical modifiers · oldest 373t\n- Graph now: avg degree 3.84 · max degree 7 · avg lane length 18.8\n- Churn since last milestone: 31 empire births+deaths\n- Cumulative: 5 founded, 26 collapsed, 1 merged, 43 rebellions, 22 coups, 0 transcended\n- Religion: 6 faiths · 1 founded · 18 state conversions\n - Spread: Loraz Mysteries (51), Elcerdor Communion (50), Disciples of Solsol (36)\n- Monsters: 1 at large · 2 spawned · 1 slain · Oddities roaming: 0 · Crises/oddity events: 1\n- Factions: 10 active (33 worlds, 2 near uprising) · 25 formed · 4 uprisings\n- Subjects: 2 active (1 protectorate, 1 tributary) · 4 created · 0 rebellions · 0 integrations · 0 liberations\n- Quests: 5 launched · 2 completed\n- Specialist ships: 1 missionary, 2 dropship, 1 gunstation active\n- Careers: 17 promotions · 11 falls · top house: Jarax (prestige 51, legitimacy +1.00)\n\n## Tick 3000\n\n- Living empires: 42 (0/12 original founders survive — 0% survival)\n- Owned systems: 292/400\n- Largest power: Lost Independence Movement (39 systems, 9.8% of map)\n- Active wars now: 14 · cumulative wars declared: 780\n- Alliances: 3 blocs · Trade routes: 37\n- Artifacts: 22 · Markers/scars: 1412\n- Dynasties: 176 (169 living) · People: 687 · Deepest ruler chain: 6 — Rogue Resistance (House of Zanpri)\n- Grievances: 14 active across 9 feuds (8 empires) · avg +30 tension, peak +30 · 6% of historical modifiers · oldest 637t\n- Graph now: avg degree 3.84 · max degree 7 · avg lane length 18.8\n- Churn since last milestone: 211 empire births+deaths\n- Cumulative: 34 founded, 208 collapsed, 9 merged, 145 rebellions, 88 coups, 0 transcended\n- Religion: 10 faiths · 5 founded · 74 state conversions\n - Spread: Elcerdor Communion (151), Loraz Mysteries (87), Song of Xardenvor (79)\n- Monsters: 2 at large · 9 spawned · 7 slain · Oddities roaming: 0 · Crises/oddity events: 8\n- Factions: 11 active (24 worlds, 2 near uprising) · 173 formed · 72 uprisings\n- Subjects: 5 active (2 vassal, 2 tributary, 1 protectorate) · 28 created · 4 rebellions · 0 integrations · 9 liberations\n- Quests: 23 launched · 7 completed\n- Specialist ships: 2 science, 2 gunstation, 2 dropship, 1 disruptor, 1 missionary active\n- Careers: 25 promotions · 48 falls · top house: Varar (prestige 49, legitimacy +0.20)\n\n", - "stderrTail": "\n✓ Determinism check passed — identical replay from the same seed.\n", - "codeState": { - "head": "f498391857766766a1ab9f4fb4bfdf9f5c7f6b0a", - "diffHash": "8adb2e39dd2f0354a3134159aa17a4298b84a8afe695bab4929c0f8738984ef9" - } -} diff --git a/.noet/verification/VER-12.json b/.noet/verification/VER-12.json deleted file mode 100644 index b812d92..0000000 --- a/.noet/verification/VER-12.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "VER-12", - "command": "npm run build", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T18:36:40.218Z", - "finishedAt": "2026-07-15T18:36:47.117Z", - "durationMs": 6898, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 build\n> tsc -b && vite build\n\n\u001b[36mvite v8.0.16 \u001b[32mbuilding client environment for production...\u001b[36m\u001b[39m\n\u001b[2K\rtransforming...✓ 48 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.47 kB │ gzip: 0.30 kB\ndist/assets/index-Hx8UZdve.css 15.95 kB │ gzip: 3.40 kB\ndist/assets/index-BKfrphMh.js 471.33 kB │ gzip: 138.68 kB\n\n\u001b[32m✓ built in 369ms\u001b[39m\n", - "codeState": { - "head": "0f261096cc597cf1847cb4b4550a8192b47516be", - "diffHash": "88ee1f25723278604d39b60ddc94bb16c66c0c303723954ca377f47eb977708b" - } -} diff --git a/.noet/verification/VER-13.json b/.noet/verification/VER-13.json deleted file mode 100644 index 6a16410..0000000 --- a/.noet/verification/VER-13.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "VER-13", - "command": "npm run lint", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T18:36:47.476Z", - "finishedAt": "2026-07-15T18:37:00.411Z", - "durationMs": 12935, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 lint\n> eslint .\n\n", - "codeState": { - "head": "0f261096cc597cf1847cb4b4550a8192b47516be", - "diffHash": "88ee1f25723278604d39b60ddc94bb16c66c0c303723954ca377f47eb977708b" - } -} diff --git a/.noet/verification/VER-14.json b/.noet/verification/VER-14.json deleted file mode 100644 index 8f710e7..0000000 --- a/.noet/verification/VER-14.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "VER-14", - "command": "npm run test:unit", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T18:37:00.776Z", - "finishedAt": "2026-07-15T18:37:02.234Z", - "durationMs": 1458, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 test:unit\n> node --import tsx --test src/sim/*.test.ts\n\n✔ gcEvents keeps an event still pointed at by a relationship modifier's sourceEventId (15.3925ms)\n✔ gcEvents keeps an event still pointed at by a faction's historicalEventIds (8.3924ms)\n✔ gcEvents keeps an event still pointed at by a subject relation's historicalEventIds (4.5457ms)\n✔ gcEvents still drops events with no surviving reference at all (3.0577ms)\n✔ absorbed empire's subject status transfers to the dominant empire (12.3859ms)\n✔ absorbed empire's own subjects transfer to the dominant empire as their new overlord (3.8125ms)\n✔ a direct subject tie between the dominant and absorbed empire collapses (3.4433ms)\n✔ a conflicting subject transfer is liberated instead of creating a second overlord (2.8264ms)\n✔ a conflicting overlord transfer is liberated instead of creating a subject chain (1.9716ms)\n✔ sandbox empire creation keeps IDs unique after a same-tick merge (21.5425ms)\nℹ tests 10\nℹ suites 0\nℹ pass 10\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 551.6987\n", - "codeState": { - "head": "0f261096cc597cf1847cb4b4550a8192b47516be", - "diffHash": "88ee1f25723278604d39b60ddc94bb16c66c0c303723954ca377f47eb977708b" - } -} diff --git a/.noet/verification/VER-15.json b/.noet/verification/VER-15.json deleted file mode 100644 index 5b69783..0000000 --- a/.noet/verification/VER-15.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "VER-15", - "command": "npm run report", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T18:37:02.596Z", - "finishedAt": "2026-07-15T18:38:42.841Z", - "durationMs": 100245, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 report\n> tsx scripts/headless-report.ts\n\n# galimulator-ng headless report\n\nSeed: 42 · Stars: 400 · Starting empires: 12\nShape: spiral · Lanes: standard · Layout: classic\nInitial graph: avg degree 3.84 · max degree 7 · avg lane length 18.8\nMilestones: 1000, 3000 ticks\n\n## Tick 1000\n\n- Living empires: 36 (8/12 original founders survive — 67% survival)\n- Owned systems: 346/400\n- Largest power: Dark Dominion (36 systems, 9.0% of map)\n- Active wars now: 10 · cumulative wars declared: 105\n- Alliances: 2 blocs · Trade routes: 25\n- Artifacts: 21 · Markers/scars: 425\n- Dynasties: 56 (53 living) · People: 241 · Deepest ruler chain: 4 — Solsorlar Order (House of Belcer)\n- Grievances: 8 active across 6 feuds (5 empires) · avg +30 tension, peak +30 · 5% of historical modifiers · oldest 373t\n- Graph now: avg degree 3.84 · max degree 7 · avg lane length 18.8\n- Churn since last milestone: 31 empire births+deaths\n- Cumulative: 5 founded, 26 collapsed, 1 merged, 43 rebellions, 22 coups, 0 transcended\n- Religion: 6 faiths · 1 founded · 18 state conversions\n - Spread: Loraz Mysteries (51), Elcerdor Communion (50), Disciples of Solsol (36)\n- Monsters: 1 at large · 2 spawned · 1 slain · Oddities roaming: 0 · Crises/oddity events: 1\n- Factions: 10 active (33 worlds, 2 near uprising) · 25 formed · 4 uprisings\n- Subjects: 2 active (1 protectorate, 1 tributary) · 4 created · 0 rebellions · 0 integrations · 0 liberations\n- Quests: 5 launched · 2 completed\n- Specialist ships: 1 missionary, 2 dropship, 1 gunstation active\n- Careers: 17 promotions · 11 falls · top house: Jarax (prestige 51, legitimacy +1.00)\n\n## Tick 3000\n\n- Living empires: 42 (0/12 original founders survive — 0% survival)\n- Owned systems: 292/400\n- Largest power: Lost Independence Movement (39 systems, 9.8% of map)\n- Active wars now: 14 · cumulative wars declared: 780\n- Alliances: 3 blocs · Trade routes: 37\n- Artifacts: 22 · Markers/scars: 1412\n- Dynasties: 176 (169 living) · People: 687 · Deepest ruler chain: 6 — Rogue Resistance (House of Zanpri)\n- Grievances: 14 active across 9 feuds (8 empires) · avg +30 tension, peak +30 · 6% of historical modifiers · oldest 637t\n- Graph now: avg degree 3.84 · max degree 7 · avg lane length 18.8\n- Churn since last milestone: 211 empire births+deaths\n- Cumulative: 34 founded, 208 collapsed, 9 merged, 145 rebellions, 88 coups, 0 transcended\n- Religion: 10 faiths · 5 founded · 74 state conversions\n - Spread: Elcerdor Communion (151), Loraz Mysteries (87), Song of Xardenvor (79)\n- Monsters: 2 at large · 9 spawned · 7 slain · Oddities roaming: 0 · Crises/oddity events: 8\n- Factions: 11 active (24 worlds, 2 near uprising) · 173 formed · 72 uprisings\n- Subjects: 5 active (2 vassal, 2 tributary, 1 protectorate) · 28 created · 4 rebellions · 0 integrations · 9 liberations\n- Quests: 23 launched · 7 completed\n- Specialist ships: 2 science, 2 gunstation, 2 dropship, 1 disruptor, 1 missionary active\n- Careers: 25 promotions · 48 falls · top house: Varar (prestige 49, legitimacy +0.20)\n\n", - "stderrTail": "\n✓ Determinism check passed — identical replay from the same seed.\n", - "codeState": { - "head": "0f261096cc597cf1847cb4b4550a8192b47516be", - "diffHash": "88ee1f25723278604d39b60ddc94bb16c66c0c303723954ca377f47eb977708b" - } -} diff --git a/.noet/verification/VER-2.json b/.noet/verification/VER-2.json deleted file mode 100644 index df2c0e1..0000000 --- a/.noet/verification/VER-2.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "VER-2", - "command": "npm run lint", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T00:41:13.108Z", - "finishedAt": "2026-07-15T00:41:25.115Z", - "durationMs": 12007, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 lint\n> eslint .\n\n", - "codeState": { - "head": "d3a915b3e9449a13e1e9ee0754c54989091d0c71", - "diffHash": "c696deb40b9fb1ca139373d3da1d53610a0403074f4af89b196bce333b0fe644" - } -} diff --git a/.noet/verification/VER-3.json b/.noet/verification/VER-3.json deleted file mode 100644 index e5af1ba..0000000 --- a/.noet/verification/VER-3.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "VER-3", - "command": "npm run report", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T00:41:25.139Z", - "finishedAt": "2026-07-15T00:43:02.553Z", - "durationMs": 97414, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 report\n> tsx scripts/headless-report.ts\n\n# galimulator-ng headless report\n\nSeed: 42 · Stars: 400 · Starting empires: 12\nShape: spiral · Lanes: standard · Layout: classic\nInitial graph: avg degree 3.84 · max degree 7 · avg lane length 18.8\nMilestones: 1000, 3000 ticks\n\n## Tick 1000\n\n- Living empires: 36 (8/12 original founders survive — 67% survival)\n- Owned systems: 346/400\n- Largest power: Dark Dominion (36 systems, 9.0% of map)\n- Active wars now: 10 · cumulative wars declared: 105\n- Alliances: 2 blocs · Trade routes: 25\n- Artifacts: 21 · Markers/scars: 425\n- Dynasties: 56 (53 living) · People: 241 · Deepest ruler chain: 4 — Solsorlar Order (House of Belcer)\n- Grievances: 8 active across 6 feuds (5 empires) · avg +30 tension, peak +30 · 5% of historical modifiers · oldest 373t\n- Graph now: avg degree 3.84 · max degree 7 · avg lane length 18.8\n- Churn since last milestone: 31 empire births+deaths\n- Cumulative: 5 founded, 26 collapsed, 1 merged, 43 rebellions, 22 coups, 0 transcended\n- Religion: 6 faiths · 1 founded · 18 state conversions\n - Spread: Loraz Mysteries (51), Elcerdor Communion (50), Disciples of Solsol (36)\n- Monsters: 1 at large · 2 spawned · 1 slain · Oddities roaming: 0 · Crises/oddity events: 1\n- Factions: 10 active (33 worlds, 2 near uprising) · 25 formed · 4 uprisings\n- Subjects: 2 active (1 protectorate, 1 tributary) · 4 created · 0 rebellions · 0 integrations · 0 liberations\n- Quests: 5 launched · 2 completed\n- Specialist ships: 1 missionary, 2 dropship, 1 gunstation active\n- Careers: 17 promotions · 11 falls · top house: Jarax (prestige 51, legitimacy +1.00)\n\n## Tick 3000\n\n- Living empires: 42 (0/12 original founders survive — 0% survival)\n- Owned systems: 292/400\n- Largest power: Lost Independence Movement (39 systems, 9.8% of map)\n- Active wars now: 14 · cumulative wars declared: 780\n- Alliances: 3 blocs · Trade routes: 37\n- Artifacts: 22 · Markers/scars: 1412\n- Dynasties: 176 (169 living) · People: 687 · Deepest ruler chain: 6 — Rogue Resistance (House of Zanpri)\n- Grievances: 14 active across 9 feuds (8 empires) · avg +30 tension, peak +30 · 6% of historical modifiers · oldest 304t\n- Graph now: avg degree 3.84 · max degree 7 · avg lane length 18.8\n- Churn since last milestone: 211 empire births+deaths\n- Cumulative: 34 founded, 208 collapsed, 9 merged, 145 rebellions, 88 coups, 0 transcended\n- Religion: 10 faiths · 5 founded · 74 state conversions\n - Spread: Elcerdor Communion (151), Loraz Mysteries (87), Song of Xardenvor (79)\n- Monsters: 2 at large · 9 spawned · 7 slain · Oddities roaming: 0 · Crises/oddity events: 8\n- Factions: 11 active (24 worlds, 2 near uprising) · 173 formed · 72 uprisings\n- Subjects: 5 active (2 vassal, 2 tributary, 1 protectorate) · 28 created · 4 rebellions · 0 integrations · 9 liberations\n- Quests: 23 launched · 7 completed\n- Specialist ships: 2 science, 2 gunstation, 2 dropship, 1 disruptor, 1 missionary active\n- Careers: 25 promotions · 48 falls · top house: Varar (prestige 49, legitimacy +0.20)\n\n", - "stderrTail": "\n✓ Determinism check passed — identical replay from the same seed.\n", - "codeState": { - "head": "d3a915b3e9449a13e1e9ee0754c54989091d0c71", - "diffHash": "c696deb40b9fb1ca139373d3da1d53610a0403074f4af89b196bce333b0fe644" - } -} diff --git a/.noet/verification/VER-4.json b/.noet/verification/VER-4.json deleted file mode 100644 index dc7ada8..0000000 --- a/.noet/verification/VER-4.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "VER-4", - "command": "npm run build", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T01:01:29.541Z", - "finishedAt": "2026-07-15T01:01:35.075Z", - "durationMs": 5534, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 build\n> tsc -b && vite build\n\nvite v8.0.16 building client environment for production...\n\u001b[2K\rtransforming...✓ 48 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.46 kB │ gzip: 0.29 kB\ndist/assets/index-Hx8UZdve.css 15.95 kB │ gzip: 3.40 kB\ndist/assets/index-Bc5jnuWb.js 470.96 kB │ gzip: 138.61 kB\n\n✓ built in 270ms\n", - "codeState": { - "head": "0bd9af70d680be2e2c893b23f401c87584fe9e9b", - "diffHash": "b885d7b8fcba382df51077505b210f40d3e6d694973d8723ac2dbb067700b8e8" - } -} diff --git a/.noet/verification/VER-5.json b/.noet/verification/VER-5.json deleted file mode 100644 index 0acb609..0000000 --- a/.noet/verification/VER-5.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "VER-5", - "command": "npm run lint", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T01:01:35.104Z", - "finishedAt": "2026-07-15T01:01:46.594Z", - "durationMs": 11490, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 lint\n> eslint .\n\n", - "codeState": { - "head": "0bd9af70d680be2e2c893b23f401c87584fe9e9b", - "diffHash": "b885d7b8fcba382df51077505b210f40d3e6d694973d8723ac2dbb067700b8e8" - } -} diff --git a/.noet/verification/VER-6.json b/.noet/verification/VER-6.json deleted file mode 100644 index 9679832..0000000 --- a/.noet/verification/VER-6.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "VER-6", - "command": "npm run test:unit", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T01:01:46.618Z", - "finishedAt": "2026-07-15T01:01:47.222Z", - "durationMs": 604, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 test:unit\n> node --import tsx --test src/sim/*.test.ts\n\nTAP version 13\n# Subtest: absorbed empire's subject status transfers to the dominant empire\nok 1 - absorbed empire's subject status transfers to the dominant empire\n ---\n duration_ms: 10.796552\n type: 'test'\n ...\n# Subtest: absorbed empire's own subjects transfer to the dominant empire as their new overlord\nok 2 - absorbed empire's own subjects transfer to the dominant empire as their new overlord\n ---\n duration_ms: 3.407577\n type: 'test'\n ...\n# Subtest: a direct subject tie between the dominant and absorbed empire collapses\nok 3 - a direct subject tie between the dominant and absorbed empire collapses\n ---\n duration_ms: 2.027519\n type: 'test'\n ...\n# Subtest: a conflicting subject transfer is liberated instead of creating a second overlord\nok 4 - a conflicting subject transfer is liberated instead of creating a second overlord\n ---\n duration_ms: 2.768959\n type: 'test'\n ...\n# Subtest: a conflicting overlord transfer is liberated instead of creating a subject chain\nok 5 - a conflicting overlord transfer is liberated instead of creating a subject chain\n ---\n duration_ms: 1.938849\n type: 'test'\n ...\n1..5\n# tests 5\n# suites 0\n# pass 5\n# fail 0\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 371.44327\n", - "codeState": { - "head": "0bd9af70d680be2e2c893b23f401c87584fe9e9b", - "diffHash": "b885d7b8fcba382df51077505b210f40d3e6d694973d8723ac2dbb067700b8e8" - } -} diff --git a/.noet/verification/VER-7.json b/.noet/verification/VER-7.json deleted file mode 100644 index c79d86a..0000000 --- a/.noet/verification/VER-7.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "VER-7", - "command": "npm run report", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T01:01:47.249Z", - "finishedAt": "2026-07-15T01:03:24.869Z", - "durationMs": 97620, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 report\n> tsx scripts/headless-report.ts\n\n# galimulator-ng headless report\n\nSeed: 42 · Stars: 400 · Starting empires: 12\nShape: spiral · Lanes: standard · Layout: classic\nInitial graph: avg degree 3.84 · max degree 7 · avg lane length 18.8\nMilestones: 1000, 3000 ticks\n\n## Tick 1000\n\n- Living empires: 36 (8/12 original founders survive — 67% survival)\n- Owned systems: 346/400\n- Largest power: Dark Dominion (36 systems, 9.0% of map)\n- Active wars now: 10 · cumulative wars declared: 105\n- Alliances: 2 blocs · Trade routes: 25\n- Artifacts: 21 · Markers/scars: 425\n- Dynasties: 56 (53 living) · People: 241 · Deepest ruler chain: 4 — Solsorlar Order (House of Belcer)\n- Grievances: 8 active across 6 feuds (5 empires) · avg +30 tension, peak +30 · 5% of historical modifiers · oldest 373t\n- Graph now: avg degree 3.84 · max degree 7 · avg lane length 18.8\n- Churn since last milestone: 31 empire births+deaths\n- Cumulative: 5 founded, 26 collapsed, 1 merged, 43 rebellions, 22 coups, 0 transcended\n- Religion: 6 faiths · 1 founded · 18 state conversions\n - Spread: Loraz Mysteries (51), Elcerdor Communion (50), Disciples of Solsol (36)\n- Monsters: 1 at large · 2 spawned · 1 slain · Oddities roaming: 0 · Crises/oddity events: 1\n- Factions: 10 active (33 worlds, 2 near uprising) · 25 formed · 4 uprisings\n- Subjects: 2 active (1 protectorate, 1 tributary) · 4 created · 0 rebellions · 0 integrations · 0 liberations\n- Quests: 5 launched · 2 completed\n- Specialist ships: 1 missionary, 2 dropship, 1 gunstation active\n- Careers: 17 promotions · 11 falls · top house: Jarax (prestige 51, legitimacy +1.00)\n\n## Tick 3000\n\n- Living empires: 42 (0/12 original founders survive — 0% survival)\n- Owned systems: 292/400\n- Largest power: Lost Independence Movement (39 systems, 9.8% of map)\n- Active wars now: 14 · cumulative wars declared: 780\n- Alliances: 3 blocs · Trade routes: 37\n- Artifacts: 22 · Markers/scars: 1412\n- Dynasties: 176 (169 living) · People: 687 · Deepest ruler chain: 6 — Rogue Resistance (House of Zanpri)\n- Grievances: 14 active across 9 feuds (8 empires) · avg +30 tension, peak +30 · 6% of historical modifiers · oldest 304t\n- Graph now: avg degree 3.84 · max degree 7 · avg lane length 18.8\n- Churn since last milestone: 211 empire births+deaths\n- Cumulative: 34 founded, 208 collapsed, 9 merged, 145 rebellions, 88 coups, 0 transcended\n- Religion: 10 faiths · 5 founded · 74 state conversions\n - Spread: Elcerdor Communion (151), Loraz Mysteries (87), Song of Xardenvor (79)\n- Monsters: 2 at large · 9 spawned · 7 slain · Oddities roaming: 0 · Crises/oddity events: 8\n- Factions: 11 active (24 worlds, 2 near uprising) · 173 formed · 72 uprisings\n- Subjects: 5 active (2 vassal, 2 tributary, 1 protectorate) · 28 created · 4 rebellions · 0 integrations · 9 liberations\n- Quests: 23 launched · 7 completed\n- Specialist ships: 2 science, 2 gunstation, 2 dropship, 1 disruptor, 1 missionary active\n- Careers: 25 promotions · 48 falls · top house: Varar (prestige 49, legitimacy +0.20)\n\n", - "stderrTail": "\n✓ Determinism check passed — identical replay from the same seed.\n", - "codeState": { - "head": "0bd9af70d680be2e2c893b23f401c87584fe9e9b", - "diffHash": "b885d7b8fcba382df51077505b210f40d3e6d694973d8723ac2dbb067700b8e8" - } -} diff --git a/.noet/verification/VER-8.json b/.noet/verification/VER-8.json deleted file mode 100644 index a1a24c9..0000000 --- a/.noet/verification/VER-8.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "VER-8", - "command": "npm run build", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T14:59:06.043Z", - "finishedAt": "2026-07-15T14:59:11.887Z", - "durationMs": 5844, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 build\n> tsc -b && vite build\n\nvite v8.0.16 building client environment for production...\n\u001b[2K\rtransforming...✓ 48 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.46 kB │ gzip: 0.29 kB\ndist/assets/index-Hx8UZdve.css 15.95 kB │ gzip: 3.40 kB\ndist/assets/index-CeK-oW-A.js 471.27 kB │ gzip: 138.65 kB\n\n✓ built in 284ms\n", - "codeState": { - "head": "f498391857766766a1ab9f4fb4bfdf9f5c7f6b0a", - "diffHash": "8adb2e39dd2f0354a3134159aa17a4298b84a8afe695bab4929c0f8738984ef9" - } -} diff --git a/.noet/verification/VER-9.json b/.noet/verification/VER-9.json deleted file mode 100644 index 9d111c7..0000000 --- a/.noet/verification/VER-9.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "VER-9", - "command": "npm run lint", - "exitCode": 0, - "source": "recorded", - "startedAt": "2026-07-15T14:59:11.912Z", - "finishedAt": "2026-07-15T14:59:24.366Z", - "durationMs": 12454, - "summary": "exit 0", - "stdoutTail": "\n> galimulator-ng@0.0.0 lint\n> eslint .\n\n", - "codeState": { - "head": "f498391857766766a1ab9f4fb4bfdf9f5c7f6b0a", - "diffHash": "8adb2e39dd2f0354a3134159aa17a4298b84a8afe695bab4929c0f8738984ef9" - } -}