Commit graph

11 commits

Author SHA1 Message Date
Till JS
d5d2b6fcf8 docs(sync): document F1 + F3 commit-log corrections (Punkt 9 + 11)
Two commit artifacts from the multi-terminal sprint phase that can't be
fixed without destructive rebase + force-push of 28+ already-pushed
commits on origin/main:

- Punkt 9: F1 commit (7766ea502) shipped under the misleading title
  'docs(plans): mark llm-fallback-aliases SHIPPED' due to a parallel
  terminal session. The real F1 implementation (27 files: shared-ai/
  field-meta.ts, database.ts hooks, sync.ts wire format, mana-sync Go
  schema reset, mana-ai projections, MCP sync-db.ts) is intact in that
  commit despite the title.

- Punkt 11: F3 commit (6bb9d77be) accidentally bundled a one-line
  DragType addition (`| 'last'` in packages/shared-ui/src/dnd/types.ts)
  that belongs to the Lasts module, not to F3's updatedAt sweep. The
  regex codemod ran across the same files and pulled the change in.
  Functionally harmless (DragType union is additive) but semantically
  two unrelated changes share one commit.

Both commits are now annotated via local Git tags
(`sync-field-meta-overhaul-F1`, `sync-field-meta-overhaul-F3`) so a
future reader can find them by name regardless of the commit titles.
Tags are local — `git push --tags` makes them visible upstream.

Plan: docs/plans/sync-field-meta-overhaul.md "Commit-Log Corrections"
section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 01:56:38 +02:00
Till JS
275130f8a6 test(sync): cross-cutting integration tests for field-meta overhaul (Punkt 12)
Six new tests in sync.test.ts under the "field-meta overhaul (F1-F4-fu)"
block, verifying the architectural promises of the 2026-04-26 sync
field-meta overhaul end-to-end:

- deriveUpdatedAt returns max(__fieldMeta[*].at)
- deriveUpdatedAt gracefully handles legacy / null records
- Dexie creating-hook stamps __fieldMeta + _updatedAtIndex on every
  local write
- Dexie updating-hook bumps __fieldMeta only for changed fields and
  syncs _updatedAtIndex with the latest at
- SYSTEM_BOOTSTRAP-stamped local insert produces origin='system' (the
  fallback path in userContextStore + kontextStore)
- Bootstrap-twin race scenario: local SYSTEM_BOOTSTRAP row + later
  server insert collapses via field-LWW with no conflict surface

Also re-exports SYSTEM_BOOTSTRAP from $lib/data/events/actor for
parity with the other SYSTEM_* sentinels.

35/35 sync.test.ts pass (29 prior + 6 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 01:54:35 +02:00
Till JS
421a49a2a8 docs(sync): close Punkt 5 audit — backend updated_at columns are not sync orphans
Survey of all 17 backend Drizzle schemas (mana-mail/-media/-auth/
-analytics/-research/-events/-subscriptions/-credits + apps/api/
{unlisted,website,traces,presi,todo}):

- 3 columns are actively read by service code:
  - research.providerConfigs.updatedAt — explicit write + DTO field
  - unlisted.snapshots.updatedAt — read in public response
  - website.customDomains.updatedAt — read in DNS-status response
- 14 columns are AUTO-ONLY: Drizzle stamps them via defaultNow() /
  $onUpdate(), no service code reads them.

But the AUTO-ONLY columns are NOT sync-orphans — they're standard
Drizzle audit-timestamp convention, useful for Postgres-level forensics
(`ORDER BY updated_at DESC` to find recently-modified rows during
debugging). F3's plan note ("pure server-internal columns, not touched")
correctly identified them. No cleanup is needed.

Closing the audit item with rationale documented in
docs/plans/sync-field-meta-overhaul.md and DATA_LAYER_AUDIT.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 01:47:41 +02:00
Till JS
ae6a14fb76 feat(shared-ai): SYSTEM_BOOTSTRAP system source — fallback inserts now stamp origin='system'
The race-window `getOrCreateLocalDoc()` fallback in userContextStore +
kontextStore stays (without it, a write that lands between "endpoint
provisioned the singleton in mana_sync" and "first pull landed it in
IndexedDB" would hit `update(missing-id, diff)` — a Dexie no-op that
silently swallows the user's edit). But it was semantically lying: the
insert stamped `origin='user'` even though the row is logically a
client-side replica of the server-side bootstrap.

This commit adds `SYSTEM_BOOTSTRAP = 'system:bootstrap'` to
`@mana/shared-ai` and wraps the two fallback inserts in
`runAsAsync(makeSystemActor(SYSTEM_BOOTSTRAP), ...)`. The Dexie hook
now stamps `origin: 'system'` on the empty-row insert — structurally
identical to the row mana-auth's bootstrap-singletons.ts writes. When
the server's pull arrives later both sides carry the same origin and
the conflict-gate stays quiet. The user's subsequent writes still
stamp `origin: 'user'` on the changed fields.

Plan: docs/plans/sync-field-meta-overhaul.md (F4-fu Fallback-Origin row).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 01:44:30 +02:00
Till JS
099cac4a01 feat(auth): explicit bootstrap-singletons endpoint + idempotent functions (F4 robust)
The F4 server-side singleton bootstrap was fire-and-forget at signup
time — a transient mana_sync outage during registration would leave the
user with no singleton and only the in-store `getOrCreateLocalDoc()`
fallback to race on the first write. The signup-hook is still the
happy-path zero-latency bootstrap; this commit adds a deliberate
reconciliation path that converges on every boot.

- Idempotent `bootstrapUserSingletons` / `bootstrapSpaceSingletons`:
  both functions now existence-check sync_changes before INSERT and
  return boolean (true=inserted, false=skipped).
- New endpoint `POST /api/v1/me/bootstrap-singletons` — JWT-gated under
  the existing `/api/v1/me/*` prefix. Provisions the caller's
  userContext and the kontextDoc for every Space they're a member of.
  Returns `{ ok, bootstrapped: { userContext, spaces: { id: bool } } }`.
- Webapp `(app)/+layout.svelte` calls the endpoint once per
  authenticated boot, after `restoreClientIdFromDexie()` and before
  `createUnifiedSync.startAll()`. Best-effort; failures swallow into a
  console warning and the in-store fallback still covers the rare
  race window.

Plan: docs/plans/sync-field-meta-overhaul.md (F4-robust row).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 01:38:14 +02:00
Till JS
53fecbf4a7 chore(dexie): v55 — sweep orphan updatedAt field from existing rows (F3 cleanup)
After F3 of the sync field-meta overhaul, every read of "last modified"
goes through `deriveUpdatedAt(record)` over `__fieldMeta`. The legacy
`updatedAt` field on existing IndexedDB rows was deliberately left in
place by v53 (its comment explicitly defers the row-rewrite to a later
cleanup) so the cut-over could proceed without a full DB rewrite.

This v55 upgrade walks every sync-relevant table (`Object.keys(TABLE_TO_APP)`)
and `delete row.updatedAt`. Idempotent (rows without the field are a
no-op), best-effort (try/catch per table guards against a registry
entry that doesn't yet have a Dexie store row).

Local-only tables (_pendingChanges, _activity, _clientIdentity,
_aiDebugLog) never carried `updatedAt`, so they stay out of the sweep.

Plan: docs/plans/sync-field-meta-overhaul.md (F3-fu row in Shipping Log).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 01:26:21 +02:00
Till JS
3df7391905 feat(auth): bootstrap per-Space kontextDoc on Space-creation (F4 follow-up)
Symmetrically extends the F4 server-side singleton bootstrap to the
per-Space `kontextDoc`. Every Space-creation — Personal at signup and
brand/club/family/team/practice via the org plugin — now writes an empty
kontextDoc row straight into mana_sync.sync_changes with origin='system',
client_id='system:bootstrap'. Fresh clients pull the row instead of
racing on a local insert that the next pull would clobber.

- New `bootstrapSpaceSingletons(spaceId, ownerUserId, syncSql)` in
  services/mana-auth/src/services/bootstrap-singletons.ts; shared
  `buildFieldMeta` helper extracted.
- `createBetterAuth(databaseUrl, syncDatabaseUrl, webauthn)` now takes
  the sync-DB URL and lazy-creates a module-scoped postgres pool for
  the bootstrap inserts.
- Hook into `databaseHooks.user.create.after` (only on `created: true`
  from createPersonalSpaceFor) and `organizationHooks.afterCreateOrganization`.
- Webapp `kontextStore.ensureDoc()` made private as `getOrCreateLocalDoc()` —
  same fallback role as userContextStore's after F5. Public API is now just
  setContent + appendContent.

Plan: docs/plans/sync-field-meta-overhaul.md (F4-fu row in Shipping Log).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 01:21:31 +02:00
Till JS
fd11481d94 docs(plans): mark sync-field-meta-overhaul F1-F7 SHIPPED
All seven phases of docs/plans/sync-field-meta-overhaul.md landed.
Final shipping log:

  F1 7766ea502  __fieldMeta replaces __fieldTimestamps trio
  F2 ad5e04a55  origin-gated conflict detection
  F3 6bb9d77be  drop updatedAt as a synced data field
  F4 c07db300b  server-side singleton bootstrap (mana-auth)
  F5 d78f57c04  drop public userContextStore.ensureDoc()
  F6 a031493fe  stable client_id in Dexie
  F7 2a8e8ff98  drop repair-silent-twin + legacy-avatar migrations

Structural outcome: the four conflict-toast root-causes diagnosed
on 2026-04-26 (updatedAt as synced field, history-replay false-
positives, ensureDoc race, localStorage-bound client_id) are all
closed. The conflict surface fires only when a real user edit
genuinely loses to a server overwrite — anything else is silent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:54:09 +02:00
Till JS
6bb9d77be9 feat(sync): F3 — drop updatedAt as a synced data field
Removes `updatedAt` from the wire protocol and from every Local-prefixed
record type. Replaced by two orthogonal mechanisms — deriveUpdatedAt()
for read-side public-facing values, _updatedAtIndex shadow for indexed
sorts.

Local-side:
- New `_updatedAtIndex` shadow column. Stamped by the Dexie creating /
  updating hook on every write. Stripped from the pending-change payload
  so it never travels to mana-sync. Indexed in Dexie v53 on the 22 tables
  that previously indexed `updatedAt`.
- `deriveUpdatedAt(record)` in sync.ts returns max(__fieldMeta[*].at) so
  the public-facing Task / Note / etc. shape keeps an `updatedAt: string`
  property without holding it as data.
- Type-converters across ~60 module/queries.ts and types.ts files now
  call `deriveUpdatedAt(local)` instead of reading `local.updatedAt`.

Module-store sweep:
- Regex codemod removed `updatedAt: new Date().toISOString()` /
  `: now` / `: now()` / `: nowIso()` stamping from 121 store files
  (~382 call sites total). Single-property update calls
  (`{ updatedAt: now }`) collapsed to `{}`; touch-only patterns
  (writing/drafts, writing/generations) kept the call as a no-op
  because the hook now stamps `_updatedAtIndex` automatically on
  any Dexie modification.
- Local* interfaces stripped of `updatedAt: string` (43 types.ts files).
  Public-facing types (Task, Note, Mission, Agent, …) keep
  `updatedAt: string` as a computed read-side property.
- Companion's chat conversation now sorts on a real
  `lastMessageAt` data field instead of touching `updatedAt`.
- Session-only stores (times/session-alarms, session-countdown-timers)
  stamp `updatedAt: now` directly because they're not in Dexie and
  have no field-meta layer to derive from.

Sync engine:
- applyServerChanges sets `_updatedAtIndex` itself when applying
  server changes (max of server-field times for updates, recordTime
  for inserts) so server-replays land orderable.
- Dropped the legacy `localUpdatedAt` fallback — every record now has
  `__fieldMeta`, the per-field at is the canonical source.
- Soft-delete tombstone path stops stamping `updatedAt: serverTime`,
  uses `_updatedAtIndex` instead.

Server-side:
- mana-ai iteration-writer no longer emits `updatedAt` in
  sync_changes.data; receivers derive it from the field-meta map.
- mana-sync types: no change (the wire format already uses
  `field_meta` / `at` from F1).

Out of scope: backend Drizzle schemas (mana-credits, mana-events, …)
keep their `updated_at` columns. Those are pure server-internal — not
part of the sync_changes / __fieldMeta mechanism F3 cleans up.

Tests + checks:
- 0 svelte-check errors over 7652 files.
- 29/29 sync.test.ts (vitest).
- 61 mana-ai bun tests.
- mana-sync go test ./... cached green.

Plan: docs/plans/sync-field-meta-overhaul.md F3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:12:22 +02:00
Till JS
ad5e04a554 feat(sync): F2 — origin-gated conflict-detection
Closes the false-positive conflict-toast loop on history-replay. Conflict
notifications now fire only when the local field meta records origin='user'
AND the pull is not an initial hydration round.

Origin source-of-truth:
- shared-ai/field-meta.ts → originFromActor(actor) maps actor.kind onto
  the FieldOrigin enum: user→'user', ai→'agent', system+SYSTEM_MIGRATION
  →'migration', any other system source→'system'.
- Dexie creating/updating hooks call it once per write so every persisted
  field carries the right pipeline tag.
- repair-silent-twin + legacy-avatar wrap their writes in
  runAsAsync(makeSystemActor(SYSTEM_MIGRATION, ...)) so the hook stamps
  origin='migration'. Future replays of those rows from another device
  will not surface as conflicts.

applyServerChanges options:
- New ApplyServerChangesOptions { isInitialHydration?: boolean }.
- Push-response and pull-paged-loop callers compute it from the cursor
  state (`!oldestCursor` / `!cursor`). Pagination resets the flag after
  the first page.
- Conflict-trigger gates on `!options.isInitialHydration && localMeta[k]
  ?.origin === 'user'` in addition to the prior tests.

Tests (sync.test.ts):
- New: replay-burst (10 sequential server updates → 0 conflicts)
- New: agent-origin local write + server overwrite → 0 conflicts
- New: isInitialHydration suppresses everything → 0 conflicts
- New: real user edit + server overwrite → 1 conflict
- All 25 prior tests still pass.

29/29 vitest sync.test.ts cases green; svelte-check 0 errors over 7647
files.

Plan: docs/plans/sync-field-meta-overhaul.md F2 done-criteria met.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:38:56 +02:00
Till JS
7766ea5021 docs(plans): mark llm-fallback-aliases SHIPPED, add M-by-M commit table
All 5 milestones landed today in one continuous session: registry,
health cache, fallback router, observability, and consumer migration.
115 service-side tests, validator covers 2538 files.
2026-04-26 21:27:57 +02:00