Skip to content

feat(effort-graph,flatbread): committed-generation bridge completes the ADR-0003/0004 contract - #214

Merged
tonyketcham merged 2 commits into
mainfrom
stack/tonyketcham/feat/unified-watch-effort-graph/committed-gen-bridge-completes-adr-0003-0004--a531b2eb
Jul 18, 2026
Merged

tonyketcham merged 2 commits into
mainfrom
stack/tonyketcham/feat/unified-watch-effort-graph/committed-gen-bridge-completes-adr-0003-0004--a531b2eb

Conversation

@tonyketcham

Copy link
Copy Markdown
Collaborator

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

Depends-On: #213

@tonyketcham

tonyketcham commented Jul 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

This pull request is part of a Mergify stack:

# Pull Request Link
1 docs(effort-graph): add Effort Graph glossary and ADRs 0001-0007 #205
2 feat(core,cli): unified watch with incremental reindex and live schema swap #206
3 feat(effort-graph): journaled semantic writer for the Effort Graph #207
4 refactor(config): loadConfig returns an initialized LoadedFlatbreadConfig #208
5 refactor(core): single Record-production module owns file→Record and path classification #209
6 refactor(effort-graph): per-mutate snapshot + Decision lifecycle module #210
7 refactor(core): collection query executor owns filter/sort/paging with copy-on-output #211
8 refactor(core,cli): unified watch coordinator behind one seam, two adapters #212
9 refactor(core): per-build schema composer; delete the config-keyed schema cache #213
10 feat(effort-graph,flatbread): committed-generation bridge completes the ADR-0003/0004 contract #214 👈
11 chore(core): drop unused graphql-compose-json and lru-cache dependencies #215
12 test: raise AVA concurrency now that the global-composer pin is obsolete #216
13 docs(agents): define orchestrator-executor operating model #217

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Open in Web View Automation 

Sent by Cursor Automation: Flatbread - Fix CI

cursor Bot pushed a commit that referenced this pull request Jul 18, 2026
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
@tonyketcham
tonyketcham force-pushed the stack/tonyketcham/feat/unified-watch-effort-graph/committed-gen-bridge-completes-adr-0003-0004--a531b2eb branch from e15042a to a3ee014 Compare July 18, 2026 07:36
@tonyketcham

Copy link
Copy Markdown
Collaborator Author

Revision history

# Type Changes Reason Date
1 initial e15042a 2026-07-18 07:37 UTC
2 content e15042a → a3ee014 (raw) Fix CI: defer the native watcher import outside watch mode and make the journal visibility assertion resilient to asynchronous directory creation. 2026-07-18 07:37 UTC

@tonyketcham

Copy link
Copy Markdown
Collaborator Author

@Mergifyio queue

@mergify

mergify Bot commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • ✅ Entered queue — 2026-07-18 08:36 UTC · Rule: owner-bypass · triggered by @tonyketcham with the @mergifyio queue command
  • ✅ Checks passed · in-place
  • ✅ Merged — 2026-07-18 09:19 UTC · at d30444fb24ca207900732b742143151b5603dd71

This pull request spent 43 minutes 18 seconds in the queue, including 4 minutes 50 seconds running CI.

Required conditions to merge

@mergify mergify Bot added the queued label Jul 18, 2026
Base automatically changed from stack/tonyketcham/feat/unified-watch-effort-graph/per-build-schema-composer-delete-config-keyed--676c4789 to main July 18, 2026 09:15
…graph/committed-gen-bridge-completes-adr-0003-0004--a531b2eb
@tonyketcham
tonyketcham merged commit 1489cef into main Jul 18, 2026
19 checks passed
@tonyketcham
tonyketcham deleted the stack/tonyketcham/feat/unified-watch-effort-graph/committed-gen-bridge-completes-adr-0003-0004--a531b2eb branch July 18, 2026 09:20
@mergify mergify Bot removed the queued label Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant