- BOOTSTRAP__AI_Agent_Quickstart.md: new category 9 under Common Mistakes pointing to today's incident (local/remote config sync pitfalls) - REFERENCE__Common_Agent_Mistakes.md: three new entries — 16) local "shadow field" silently bypasses an admin-synced master field 17) SWR await after a write does not mean dependent caches are fresh 18) conditional/stateful sync gates are effectively undebuggable - PROJECT__Stores_Svelte5_Migration.md: updated the canonical Migration Pattern example to stamp __version in the serializer (the old example silently didn't, which is exactly what caused the leads_loc/pres_mgmt_loc bugs fixed today); flagged badges_loc/launcher_loc/events_auth_loc as not yet fixed (dormant, not harmful) and idaa_loc as the next migration that should get this from day one - TODO__Agents.md: cross-referenced today's Pres Mgmt config sync overhaul in the LCI October restoration section for context, without touching the open items in that list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
156 lines
7.5 KiB
Markdown
156 lines
7.5 KiB
Markdown
# Project: Svelte 4 Store → Svelte 5 State Migration
|
|
|
|
**Status:** Events module — COMPLETE. Core / IDAA — In Progress (field cleanup done, PersistedState pending).
|
|
**Priority:** High
|
|
**Created:** 2026-03-30
|
|
**Last Updated:** 2026-06-11
|
|
|
|
---
|
|
|
|
## Background
|
|
|
|
All core Aether stores were built with `svelte-persisted-store` (Svelte 4 contract). This provides
|
|
coarse reactivity: any write to any field notifies *all* subscribers and re-serializes the entire
|
|
object. For large stores like `ae_loc` and `ae_events_loc`, this caused unnecessary re-renders and
|
|
was the root cause of the IDAA "Access Denied" corruption bug (a bootstrap write to `ae_loc` would
|
|
overwrite `authenticated_access` if a persisted value was slightly different, corrupting IDAA
|
|
member state stored in the same key).
|
|
|
|
The migration target: replace all `persisted()` stores with `runed`'s `PersistedState`, which uses
|
|
Svelte 5 fine-grained reactivity — a write to one field only triggers effects that read that field.
|
|
|
|
---
|
|
|
|
## Completed: Events Module (2026-06-11)
|
|
|
|
All `ae_events_stores` sub-modules have been promoted to their own `PersistedState` stores and
|
|
`events_loc` (the old `persisted()` store) has been **fully retired**.
|
|
|
|
| Store | File | localStorage key | Status |
|
|
|---|---|---|---|
|
|
| `badges_loc` | `ae_events_stores__badges.svelte.ts` | `ae_badges_loc` | ✅ Done (2026-04-02) |
|
|
| `leads_loc` | `ae_events_stores__leads.svelte.ts` | `ae_leads_loc` | ✅ Done (2026-04-03) |
|
|
| `pres_mgmt_loc` | `ae_events_stores__pres_mgmt.svelte.ts` | `ae_pres_mgmt_loc` | ✅ Done (2026-04-03) |
|
|
| `launcher_loc` | `ae_events_stores__launcher.svelte.ts` | `ae_launcher_loc` | ✅ Done (2026-06-11) |
|
|
| `events_auth_loc` | `ae_events_stores__auth.svelte.ts` | `ae_events_auth_loc` | ✅ Done (2026-06-11) |
|
|
| `events_loc` | *(retired)* | `ae_events_loc` | ✅ Store removed (2026-06-11) |
|
|
|
|
`ae_events_stores.ts` now only exports `events_sess` (in-memory writable, no migration needed),
|
|
`events_slct`, `events_trig`, `events_trig_kv`, `events_trigger`, and the `EVENTS_MODULE_TITLE`
|
|
constant. The file no longer imports `svelte-persisted-store`.
|
|
|
|
`store_versions.ts` still calls `_check_and_wipe('ae_events_loc', AE_EVENTS_LOC_VERSION)` to clean
|
|
old data out of users' browsers — this is intentional and should be kept for at least one year.
|
|
|
|
---
|
|
|
|
## In Progress / Remaining: `ae_stores.ts` and `ae_idaa_stores.ts`
|
|
|
|
Both stores had their unused default properties pruned (2026-06-11), reducing migration scope:
|
|
|
|
**`ae_loc`** (in `ae_stores.ts`) — still `persisted('ae_loc', ...)`:
|
|
- Remaining fields: auth/identity, theme, permissions, ui config, file upload tracking, query prefs
|
|
- This is the highest-impact remaining migration — used in nearly every route
|
|
- Root cause of IDAA "Access Denied" bug (coarse write during bootstrap stomps on permission fields)
|
|
|
|
**`idaa_loc`** (in `ae_idaa_stores.ts`) — still `persisted('ae_idaa_loc', ...)`:
|
|
- Remaining fields: `novi_uuid/verified/ts`, `novi_admin_li/trusted_li`, `archives/bb/recovery_meetings` sub-objects
|
|
- Fields pruned (2026-06-11): `ds`, `idaa_cfg_json`, top-level `qry__*`, `novi_*_base_url`, `novi_rate_limited_until`
|
|
|
|
---
|
|
|
|
## Migration Pattern (Established)
|
|
|
|
Each sub-store follows this pattern, using the `badges` store as the canonical reference:
|
|
|
|
### 1. Defaults file (`*_defaults.ts`)
|
|
Define the shape and defaults as a plain TypeScript object (and interface if complex):
|
|
|
|
```ts
|
|
export interface BadgesLocState { ... }
|
|
export const badges_loc_defaults: BadgesLocState = { ... };
|
|
```
|
|
|
|
### 2. Store file (`*.svelte.ts`)
|
|
|
|
**Updated 2026-06-16 — stamp `__version` in the serializer.** The original pattern below (bare
|
|
`JSON.stringify`) looked fine but has a real failure mode: `store_versions.ts`'s
|
|
`_check_and_wipe()` compares a `__version` field *inside* the stored JSON, and a bare
|
|
`JSON.stringify` never writes one. Two consequences were found in production code:
|
|
`pres_mgmt_loc`'s version bump was silently a no-op (never wired into `_check_and_wipe` at
|
|
all), and `leads_loc`'s *was* wired in, so `parsed?.__version` was always `undefined` —
|
|
always failing the comparison — wiping `ae_leads_loc` on **every single page load**. Both
|
|
fixed 2026-06-16. `badges_loc`, `launcher_loc`, and `events_auth_loc` still use the old bare
|
|
pattern below and are **not yet wired into `_check_and_wipe`** — not actively harmful (the
|
|
wipe never runs), but apply this fix before wiring any of them in.
|
|
|
|
```ts
|
|
import { PersistedState } from 'runed';
|
|
import { badges_loc_defaults } from './ae_events_stores__badges_defaults';
|
|
import { AE_BADGES_LOC_VERSION } from './store_versions';
|
|
|
|
export const badges_loc = new PersistedState('ae_badges_loc', badges_loc_defaults, {
|
|
serializer: {
|
|
// Stamp __version on every write so store_versions.ts's _check_and_wipe() can
|
|
// detect a breaking schema change and clear stale browsers on next load. This
|
|
// import also guarantees store_versions.ts's wipe side-effect runs before this
|
|
// PersistedState reads from localStorage (ES module execution order).
|
|
serialize: (value) =>
|
|
JSON.stringify({ ...value, __version: AE_BADGES_LOC_VERSION }),
|
|
// Merge with defaults so new fields added after first session get their defaults,
|
|
// and strip __version back out so it doesn't pollute the typed state object.
|
|
deserialize: (raw: string) => {
|
|
const { __version, ...stored } = JSON.parse(raw);
|
|
return { ...badges_loc_defaults, ...stored };
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
Then in `store_versions.ts`'s startup block:
|
|
```ts
|
|
_check_and_wipe('ae_badges_loc', AE_BADGES_LOC_VERSION);
|
|
```
|
|
|
|
See `ae_events_stores__pres_mgmt.svelte.ts` and `ae_events_stores__leads.svelte.ts` for the
|
|
two real, currently-correct examples of this pattern.
|
|
|
|
### 3. Consumer syntax
|
|
```ts
|
|
// Import (note .svelte extension, not .svelte.ts):
|
|
import { badges_loc } from '$lib/stores/ae_events_stores__badges.svelte';
|
|
|
|
// Read:
|
|
badges_loc.current.fulltext_search_qry_str
|
|
|
|
// Write (fine-grained — only triggers effects that read this field):
|
|
badges_loc.current.fulltext_search_qry_str = 'hello';
|
|
|
|
// Bulk reset:
|
|
badges_loc.current = { ...badges_loc_defaults };
|
|
```
|
|
|
|
### Key differences from old `svelte-persisted-store` pattern:
|
|
| | Old (`persisted()`) | New (`PersistedState`) |
|
|
|---|---|---|
|
|
| Import | `import { events_loc } from '...ae_events_stores'` | `import { badges_loc } from '...ae_events_stores__badges.svelte'` |
|
|
| Read | `$events_loc.badges.field` | `badges_loc.current.field` |
|
|
| Write | `$events_loc.badges.field = x` | `badges_loc.current.field = x` |
|
|
| Reactivity | Coarse — entire store notified | Fine-grained — only affected fields |
|
|
| In `$effect` | Subscribes to entire store | Only subscribes to fields you read |
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. **`idaa_loc` → PersistedState** — Highest priority for IDAA stability. Promotes `novi_uuid/verified`,
|
|
`archives`, `bb`, `recovery_meetings` sub-objects to their own stores following the same pattern.
|
|
Primary benefit: eliminates the IDAA "Access Denied" corruption from `ae_loc` bootstrap writes.
|
|
Use the `__version`-stamping serializer from the start (see Migration Pattern above) and wire
|
|
it into `_check_and_wipe()` immediately — `idaa_loc` holds auth state, so a real wipe-on-schema-
|
|
change matters more here than anywhere else this pattern has been applied so far.
|
|
|
|
2. **`ae_loc` → PersistedState** — Largest scope (~every route in the app). Defer until after
|
|
`idaa_loc` is done. Consider extracting `auth_loc` (the identity/permission fields) as the
|
|
first sub-store since those are the fields implicated in the IDAA corruption bug.
|