- commit
- bded761f404d04948e61ebe6e43a28da29a86118
- parent
- bee2e1734534b05cdfd4478bf0ded347e05a1435
- author
- Dami <dami.etoile@gmail.com>
| A | openspec/ | 2 | + |
| A | openspec/ | 51 | ++++++++++++++++++ |
| A | openspec/ | 40 | ++++++++++++++ |
| A | openspec/ | 83 | ++++++++++++++++++++++++++++++ |
| A | openspec/ | 30 | +++++++++++ |
@@ -0,0 +1,51 @@
1+## Context2+3+`createCatalog()` stores normalized pack records in a map and exposes stable4+level IDs. `getLevels()` currently deep-clones every level, `getLevel()` calls5+that full-catalog path before cloning the selected level again, and the stage6+keeps `levelNo` as the primary application identity. `main.js` also calls7+`refreshIndex()` from progress and win callbacks while packs may be replaced8+or removed.9+10+## Decisions11+12+### Stable identity is the source of truth13+14+The application shall store the active proof as `activeLevelId`, using the15+existing `${packId}:${localId}` value. A numeric index may remain an input16+convenience for level selection, but it is resolved once and not retained as17+the identity across catalog mutations. `packId` and `localId` remain unchanged18+in returned levels and shared data.19+20+When a selected level is replaced, the same stable ID remains active if the21+replacement still contains that local level. If it does not, selection falls22+back deterministically to the replacement pack's first level, then the first23+available level. Removing the active pack uses the same deterministic fallback.24+25+### Read boundaries and invalidation26+27+The catalog shall maintain normalized internal records and a mutation28+generation. Register, replace, and remove invalidate only derived lookup29+views affected by that generation. A single-level read resolves the requested30+record directly. Full-list reads retain their current fresh-clone behavior so31+callers cannot mutate catalog state through returned objects.32+33+The optimization must not return a mutable cached object to callers. Any34+internal cache stores canonical normalized data or immutable derived metadata;35+public reads still clone at the boundary. Tests shall prove that mutating one36+returned level, pack, or list does not affect a later read.37+38+### Application handoffs39+40+`loadLevel()` resolves and records the stable level ID before creating the41+game. Progress writes use that level's `packId` and `localId`. Win and42+continue callbacks capture the stable ID and verify it still resolves before43+showing a transition. Catalog replacement/removal refreshes the index and44+re-resolves the active ID before deciding whether to load a fallback.45+46+### Compatibility and failure behavior47+48+Existing methods and return shapes remain compatible. Unknown stable IDs still49+return `undefined` from reads and `setActiveLevel` returns `false`. A removed50+active ID is never left pointing at an unavailable level. Level ordering stays51+registration order; identity resolution does not depend on array position.@@ -0,0 +1,40 @@
1+## Why2+3+The catalog already assigns stable `${packId}:${localId}` level IDs, but the4+application still carries the active proof as a mutable array index while5+catalog access repeatedly clones every pack and level. Pack replacement,6+removal, progress updates, and win handoff can therefore resolve different7+levels after the catalog changes, while ordinary UI refreshes pay a full8+catalog clone cost.9+10+## What Changes11+12+- Make the canonical active-level identity the existing stable global level ID,13+ while retaining numeric index arguments for compatibility.14+- Define catalog read boundaries that resolve one level or the active level by15+ stable ID and invalidate derived views after register, replace, and remove.16+- Keep existing public clone and mutation-safety guarantees for `getLevels`,17+ `getLevel`, and `getPack`; add focused cache and identity tests before18+ changing callers.19+- Migrate application handoff, progress marking, level replacement, and pack20+ removal flows to re-resolve by stable ID instead of retaining a stale index.21+22+This change does not alter pack IDs, local level IDs, sharing payloads, save23+keys, solver state, or level ordering semantics.24+25+## Capabilities26+27+### New Capabilities28+29+- `stable-active-level-identity`30+31+### Modified Capabilities32+33+None.34+35+## Impact36+37+`src/catalog.js`, `src/main.js`, and their focused tests will change. Existing38+catalog methods remain compatible and continue returning caller-owned data.39+The implementation may add internal generation counters or immutable records,40+but those details are not exposed in pack or level payloads.@@ -0,0 +1,83 @@
1+## ADDED Requirements2+3+### Requirement: The active level SHALL use a stable catalog identity4+5+The application SHALL use the existing `${packId}:${localId}` level ID as the6+canonical active-level identity. Numeric indexes MAY select a level but SHALL7+not be used as the durable identity across catalog mutations.8+9+#### Scenario: Selection survives unrelated registration10+11+- **WHEN** the active level is selected and another pack is registered before12+ it13+- **THEN** the same stable active level ID remains selected and resolves to the14+ same level15+16+#### Scenario: Replacement preserves an existing local level17+18+- **WHEN** a pack is replaced with the same pack ID and still contains the19+ active local level ID20+- **THEN** the active stable ID is preserved and resolves to the replacement21+ level22+23+#### Scenario: Replacement removes the active local level24+25+- **WHEN** a pack replacement omits the active local level26+- **THEN** selection falls back to the replacement pack's first level or the27+ first available catalog level, and no stale ID remains active28+29+### Requirement: Catalog reads SHALL preserve clone safety without repeated full clones30+31+Single-level and active-level reads SHALL resolve directly from normalized32+catalog records. Full-list compatibility methods SHALL continue returning33+fresh caller-owned data, while internal derived views SHALL be invalidated on34+pack register, replacement, and removal.35+36+#### Scenario: A caller cannot mutate a cached catalog record37+38+- **WHEN** a caller mutates a level returned by a catalog read39+- **THEN** a later read returns the normalized authored value40+41+#### Scenario: Replacement invalidates level and pack views42+43+- **WHEN** a pack is replaced with changed metadata or levels44+- **THEN** subsequent reads return only replacement data and no stale cached45+ level, pack, or level count46+47+#### Scenario: Removal invalidates active and list views48+49+- **WHEN** an imported pack is removed50+- **THEN** subsequent reads omit its levels and the active ID resolves to a51+ remaining level or `null`52+53+### Requirement: Progress and win handoffs SHALL resolve by stable identity54+55+Progress marking, win promotion, continue navigation, and pack refresh SHALL56+re-resolve the captured stable level ID before acting. A catalog mutation or57+level transition SHALL not allow an old numeric index or reused authored ID to58+promote the wrong level.59+60+#### Scenario: A win callback cannot promote a replaced level61+62+- **WHEN** a level is won and its pack is replaced before the delayed win63+ handoff runs64+- **THEN** the callback promotes only the still-resolved stable level ID or65+ safely abandons the stale handoff66+67+#### Scenario: Progress remains attached to pack and local IDs68+69+- **WHEN** levels are reordered by registration or an unrelated pack changes70+- **THEN** solved progress remains attached to the original `packId` and71+ `localId`72+73+### Requirement: Cache invalidation SHALL be observable at mutation boundaries74+75+Catalog mutations SHALL invalidate all derived active-level, pack-summary, and76+level-lookup views that can contain changed records. The invalidation SHALL77+occur before the mutation result is returned to application callers.78+79+#### Scenario: Replace refreshes summaries immediately80+81+- **WHEN** a pack replacement changes its name, world metadata, or level count82+- **THEN** the next pack and level reads expose replacement summaries without a83+ refresh-only workaround@@ -0,0 +1,30 @@
1+## 1. Catalog identity and read boundaries2+3+- [ ] 1.1 Add stable-ID resolution helpers while preserving existing catalog4+ method signatures and return shapes.5+- [ ] 1.2 Track active level by `${packId}:${localId}` and define replacement,6+ removal, and empty-catalog fallback behavior.7+- [ ] 1.3 Replace repeated full-catalog work for single-level and active-level8+ reads with direct normalized-record lookup and safe boundary cloning.9+- [ ] 1.4 Add cache/generation invalidation for register, replace, and remove;10+ prove summaries and level counts never remain stale.11+12+## 2. Application handoffs and progress13+14+- [ ] 2.1 Migrate level loading, continue navigation, progress marking, and win15+ promotion to stable IDs while retaining numeric selection compatibility.16+- [ ] 2.2 Handle pack replacement/removal during delayed win and gift/workshop17+ handoffs without promoting a stale level.18+- [ ] 2.3 Verify solved progress remains keyed by pack and local level IDs after19+ registration order changes.20+21+## 3. Regression and verification22+23+- [ ] 3.1 Add catalog tests for replace, remove, active fallback, clone safety,24+ direct reads, and cache invalidation.25+- [ ] 3.2 Add application tests for progress and delayed Win handoffs across26+ replacement/removal and reused local IDs.27+- [ ] 3.3 Run the focused catalog/application tests, full Node suite, production28+ build, and browser pack import/share flows.29+- [ ] 3.4 Run strict OpenSpec validation and record the measured catalog read30+ improvement before archiving the change.