- W1: 02-RESEARCH.md Open Questions section now flagged (RESOLVED) and each
Recommendation prefixed with RESOLVED + a pointer to the artifact that
codified the resolution.
- W8: 02-02 Plan example moves `import type { GrowthStage }` to the top of
commands.ts (alongside the other type-only imports) and drops the trailing
parenthetical apology — the executor doesn't need to fix anything.
Two distinct fields with strict separation:
- lastTickAt: wall-clock milliseconds. Written ONLY at saveSync time by
the application layer. The sim NEVER writes this field.
computeOfflineCatchup uses it as the wall-clock anchor.
- tickCount: monotonic sim-internal counter (one per simulate() call).
Used for STRY-10 narrative gating that must be immune to wall-clock
manipulation. The sim writes this field; the application layer reads
it via simAdapter.applyTickCount.
Changes:
02-01: SimState + V1Payload gain `tickCount: number`; migrations[1]
defaults to 0; GardenSlice exposes tickCount + lastTickAt + setters;
simAdapter exposes applyTickCount; tests assert the round-trip.
02-02: simulateOneTick increments next.tickCount + 1 (not lastTickAt:
currentTick); Garden scene's SimState snapshot reads lastTickAt
through from store and writes tickCount: this.currentTick locally;
acceptance_criteria forbids `lastTickAt: this.*` in the sim and scene.
02-05: buildPayloadFromStore now persists tickCount (from store);
hydrateStoreFromPayload restores it via state.setTickCount.
This unblocks the offline-catchup math: computeOfflineCatchup(payload.lastTickAt,
nowMs) now reliably reads wall-clock ms because the sim never overwrites it
with a tick counter.