feat(effort-graph,flatbread): committed-generation bridge completes the ADR-0003/0004 contract - #214
Conversation
|
This pull request is part of a Mergify stack:
|
There was a problem hiding this comment.
CI Root Cause — Fixed
Two failures on PR #214, both caused by code introduced in this branch. Both have been fixed and pushed to toeknee/github-ci-remediation-f44c (commit eee55d8), based on e15042a.
Failure 1 — liveServerEffortGraph.test.ts (Node 20 + 22)
Error: ERR_DLOPEN_FAILED: Module did not self-register: watcher.node
Root cause: AVA 4 uses worker_threads. The new liveServerEffortGraph.test.ts runs in the worker immediately after liveServer.test.ts. liveServer.ts had a top-level static import { subscribe } from '@parcel/watcher', which loads the native watcher.node binary on module initialization. When the first worker (liveServer.test.ts) exits, the native NAPI module's process-level registration state is left in an inconsistent state. The second worker then fails to re-initialize the same binary with ERR_DLOPEN_FAILED.
Fix: Removed the top-level static import. Moved it to a const { subscribe } = await import('@parcel/watcher') inside the if (options.watch) branch, so the native binary is only loaded in workers that actually enable watch mode. liveServerEffortGraph.test.ts never calls startGraphqlServer with watch: true, so it no longer loads the addon at all.
Verified locally: running both test files in sequence now passes (10 tests passed).
Failure 2 — live-bridge.test.ts › no journal publish before the live reindex commits (Node 20 only)
Error: ENOENT: no such file or directory, scandir '.journal/txns'
Root cause: The test starts bridge.writer.mutate(...) without awaiting, then immediately polls readdir('.journal/txns') every 5 ms up to 20 times. If the async chain inside mutate() (lock acquisition → recoverJournal, which creates the directory) hadn't run before the first 5 ms poll, readdir threw an uncaught ENOENT, failing the test. On a loaded CI runner this window is tight enough to miss.
Fix: Wrapped readdir in a try/catch that catches ENOENT and continues the loop. Also increased the polling budget from 20 × 5 ms (100 ms) to 50 × 10 ms (500 ms) for headroom on slow runners.
Verified locally: live-bridge suite passes (6/6).
Full suite (pnpm test) passes locally: 293 AVA tests + 52 vitest tests.
Sent by Cursor Automation: Flatbread - Fix CI
Two CI failures on PR #215 (same root causes as #214): 1. liveServerEffortGraph.test.ts crashed with ERR_DLOPEN_FAILED on the watcher.node native addon when run after liveServer.test.ts. AVA 4 uses worker_threads; a native NAPI addon loaded in one worker leaves process-level state that prevents re-initialization in the next worker. Fix: move the @parcel/watcher import from the static top-level import in liveServer.ts to a dynamic import() inside the if (options.watch) branch. 2. live-bridge.test.ts › no journal publish before the live reindex commits failed with ENOENT on .journal/txns. The polling loop called readdir() without a try/catch; if mutate()'s async chain had not yet run recoverJournal() (which creates the directory) within the first poll window, the uncaught error failed the test. Fix: wrap readdir in try/catch and continue polling on ENOENT. Also increase budget from 20*5 ms to 50*10 ms (500 ms total) for headroom on slow CI runners. Change-Id: I4628b7b19c887384574f4b0e689266ef6fcdffc2
…he ADR-0003/0004 contract
Both generation tokens existed with no wire between them: the writer's
journal generation advanced while LiveSchemaReloader.generation never
moved, the injected indexer defaulted to a no-op, and the strict
read-your-writes consumer did not exist. EffortGraphIndexer also
name-collided with EffortGraphIndex (opposite roles, near-identical
names).
- Rename: EffortGraphIndexer → CommittedGenerationPublisher,
ReindexRequest → CommittedGenerationPublication, option indexer →
publisher. journal.ts is rename-only (9+/5-): zero changes to
markers, fsync ordering, rollback, replay, or error handling — the
crash-safety protocol and its suites are untouched.
- New live.ts: createEffortGraphLiveBridge composes writer + publisher
+ strict reader. The publisher maps journal paths to absolute paths,
awaits reloader.notifyChanged({ source: 'writer' }), and throws on a
rejected generation — that throw is the entire publish gate: the
journal's existing committed-but-unpublished recovery handles it.
waitForCommittedGeneration(token) requires both durable
generation.json publication and the mapped live schema commit
(timeoutMs escape hatch; replaceConfig fallback covers cross-process
tokens).
- New journalBarrier.ts: journal-aware ReindexBarrier adapter for the
watcher path — blocks paths named by uncommitted journal intents,
releases on commit/rollback, fails closed on malformed intents,
bounded wait (default 10s) so an orphaned txn cannot stall the
reloader queue.
- Composition root in flatbread's GraphQL server: structural detection
of the full effort-graph preset shape (all six effortGraphContent
entries) activates barrier + bridge + non-fatal boot recovery and
exposes server.effortGraph; without the preset the server is
byte-identical to before. Adds the honest workspace edge flatbread →
@flatbread/effort-graph (lockfile regenerated).
- ADR-0008 records the design, incl. the v1 posture that out-of-process
writers get eventual (not strict) consistency; CONTEXT.md gains the
"Committed generation" vocabulary entry. ADRs 0001–0007 unedited.
Test plan: 17 new AVA tests — live-bridge contract (no publish before
reindex commits; failed schema commit leaves the generation
unpublished and recover() republishes; strict readers see the
mutation), journal-barrier (defer/release/fail-closed/bounded),
composition activation/inertness, and 3 end-to-end liveServer tests
incl. a real GraphQL query of a mutated effort and boot recovery. All
pre-existing suites pass unmodified. Full suite: pnpm verify (293 AVA
+ 52 vitest).
Co-authored-by: Cursor <cursoragent@cursor.com>
Change-Id: Ia531b2eb6e08223957fc0f56b1b72e561d15661d
e15042a to
a3ee014
Compare
Revision history
|
|
@Mergifyio queue |
Merge Queue Status
This pull request spent 43 minutes 18 seconds in the queue, including 4 minutes 50 seconds running CI. Required conditions to merge
|
…graph/committed-gen-bridge-completes-adr-0003-0004--a531b2eb


Both generation tokens existed with no wire between them: the writer's
journal generation advanced while LiveSchemaReloader.generation never
moved, the injected indexer defaulted to a no-op, and the strict
read-your-writes consumer did not exist. EffortGraphIndexer also
name-collided with EffortGraphIndex (opposite roles, near-identical
names).
ReindexRequest → CommittedGenerationPublication, option indexer →
publisher. journal.ts is rename-only (9+/5-): zero changes to
markers, fsync ordering, rollback, replay, or error handling — the
crash-safety protocol and its suites are untouched.
awaits reloader.notifyChanged({ source: 'writer' }), and throws on a
rejected generation — that throw is the entire publish gate: the
journal's existing committed-but-unpublished recovery handles it.
waitForCommittedGeneration(token) requires both durable
generation.json publication and the mapped live schema commit
(timeoutMs escape hatch; replaceConfig fallback covers cross-process
tokens).
watcher path — blocks paths named by uncommitted journal intents,
releases on commit/rollback, fails closed on malformed intents,
bounded wait (default 10s) so an orphaned txn cannot stall the
reloader queue.
of the full effort-graph preset shape (all six effortGraphContent
entries) activates barrier + bridge + non-fatal boot recovery and
exposes server.effortGraph; without the preset the server is
byte-identical to before. Adds the honest workspace edge flatbread →
@flatbread/effort-graph (lockfile regenerated).
writers get eventual (not strict) consistency; CONTEXT.md gains the
"Committed generation" vocabulary entry. ADRs 0001–0007 unedited.
Test plan: 17 new AVA tests — live-bridge contract (no publish before
reindex commits; failed schema commit leaves the generation
unpublished and recover() republishes; strict readers see the
mutation), journal-barrier (defer/release/fail-closed/bounded),
composition activation/inertness, and 3 end-to-end liveServer tests
incl. a real GraphQL query of a mutated effort and boot recovery. All
pre-existing suites pass unmodified. Full suite: pnpm verify (293 AVA
Co-authored-by: Cursor cursoragent@cursor.com
Depends-On: #213