10-simulation-kind.md §6.1 (design/20-contract.md:2852-2854):
Stacking is governed by StatusEffect.stacking (§2.3): a second effect from the same sourceId with "refresh" replaces the first and resets its expiry; "stack" adds a second, independent layer. Two different sources always stack.
Flagged by qodo-code-review on PR #194 against src/engine/src/kinds/simulation/modifiers.ts:26 (collectModifiers): the collector applies every matching modifier without regard to StatusEffect.stacking, so two same-sourceId "refresh" effects in activeEffects would both contribute instead of the newer one replacing the older.
Classified out of scope for W51/PR #194 rather than a defect there, because the fix cannot live in collectModifiers. That function is a pure, read-time collector over an already-given activeEffects list — it has no way to "reset expiry" (a mutation of a stored StatusEffect). The stacking rule is inherently an insertion-time invariant: whatever adds a new StatusEffect to activeEffects (an item purchase, job/course effect, event, etc.) must, when the new effect's sourceId matches an existing one with stacking: "refresh", remove/replace the older entry rather than leave both in the list.
No such "apply effect" code exists yet anywhere in src/engine/src/kinds/simulation/ — W51's own commit message says as much ("No job/course content is wired... out of scope"). initial.ts sets activeEffects: [] and nothing else currently writes to it. This is a gap for whichever future slice adds the mechanism that inserts a StatusEffect into activeEffects (job/course/item/event application).
Suggested fix, when that slice is cut: at the insertion site, before pushing the new StatusEffect, drop any existing entry in activeEffects whose sourceId matches and whose stacking is "refresh". Add tests covering same-source refresh (replaces), same-source stack (both persist), and different-source (always stack) per the contract text above.
Done when
10-simulation-kind.md§6.1 (design/20-contract.md:2852-2854):Flagged by qodo-code-review on PR #194 against
src/engine/src/kinds/simulation/modifiers.ts:26(collectModifiers): the collector applies every matching modifier without regard toStatusEffect.stacking, so two same-sourceId"refresh"effects inactiveEffectswould both contribute instead of the newer one replacing the older.Classified out of scope for W51/PR #194 rather than a defect there, because the fix cannot live in
collectModifiers. That function is a pure, read-time collector over an already-givenactiveEffectslist — it has no way to "reset expiry" (a mutation of a storedStatusEffect). The stacking rule is inherently an insertion-time invariant: whatever adds a newStatusEffecttoactiveEffects(an item purchase, job/course effect, event, etc.) must, when the new effect'ssourceIdmatches an existing one withstacking: "refresh", remove/replace the older entry rather than leave both in the list.No such "apply effect" code exists yet anywhere in
src/engine/src/kinds/simulation/— W51's own commit message says as much ("No job/course content is wired... out of scope").initial.tssetsactiveEffects: []and nothing else currently writes to it. This is a gap for whichever future slice adds the mechanism that inserts aStatusEffectintoactiveEffects(job/course/item/event application).Suggested fix, when that slice is cut: at the insertion site, before pushing the new
StatusEffect, drop any existing entry inactiveEffectswhosesourceIdmatches and whosestackingis"refresh". Add tests covering same-sourcerefresh(replaces), same-sourcestack(both persist), and different-source (always stack) per the contract text above.Done when
refreshreplacement /stackindependence per §6.1refresh, same-sourcestack, and different-source effects