Skip to content

fix(world): normalize vector clocks and enforce phase-105 conflict check (#275) - #360

Merged
Darkvader-ship-it merged 1 commit into
PHASE-STELLAR:mainfrom
dannyy2000:fix/272-273-274-275-world-vector-clock
Sep 27, 2026
Merged

Darkvader-ship-it merged 1 commit into
PHASE-STELLAR:mainfrom
dannyy2000:fix/272-273-274-275-world-vector-clock

Conversation

@dannyy2000

@dannyy2000 dannyy2000 commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Closes #275
Closes #272,
Closes #273,
Closes #274.
These share the same evidence block but point at different files (app/api/x402/verify/route.ts, lib/wash-trading.ts, lib/faucet-deny-list.ts). This PR does not change those files, so they should stay open.

Problem (#275)

  • POST /api/world imported checkWorldConflict but never called it. expected_version was accepted and then ignored, so concurrent co-author saves still overwrote each other even with phase-105 enabled.
  • Worlds had no vector clock. A clock can be serialized as a plain object, as a Map, or as a [node, counter] entries array (JSON.stringify([...map])). Compared as-is, these shapes never match, so two equal clocks look divergent.

Changes

  • lib/world-conflict.ts
    • normalizeVectorClock() turns a Map, a plain object or an entries array into one canonical Record<string, number>. It drops zero counters, keeps the highest value when a node appears twice, and returns null for malformed input.
    • compareVectorClocks() returns equal, before, after or concurrent.
    • incrementVectorClock() works on stored clocks in any of those shapes.
    • checkWorldConflict() accepts an optional expectedVectorClock and returns order and serverVectorClock. It still checks version when the clocks agree, so worlds saved before this change keep working.
  • lib/narrative-world-store.ts: saveWorldForCollection now bumps vector_clock[creator_wallet ?? "anonymous"] and returns the saved record.
  • app/api/world/route.ts
    • Validates expected_version (non-negative integer) and expected_vector_clock, returning 400 if either is invalid.
    • Runs the conflict check before saving and returns 409 WORLD_VERSION_CONFLICT with order, server_version, client_version, server_vector_clock and current.
    • A successful save returns the new version and vector_clock. GET exposes vector_clock for each world.
  • Docs: added phase-105 rows to PROJECT_ARCHITECTURE.md and docs/TECHNICAL.md.

Behavior

  • Flag off: unchanged. Saves overwrite unconditionally.
  • Callers that send neither expected_version nor expected_vector_clock: unchanged, no check.

Tests

npx tsx --test lib/__tests__/world-conflict.test.ts: 15/15 pass.

  • New tests cover: normalization of Map, object and array inputs; rejection of malformed clocks; ordering; stale (before) and diverged (concurrent) conflicts; matching clocks with different shapes; and the version fallback for legacy worlds.
  • lib/__tests__/narrative-search.test.ts still passes.

Known limitation

The check and the write are still a read-then-write on the JSON store, so two requests that interleave exactly between those steps can both succeed. Closing that gap needs an atomic compare-and-swap in the store, which is outside the scope of this PR.

…eck on POST /api/world (PHASE-STELLAR#275)

checkWorldConflict was imported by POST /api/world but never called, so
concurrent world edits still overwrote each other. Worlds now carry a
per-author vector_clock; clocks arriving as a Map, plain object, or
[node, counter] entries array are normalized before comparison so equal
clocks no longer register as divergent. Stale or concurrent saves return
409 WORLD_VERSION_CONFLICT with the server version, clock, and order.
@drips-wave

drips-wave Bot commented Sep 27, 2026

Copy link
Copy Markdown

@dannyy2000 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Darkvader-ship-it
Darkvader-ship-it merged commit 9f5ce7f into PHASE-STELLAR:main Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment