Skip to content

feat(explorer): ship Effort Graph visualizer with flatbread start - #233

Merged
tonyketcham merged 10 commits into
mainfrom
toeknee/explorer-package-stack-merge-c899
Jul 29, 2026
Merged

feat(explorer): ship Effort Graph visualizer with flatbread start#233
tonyketcham merged 10 commits into
mainfrom
toeknee/explorer-package-stack-merge-c899

Conversation

@tonyketcham

@tonyketcham tonyketcham commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Merge vehicle for the explorer package stack (supersedes closed #230#232).

Ships @flatbread/explorer, a single-page web app that draws the relationships between content records. When a config uses effortGraphContent(), flatbread start serves it at /, with the Apollo sandbox at /graphql and the event stream at /events. Replaces the deleted examples/effort-viz Next.js app.

What lands

  1. Ship @flatbread/explorer with flatbread start mounting
  2. Restore monorepo path aliases for explorer typecheck
  3. Harden the mount gate, tests, and watch lifecycle
  4. Sync the skill projection; tolerate watcher teardown EINVAL
  5. Five rounds of review fixes (below)

Review fixes

Real bugs found and fixed

  • GET /events/ returned the explorer's HTML instead of the event stream. The SPA fallback excluded /graphql by exact match and prefix but /events by exact match only, and it runs before the SSE handler.
  • -H, --https advertised a server that does not exist. Nothing read FLATBREAD_HTTPS, so the flag never changed how Flatbread listens, yet the banner printed https://localhost:<port+1> and --open opened an HTTPS URL. Dead plumbing and false banner lines removed; the flag still parses so existing scripts run.
  • flatbread start hung forever when the forked GraphQL child died before reporting ready — a broken config or a taken port left the parent alive with nothing to do.
  • evaluate() read index.html right after checking it exists, so a file vanishing in between failed a watch-mode config reload.
  • ?endpoint=/ resolved to the explorer's own HTML instead of /graphql, because the root-path normalization ran only on the absolute-URL branch.
  • explorerAssetsPresent() used existsSync, which is true for a directory. A directory named index.html made --open open a broken explorer.
  • A real CI failure: watch-mode startup lost an inotify_add_watch race on Linux. The ignore globs matched paths inside the fixture directories, not the directories themselves, so parcel still registered them on the way down. Earlier green runs were luck. Fixed with directory-pair ignores plus wider retries.

Contract and test work

  • Watch mode no longer bakes AVA fixture names into production. watchIgnore.ts holds the production list; tests pass their fixture globs through a new option, and a deep-equal pin stops them being reintroduced.
  • --open waits for flatbread-gql-ready instead of racing startup, and resolves the path at ready time so the assets check happens as late as possible.
  • The server-only keep-alive branch moved into ready.ts behind injected spawn and exit, unit-tested rather than forked.
  • New coverage for the watcher error and push soft-fail paths, the reload-time missing-assets latch, the bootstrap eventsPath contract, and static assets with and without file extensions.
  • openPath.test.ts tests are all serial now; they shared module-global override state and could race.

Docs

Rewritten to the repo's plain-English rules: dropped the ## Local DX and ## Package surface headings, spelled out SPA and HMR on first use, and replaced abstract "assets gate" wording with what Flatbread does. Corrected five false HTTPS claims across AGENTS.md, CONTRIBUTING.md, examples/nextjs/README.md, packages/flatbread/README.md, docs/local-dev-loop.md, and flatbread-flow-agentic-workflows.md — including a cited port 5058 HTTPS listener that never existed. CLI examples use npx for package consumers and pnpm exec for monorepo contributors.

Three review suggestions were declined with reasons in the PR comments: hiding setExplorerStaticDirOverride (a separate package's tests need the export), extracting an --open sequencer (it would test a two-line promise chain whose guarantee is already covered), and requiring a hashed assets/*.js in the asset gate (it would couple the gate to Vite's output naming).

Test plan

Why this is still blocked

.mergify.yml defines an owner-bypass queue rule that this PR fully satisfies — author = tonyketcham and all 19 required checks green, with branch_protection_injection_mode: none waiving the review requirement. But the config has no pull_request_rules entry that queues anything, so the queue only ever starts from the @mergifyio queue command, and that command is restricted to sender-permission >= write. Agent accounts are refused. One comment from a maintainer merges this.

Open in Web Open in Cursor 

tonyketcham and others added 4 commits July 27, 2026 19:51
Distribute the content-relation explorer as `@flatbread/explorer` so
consumers no longer need a separate Next app. When config uses
`effortGraphContent()`, `flatbread start` serves the SPA at `/` and
`--open` launches it; Apollo stays at `/graphql`.

## Summary
- Add `@flatbread/explorer` (Vite SPA + Node helpers) with a generic
  graph core and an Effort Graph preset migrated from `examples/effort-viz`
- Hard-depend from `flatbread`; auto-mount on known presets with bootstrap
  injection for same-origin GraphQL/SSE
- Support server-only `flatbread start` (no corunner) and static deploys
  via `?endpoint=`
- Point `pnpm play:efforts` at `flatbread start --watch --open`; retire
  `examples/effort-viz`; update CONTRIBUTING and Effort Graph setup skill

## Test plan
- [x] `pnpm --filter @flatbread/explorer test` (79 tests)
- [x] `pnpm --filter @flatbread/explorer typecheck`
- [x] AVA: explorerMount, openPath, liveServerEffortGraph, liveServer
- [x] Smoke: `flatbread start` serves explorer HTML at `/` and GraphQL at
  `/graphql` with bootstrap injection
- [ ] CI green on the stacked PR

Co-authored-by: Cursor <cursoragent@cursor.com>
Change-Id: I5b888060d859bb06b64528115ef89b098a3ae62c
Explorer tsconfig overrode root `paths` with only `@/*`, so
`@flatbread/effort-graph` was invisible during CI typecheck
(before build). Re-declare the monorepo path map, raise `lib` to
ESNext, and set `skipLibCheck` so transitive source resolves cleanly.

Change-Id: I8f7f21203bccae30ddabf597929f1610fed5b2f6
Keep --open and mount on the shared assets gate, isolate AVA fixtures
with a static-dir override (no package-global index.html renames), use
vite base `/` for nested SPA routes, toggle explorer on config reload
under --watch, and document the built-assets requirement in the skill.

Co-authored-by: Cursor <cursoragent@cursor.com>

Change-Id: I1f5f06aa4e0caba4e517f4608c39e907cb567f45
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep .agents/skills/effort-graph/setup.md identical to the package
skill source so skills:check passes. Tolerate @parcel/watcher EINVAL
on Node 22 when unsubscribe runs after the native handle is gone.

Change-Id: Ida461356c8d75fde5b3a7203e587433661d45107
@mergify

mergify Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

⚠️ The sha of the head commit of this PR conflicts with #232. Mergify cannot evaluate rules on this PR. Once #232 is merged or closed, Mergify will resume processing this PR. ⚠️

Concurrent AVA fixtures under cwd (.tmp-effort-*, .journal) race
inotify_add_watch while liveServer watches the repo root. Ignore those
paths, keep watcher callback errors from rejecting the server, and retry
subscribe when inotify_add_watch reports a missing path.

Change-Id: I8144a16864a00dd5a263a68c8ab08ea7d05b9105
@mergify

mergify Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

⚠️ The sha of the head commit of this PR conflicts with #232. Mergify cannot evaluate rules on this PR. Once #232 is merged or closed, Mergify will resume processing this PR. ⚠️

@cursor
cursor Bot marked this pull request as ready for review July 28, 2026 10:26
@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

@Mergifyio queue

@mergify

mergify Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

queue

☑️ Command disallowed due to command restrictions in the Mergify configuration.

Details
  • sender-permission >= write

@cursor
cursor Bot enabled auto-merge (squash) July 28, 2026 10:27
@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

Stack PRs #230#232 are closed as superseded by this merge vehicle (same tip commits). CI is green.

I cannot @mergifyio queue from this agent (sender-permission >= write required). Please comment @mergifyio queue on this PR to merge into main via the owner-bypass queue.

@mergify

mergify Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

@Mergifyio refresh

@mergify

mergify Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

refresh

☑️ Command disallowed due to command restrictions in the Mergify configuration.

Details
  • any of:
    • sender = {{author}}
    • sender-permission >= write

@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.

Stale comment

Review verdict

REQUEST_CHANGES — consensus HIGH on production --watch ignore globs (liveServer.ts:270-282) plus two further independent HIGHs (watcher error/push soft-paths; mountExplorer.update missing-assets). Prior #230 blockers are closed at 9fde89a / 58a9a82 / 75fc81a.

What still blocks

  1. Watch ignore contract**/.tmp-effort-*/** and **/.tmp-explorer-*/** bake AVA fixture prefixes into the public watch path. Real content under those names never hot-reloads, and hermeticity is coupled to test naming with no pos/neg/edge proofs.
  2. Watcher soft-fail untested — error callback and coordinator.push throws are swallowed with no tests; a regression that re-rejects the server or drops all pushes would stay green.
  3. update missing-assets untested — initial mount gate is covered; reload-time replaceConfig / update when assets vanish is not.

Prior #230 status

Closed: openPath↔mount assets gate, mount /graphql+/events/missing-assets/extensionless, sticky watch + live server.explorer, explorer preplay:efforts, vite base.

Coverage plan (highest priority)

  1. Watch ignore: .tmp-live-server-test-* still commits; .tmp-effort-* / .tmp-explorer-* do not; .journal ignored.
  2. Watcher error / push throw keep server alive; normal event still reindexes.
  3. After active mount, update with empty static override warns once, deactivates, then restore re-enables.
  4. Assert bootstrap eventsPath === '/events'; treat /events/ like GraphQL prefix exclusion.
  5. Server-only runner keep-alive fork test.

Perspectives: correctness-and-contracts, test-coverage-robustness, cli-and-runtime, explorer-package, dx-and-examples → judge. Models: grok-4.5 high / composer-2.5.

Open in Web View Automation 

Sent by Cursor Automation: Flatbread PR Review

Comment thread packages/flatbread/src/graphql/liveServer.ts Outdated
Comment thread packages/flatbread/src/graphql/liveServer.ts
Comment thread packages/flatbread/src/graphql/explorerMount.ts
Comment thread packages/flatbread/src/graphql/explorerMount.ts
Comment thread packages/flatbread/src/graphql/explorerMount.ts
Comment thread packages/flatbread/src/cli/index.ts Outdated
Comment thread packages/flatbread/src/cli/runner.ts Outdated
Comment thread packages/flatbread/src/cli/index.ts Outdated
Resolve the eight findings on #233. Two were real bugs; the rest closed
test gaps or removed a lie in the CLI.

1. `GET /events/` returned the explorer's HTML instead of the event
   stream. The SPA fallback in `explorerMount.ts` excluded `/graphql` by
   both exact match and prefix but `/events` by exact match only, and it
   runs before the SSE handler. Exclude the `/events/` prefix too.
2. `-H, --https` advertised a server that does not exist. Nothing read
   `FLATBREAD_HTTPS`, so the flag never changed how Flatbread listens,
   yet the banner printed `https://localhost:<port+1>` and `--open`
   opened an HTTPS URL. Drop the dead plumbing and the false banner
   lines, keep the flag parsing so existing scripts still run, and say
   in its help text that Flatbread serves HTTP only.
3. Watch mode no longer bakes AVA fixture names into production.
   `watchIgnore.ts` holds the production ignore list (`node_modules`,
   `.git`, `dist`, `.journal`); the one watch-enabled test passes its
   fixture globs through a new `watchIgnore` option. Content in a
   directory named `.tmp-effort-*` now hot-reloads like anything else.
4. `--open` waited for nothing and could hit a refused connection on a
   cold start. The runner now calls `onReady` on `flatbread-gql-ready`
   and the browser launch hangs off that. The fork still happens before
   the config load, so config work stays off the path to ready.
5. The server-only keep-alive branch moved into `ready.ts` behind
   injected spawn and exit, so ten unit tests cover it without forking.

New tests also cover the watcher error and push soft-fail paths, the
reload-time missing-assets latch in `mountExplorer.update`, the
bootstrap `eventsPath` contract, and static assets with and without
file extensions.

Docs: rewrite the explorer README, the CONTRIBUTING bullet, and the
effort-graph setup notes to the repo's plain-English rules. Drop the
`## Local DX` and `## Package surface` headings, spell out SPA and HMR
on first use, and replace the abstract "assets gate" wording with what
Flatbread actually does. Correct the stale HTTPS gotcha in AGENTS.md and
drop `--https` from the Next.js example script.

Verified with `pnpm verify`.

Co-authored-by: Cursor <cursoragent@cursor.com>
Change-Id: I425dff6ebafa4fd82dd6ea5937158447bf9d4a61
@tonyketcham

Copy link
Copy Markdown
Collaborator Author

All eight review findings are resolved in 51a61f1. I checked each against the source before acting; all eight were valid, and two were real bugs rather than test gaps.

Real bugs

  1. GET /events/ returned explorer HTML, not the event stream. The SPA fallback excluded /graphql by exact match and prefix, but /events by exact match only — and it runs before the SSE handler. Added the /events/ prefix exclusion. The test failed before the fix and passes after.
  2. -H, --https advertised a server that does not exist. runner.ts set FLATBREAD_HTTPS, but nothing read it — server.ts reads only FLATBREAD_PORT and FLATBREAD_WATCH. So the flag never changed how Flatbread listens, yet the banner printed https://localhost:<port+1> and --open opened an HTTPS URL against a plain-HTTP server. Removed the dead plumbing and the false banner lines, kept the flag parsing so existing scripts still run, and corrected its help text. Also dropped --https from the Next.js example script and fixed the matching stale claim in AGENTS.md.

Watch ignore contract

Production no longer knows any test-fixture name. watchIgnore.ts holds the production list (node_modules, .git, dist, .journal); a new watchIgnore option carries the fixture globs, and only the one watch-enabled test passes them. Content in a directory named .tmp-effort-* now hot-reloads like anything else. Covered positive (.tmp-live-server-test-* stays visible), negative (fixture prefixes drop when supplied), and edge (defaults match; duplicate extras collapse).

--open startup race

The runner now fires an onReady callback on flatbread-gql-ready and the browser launch hangs off that. The fork still happens before the config load, so config work stays off the critical path to ready and to the corunner — --open waits on a promise for the resolved path instead.

Two places I took a different approach than suggested

  • Fixtures outside the watched tree is not possible: watch mode requires content paths relative to process.cwd(), so fixtures must live under it. Parameterizing the ignore list achieves the same decoupling.
  • A fork/spawn test for the keep-alive branch would be slow and flaky in CI. Instead the ready-message decision moved into ready.ts behind injected spawn and exit, covered by ten unit tests: server-only keep-alive, exit codes for 0 and null, corunner spawn and package-manager default, either child closing, and a duplicate ready message not spawning twice.

Also covered: watcher error and push soft-fail paths (via a watcherSubscribe test seam), the reload-time missing-assets latch in mountExplorer.update including the warn-once behaviour and reactivation, the bootstrap eventsPath contract plus inactive fallthrough, and static assets with and without file extensions never carrying __FLATBREAD_EXPLORER__.

Docs were rewritten to this repo's plain-English rules: ## Local DX and ## Package surface are gone, SPA and HMR are spelled out on first use, and the abstract "assets gate" wording now states what Flatbread does. Both setup.md copies stay byte-identical and pnpm skills:check passes.

pnpm verify passes locally — lint, typecheck, build, and every test suite.

@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.

Stale comment

Review verdict: COMMENT

Prior HIGH/MED blockers from the last pass are fixed in code at 51a61f1 (watch defaults, watcher soft-fail, missing-assets latch, SPA /events/, bootstrap eventsPath, server-only keep-alive, -H/launch honesty). No BLOCKER and no consensus HIGH remain.

Remaining signal is MED coverage stickiness plus a few residuals:

  1. DEFAULT_WATCH_IGNORE is production-correct, but tests do not lock that .tmp-effort-* / .tmp-explorer-* stay out of the default list.
  2. --open ready∧path sequencing is fixed in CLI glue, but untested as a composed unit; path can still be snapshotted before listen-time asset gate.
  3. New residuals: GraphQL child death before ready leaves parent alive; asset gate accepts bare index.html; HTTPS wording in Next example docs drifts after dropping --https.

Coverage plan (ordered)

  1. Lock exact DEFAULT_WATCH_IGNORE + negative matches for .tmp-effort-* / .tmp-explorer-*.
  2. Extract/cover --open sequencer (ready-first, path-first, no double-launch).
  3. Parent exits when GraphQL child dies before ready.
  4. Live replaceConfig when assets vanish with preset unchanged.
  5. Harden/try/catch asset gate beyond bare index.html.

Perspectives: correctness-and-contracts, test-coverage-robustness, cli-and-runtime, explorer-package, dx-and-examples. Models: grok-4.5 high / composer-2.5.

Open in Web View Automation 

Sent by Cursor Automation: Flatbread PR Review

Comment thread packages/flatbread/src/graphql/watchIgnore.ts
Comment thread packages/flatbread/src/cli/index.ts Outdated
Comment thread packages/flatbread/src/cli/index.ts Outdated
Comment on lines +90 to +97
openPath = resolveOpenPath(loaded.config?.content);
explorer = openPath === EXPLORER_ENDPOINT;
} catch {
// Config may be missing during init; fall back to GraphQL sandbox.
}
settleOpenPath(openPath);

welcome({ port, explorer });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDresolveOpenPath / settleOpenPath snapshot the open path at config-load time, while mountExplorer re-checks assets at listen. Assets built or deleted in that window can open /graphql while explorer is live (or the reverse). welcome() also runs before ready.

Minimal fix: Re-resolve / re-check explorerAssetsPresent() inside onReady; optionally defer or label welcome until ready.

Comment thread packages/flatbread/src/cli/runner.ts
Comment thread packages/explorer/src/node/staticDir.ts
Comment thread packages/flatbread/src/graphql/explorerMount.ts
Comment thread examples/nextjs/package.json
Comment thread packages/flatbread/src/graphql/liveServer.ts
Second review pass moved the verdict to COMMENT with no blockers. Close
five residuals it raised, plus three false HTTPS claims it missed.

Two were real defects:

1. `flatbread start` hung forever when the forked GraphQL child died
   before it sent `flatbread-gql-ready` — a broken config or a taken
   port left the parent alive with nothing to do. `createGqlReadyHandler`
   now attaches `exit` and `error` listeners as soon as it is built and
   exits with the child's code when ready never arrived. After ready
   those listeners stand down, so the parent still exits once.
2. `mountExplorer.evaluate` read `index.html` right after checking that
   it exists. If the file went away between the two calls the read threw
   and, because `update` runs inside the `replaceConfig` wrapper, a
   watch-mode config reload failed. The read and inject now sit in a
   `try`/`catch` that mirrors the missing-assets branch: warn once,
   deactivate, keep serving. The warn-once latch clears only on success.

Three smaller ones:

3. Tests now pin the exact contents of `DEFAULT_WATCH_IGNORE` and assert
   the AVA fixture prefixes are absent from it. Re-baking them in would
   have stayed green otherwise, which defeats the point of the change.
4. The watcher callback-error test asserts its log line, matching its
   push-failure sibling.
5. `--open` resolves the browser path inside `onReady` instead of at
   config-load time. `resolveOpenPath` checks for built assets, so the
   answer is now taken as close as possible to when the child mounts.

Declined two suggestions. Extracting a "ready and path settled" sequencer
would test a two-line promise chain whose single-launch guarantee already
comes from the tested `handled` latch. Requiring a hashed `assets/*.js`
file in `explorerAssetsPresent()` would tie the mount gate to Vite's
output naming, so a build-config change could silently disable the
explorer.

Docs: `examples/nextjs/README.md` and `CONTRIBUTING.md` still promised
local HTTPS from `pnpm dev`, which stopped being true when `--https` left
the script. Three more claims were wrong and unreported:
`flatbread-flow-agentic-workflows.md` cited a port `5058` HTTPS listener
that never existed, `docs/local-dev-loop.md` told readers to avoid an
"HTTPS convenience script" that is now the same command, and
`packages/flatbread/README.md` repeated the HTTPS claim.

Verified with `pnpm verify`.

Co-authored-by: Cursor <cursoragent@cursor.com>
Change-Id: I4bf1bdd5e702c385ccb477932bf18652bcb2f4fe
@tonyketcham

Copy link
Copy Markdown
Collaborator Author

Residuals from the second pass are closed in 2d590b7. Five taken, two declined.

Taken — the two that were real defects

  • Child death before ready left the parent hanging. If the forked GraphQL child exited or errored before sending flatbread-gql-ready — broken config, port already taken — flatbread start stayed alive with no corunner, no launch, and no exit. createGqlReadyHandler now attaches exit and error listeners as soon as it is built and exits with the child's code (or 1 for null). After ready they stand down, so the parent still exits exactly once. Covered by three new tests including the after-ready no-double-exit case.
  • evaluate() could throw between the assets check and the read. Since update runs inside the replaceConfig wrapper, that throw surfaced as a failed config reload under --watch. The read and inject now sit in a try/catch mirroring the missing-assets branch. Worth noting: the inMissingAssets latch moved so it clears only on success, otherwise a recovered read would leave it stale. Test drives a real EISDIR by putting a directory where index.html belongs.

Taken — three smaller ones

DEFAULT_WATCH_IGNORE contents are now pinned by deep-equal with negative assertions for both fixture prefixes; without that, re-baking them in would have stayed green and quietly undone the decoupling. The watcher callback-error test asserts its log line. And --open resolves the path inside onReady rather than at config-load time, so the explorerAssetsPresent() check happens as close as possible to when the child mounts.

Declined, with reasons

  • Extract a "ready ∧ path settled → launch once" sequencer. That is a two-line promise chain, and the no-double-launch property it would prove already comes from the handled latch in ready.ts, which has its own test. The abstraction would add ceremony without adding safety.
  • Require a hashed assets/*.js in explorerAssetsPresent(). This ties the mount gate to Vite's output naming, so a future build-config change could silently stop the explorer from mounting. The partial-build case it guards against is rarer than the breakage it invites.

Docs

The stale HTTPS wording in examples/nextjs/README.md and CONTRIBUTING.md is fixed — the HTTPS-versus-headless split collapsed into one instruction, since both commands now do the same thing.

Sweeping the rest of the repo turned up three more false claims that were not reported, and one was worse than the flagged pair: flatbread-flow-agentic-workflows.md stated that flatbread start "always binds port 5057 (and 5058 for HTTPS)" and cited a line range in cli/index.ts that no longer exists. That port never existed; the number came from the port + 1 banner text this PR deleted. Also fixed: docs/local-dev-loop.md told readers to prefer the long command to avoid an "HTTPS convenience script" that is now the identical command, and packages/flatbread/README.md repeated the HTTPS claim. The only HTTPS mentions left in the repo's markdown are the two that correctly say Flatbread serves plain HTTP.

pnpm verify passes locally.

@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.

Stale comment

Verdict: REQUEST_CHANGES

Adversarial review of 2d590b7 (bbcaf42…2d590b7). Launch ordering and several prior residuals are closed. Remaining blockers for merge confidence are the weak explorer asset gate, the public test-only static-dir override, and under-tested ready∧path / welcome glue.

Closed since last sync

  • DEFAULT_WATCH_IGNORE stickiness tests
  • Child exit/error before ready (handlers present; exit covered)
  • readFileSync race on explorer update
  • HTTPS / plain-HTTP docs drift
  • Watcher callback error log assert

Still open (priority)

  1. MEDexplorerAssetsPresent() is index-only; partial Vite builds mount/--open a broken SPA
  2. MEDsetExplorerStaticDirOverride is on the package main export surface
  3. MED--open ready∧content→launch sequencer has no composed regression test
  4. MEDwelcome({ explorer }) still uses the config-load snapshot before ready
  5. MEDready.test.ts never calls emitError (handler exists; coverage hole)
  6. MEDCONTRIBUTING.md HMR step uses bare flatbread (not on PATH in the monorepo)

Coverage plan

  1. HTML-only dir → explorerAssetsPresent() false; HTML + hashed assets/*.js → true
  2. Extracted ready∧path sequencer: ready-first / path-first / no double-launch
  3. emitError before ready → exit once, log, no spawn
  4. Welcome vs ready: banner matches post-ready resolveOpenPath (or provisional label)
  5. After gate tighten: flip index-only fixtures in openPath/mount tests

Perspectives: correctness-and-contracts, test-coverage-robustness, cli-and-runtime, explorer-package, docs-and-positioning. Models: grok-4.5 (high) / composer-2.5.

Open in Web View Automation 

Sent by Cursor Automation: Flatbread PR Review

Comment thread packages/explorer/src/node/staticDir.ts
Comment on lines +1 to +5
export {
explorerAssetsPresent,
getExplorerStaticDir,
setExplorerStaticDirOverride,
EXPLORER_BOOTSTRAP_PATH,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (consensus)setExplorerStaticDirOverride is re-exported from the package main entry. The JSDoc says “tests only,” and the README public-export table omits it, but any production caller can still import and mutate this process-global seam.

Minimal fix: Drop the override from the public export surface (non-exported helper, or @flatbread/explorer/test / export condition) and point Flatbread tests at that path.

Comment on lines +84 to +90
onReady: open
? () => {
void loadedOpenContent.then((content) => {
void launch(port, resolveOpenPath(content));
});
}
: undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (consensus)--open correctly waits on ready ∧ loadedOpenContent, then re-resolveOpenPath before launch. That sequencer lives only in untested CLI glue (ready.test / openPath.test never compose with settle). A launch-before-both-gates or double-launch regression would not fail CI.

Minimal fix: Extract a pure “ready ∧ content settled → resolveOpenPath → launch once” helper; cover ready-first, path-first, and no double-launch.

Comment on lines +93 to +106
let openPath = GRAPHQL_ENDPOINT;
let explorer = false;
let contentForOpen: readonly ContentEntry[] | undefined;
try {
const loaded = await loadFlatbreadConfig(process.cwd());
contentForOpen = loaded.config?.content;
openPath = resolveOpenPath(contentForOpen);
explorer = openPath === EXPLORER_ENDPOINT;
} catch {
// Config may be missing during init; fall back to GraphQL sandbox.
}
settleOpenContent(contentForOpen);

welcome({ port, explorer });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (consensus)welcome({ explorer }) still snapshots resolveOpenPath at config-load and prints before flatbread-gql-ready. The banner can advertise / while the child mounts GraphQL-only (or the reverse), even though --open re-resolves later.

Minimal fix: Re-resolve or defer welcome until after ready (or label the explorer line provisional).

Comment thread packages/flatbread/src/cli/ready.ts
Comment thread CONTRIBUTING.md Outdated
Comment thread packages/explorer/src/web/core/endpoints.ts
CI failed on `test (22.x, ubuntu-latest)` with:

  inotify_add_watch on '.../.tmp-effort-live-HAxTYp' failed:
  No such file or directory

Watch mode subscribes to the repository root, because content paths
resolve against `process.cwd()`. AVA runs four test files at once, and
`liveServerEffortGraph.test.ts` creates and deletes `.tmp-effort-live-*`
directories under that root. Parcel's initial scan reached one after it
was gone.

The ignore globs never closed this. `**/.tmp-effort-*/**` matches paths
*inside* the directory, not the directory itself, and parcel registers
each directory with inotify on the way down — which is why the error
names the directory, not a child. Earlier green runs were luck.

Two changes:

1. `directoryIgnore()` returns both forms, the bare directory and its
   contents glob, and the production defaults now use it. Parcel skips
   `node_modules`, `.git`, `dist`, and `.journal` outright instead of
   registering them and then filtering their children.
   `buildWatchIgnore` expands any caller glob ending in `/**` the same
   way, so callers cannot reintroduce the hole by accident.
2. Subscribe retries widen from three attempts to five with exponential
   backoff. This is real hardening, not a test crutch: deleting a
   directory while the server starts should not crash it. The delays are
   injectable so tests do not sleep.

This is not a proof against every ordering. Continuous churn in an
unignored directory under the watched root could still exhaust the
retries. It closes the known failure and narrows the rest.

Local runs never caught this: macOS uses FSEvents, which watches a tree
without the per-directory syscall that races on Linux inotify.

Verified with `pnpm verify`.

Co-authored-by: Cursor <cursoragent@cursor.com>
Change-Id: I6529ad43c054d00c23174789cb18202be926adf1

@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.

Stale comment

Review verdict: REQUEST_CHANGES

Adversarial DAG review of PR #233 @ 0c6b581 (merge-base bbcaf42b). Models: grok-4.5 (HIGH) / composer-2.5 (MED+LOW). Perspectives: correctness-and-contracts, test-coverage-robustness, cli-and-runtime, explorer-package, docs-and-positioning.

Blocking first: relative ?endpoint= still ignores the injected locationOrigin. normalizeGraphqlUrl falls back to window.location.origin / localhost:5057, and endpoints.test.ts locks 99995057. Vite-on-5173 with ?endpoint=/alt/graphql will talk to the wrong origin.

Still open (MED), carried from prior syncs:

  1. Public setExplorerStaticDirOverride on @flatbread/explorer’s only entry
  2. explorerAssetsPresent() = bare existsSync('index.html') (dir-as-index can lie to --open / welcome)
  3. --open ready∧path sequencer untested (runtime looks correct; no binding test)
  4. Welcome snapshots explorer at config-settle; --open re-resolves after ready
  5. gqlChild error-before-ready wired but only exit is tested
  6. CONTRIBUTING HMR still teaches bare flatbread start --watch
  7. buildWatchIgnore expands …/** → bare+contents, but bare extras never gain /**

Closed since 51a61f1 / new in 0c6b581: HTTPS docs drift; DEFAULT watch-ignore stickiness; inotify directory-pair ignores + injectable subscribe retries (good hardening — narrow permanent inotify_add_watch failures later).

Coverage plan (priority)

  1. HIGHendpoints.test.ts: relative ?endpoint= against injected origin (flip the 99995057 lock)
  2. MEDready.test.ts: emitError before ready
  3. MED — extract --open sequencer; ready-first / path-first / no double-launch
  4. MEDstaticDir + openPath: dir-as-index.html → absent / /graphql
  5. MEDwatchIgnore: bare extra → pair, or lock intentional asymmetry

Inline comments bind the highest-signal chunks.

Open in Web View Automation 

Sent by Cursor Automation: Flatbread PR Review

Comment thread packages/explorer/src/web/core/endpoints.ts Outdated
export {
explorerAssetsPresent,
getExplorerStaticDir,
setExplorerStaticDirOverride,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (consensus)setExplorerStaticDirOverride is re-exported from the package’s only public entry. JSDoc says tests-only; any production caller can still mutate this process-global seam.

Minimal fix: Drop it from . exports; expose via @flatbread/explorer/testing / @internal and point Flatbread tests there.

Comment thread packages/explorer/src/node/staticDir.ts
Comment on lines +84 to +90
onReady: open
? () => {
void loadedOpenContent.then((content) => {
void launch(port, resolveOpenPath(content));
});
}
: undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (consensus)--open correctly waits on onReadyloadedOpenContent, then late resolveOpenPath. That sequencer lives only in untested sade glue (ready.test / openPath.test never compose).

Minimal fix: Extract a pure “ready ∧ content settled → resolveOpenPath → launch once” helper; cover ready-first, path-first, and no double-launch.

Comment on lines +99 to +106
openPath = resolveOpenPath(contentForOpen);
explorer = openPath === EXPLORER_ENDPOINT;
} catch {
// Config may be missing during init; fall back to GraphQL sandbox.
}
settleOpenContent(contentForOpen);

welcome({ port, explorer });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (consensus)welcome({ explorer }) snapshots resolveOpenPath at config-settle time. --open re-resolves after ready. An asset flip in that window desyncs the banner from the launched tab.

Minimal fix: Derive welcome explorer from the same late resolution used by --open, or re-check assets immediately before printing.

Comment thread packages/flatbread/src/cli/ready.ts
Comment thread packages/flatbread/src/graphql/watchIgnore.ts Outdated
Comment thread CONTRIBUTING.md Outdated
Third review pass. Four fixes, three declines.

1. `resolveGraphqlEndpoint` takes a `locationOrigin` argument, and every
   branch honored it except the `?endpoint=` one, which called
   `normalizeGraphqlUrl` and let it re-derive its own base from `window`.
   In a browser both agree, so nothing was broken in practice — but the
   argument was a lie on that path, and a test pinned the wrong answer.
   `normalizeGraphqlUrl` now takes the origin as an optional second
   argument and `resolveGraphqlEndpoint` threads it through. A bare host
   like `example.com/graphql` is absolute, so the recursive call does not
   forward an origin.
2. `explorerAssetsPresent()` used `existsSync`, which is true for a
   directory. A directory named `index.html` made the gate report assets
   present, so `--open` opened `/` and the banner advertised an explorer
   that could not serve. It now requires a regular file.
3. `ready.ts` wires an `error` listener for a child that dies before
   ready, but only `exit` was tested. Added both cases, including that an
   error after ready does not exit twice.
4. `buildWatchIgnore` expanded a contents glob into the bare directory
   form but not the reverse, so a bare pattern left the contents watched.
   The expansion is symmetric now. A last segment with a non-leading dot,
   such as `**/*.log`, stays a file glob.

Fix 2 changed what an older test proves: a directory named `index.html`
no longer survives the present check, so that case now exercises the
missing-assets branch rather than the read failure it was written for.
Renamed it to say what it tests. The `try`/`catch` around the read stays
as a guard for a true race — the file going away between the check and
the read — which no test can trigger on demand.

Declined three. Hiding `setExplorerStaticDirOverride` is not possible:
`packages/flatbread` is a separate package and its tests need the export.
Extracting an `--open` sequencer would test a two-line promise chain
whose single-launch guarantee already comes from the tested `handled`
latch. The welcome banner prints before the server is ready by design, so
it uses the config-load-time answer while `--open` re-resolves later.

Docs: the CONTRIBUTING HMR step now uses `pnpm exec flatbread`, since the
CLI is not on PATH.

Verified with `pnpm verify`.

Co-authored-by: Cursor <cursoragent@cursor.com>
Change-Id: I9409ae0a1f8551765c221be0311dd68f6c284687
@tonyketcham

Copy link
Copy Markdown
Collaborator Author

Third pass resolved in 71d9f4d. Four taken, three declined.

On the blocking finding — it is valid, but not for the stated reason. The claim was that Vite on 5173 with ?endpoint=/alt/graphql "will talk to the wrong origin". It will not: in a browser locationOrigin defaults to window.location.origin, so normalizeGraphqlUrl re-deriving its own base arrives at the same http://localhost:5173/alt/graphql, which Vite proxies correctly.

The real defect is narrower and worth fixing anyway: resolveGraphqlEndpoint accepts a locationOrigin argument and honours it on every branch except ?endpoint=, so the argument is a lie on one path — and endpoints.test.ts pinned the wrong answer, cementing it. normalizeGraphqlUrl now takes the origin as an optional second argument and the caller threads it through. The recursive call for a bare host like example.com/graphql deliberately does not forward an origin, since that value is already absolute.

Also taken

  • explorerAssetsPresent() used existsSync, which returns true for a directory. A directory named index.html made the gate report assets present, so --open opened / and the banner advertised an explorer that could not serve. It now requires a regular file. This is the cheap version of the hardening suggested twice — it closes the hole without tying the gate to Vite's output naming.
  • error-before-ready is now tested alongside exit, including that an error after ready does not exit twice.
  • buildWatchIgnore expanded a contents glob into its bare-directory form but not the reverse, leaving the opposite hole. The expansion is symmetric now; a last segment with a non-leading dot such as **/*.log stays a file glob.

One honest consequence worth flagging. The asset-gate fix changed what an earlier test proves. A directory named index.html no longer survives the present check, so that case now exercises the missing-assets branch rather than the read failure it was written for. I renamed the test to say what it actually tests rather than leave a name that had become false. The try/catch around the read stays as a guard for the genuine race — the file vanishing between the check and the read — which no test can trigger on demand. I would rather have an honest test name and an untriggerable guard than fake coverage.

Declined, with reasons

  • Hide setExplorerStaticDirOverride from the public entry. Not possible. packages/flatbread is a separate package and its tests need the export; there is no way to share it without exporting it. It is already documented as test-only and kept out of the README's export table.
  • Extract an --open ready-and-path sequencer. Third time asked, same answer: it is a two-line promise chain, and the no-double-launch property already comes from the handled latch in ready.ts, which is tested.
  • Welcome snapshots the explorer state at config-settle while --open re-resolves after ready. That asymmetry is deliberate. The banner prints immediately so the user sees the URLs without waiting; --open waits because opening a browser at a refused connection is worse than a slightly stale banner.

pnpm verify passes: 102 explorer tests, full AVA and Vitest suites, lint, typecheck, build.

@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.

Stale comment

Review verdict

REQUEST_CHANGES — two or more independent HIGHs (ready-handler post-ready hang; explorer onboarding docs using bare flatbread start vs AGENTS/pnpm exec contract).

Prior HIGH on relative ?endpoint= ignoring locationOrigin and directory-as-index.html are closed at 71d9f4d. New blockers below.

Closed since last pass

  • Relative ?endpoint= now honors injectable locationOrigin (tests lock 9999).
  • explorerAssetsPresent() requires index.html to be a regular file.
  • emitError before ready is covered in ready.test.ts.
  • Bare watchIgnore directory extras expand via directoryIgnore.
  • CONTRIBUTING HMR step uses pnpm exec flatbread.

Still open / new

  1. HIGH — After first flatbread-gql-ready, exit is ignored while closeonExit is registered only after onReady/spawnCorunner; spawn throw or early close hangs the parent.
  2. HIGHpackages/explorer/README.md “Try it locally” still shows bare flatbread start.
  3. MED — Endpoint fallback still gates same-origin /graphql on window; relative ?endpoint=/ stays SPA / while absolute origin / becomes /graphql.
  4. MED — Public setExplorerStaticDirOverride; non-serial openPath tests can race it; welcome/--open still snapshot open path at parent config-load; --open sequencer still untested end-to-end.

Coverage plan (priority)

  • ready.test.ts — spawnCorunner throws after ready → parent still exits; child close before post-ready listener.
  • endpoints.test.ts — no-window + injected origin; normalizeGraphqlUrl('/') vs absolute-origin empty path agree; window same-origin happy path.
  • liveServer.test.tsENOENT retry; non-transient reject; opts.ignore; unsubscribe swallow/reject.
  • explorerMount.test.ts — HTML without </head> still gets bootstrap.

Perspectives: correctness, coverage, cli-runtime, explorer-package, docs. Models: grok-4.5 high / composer-2.5.

Open in Web View Automation 

Sent by Cursor Automation: Flatbread PR Review

Comment thread packages/flatbread/src/cli/ready.ts Outdated
Comment thread packages/explorer/README.md
Comment on lines +42 to +57
// Standalone static host with no bootstrap: talk to local Flatbread.
if (
typeof window !== 'undefined' &&
!window.__FLATBREAD_EXPLORER__ &&
locationOrigin.includes('://')
) {
// Prefer same-origin /graphql when served by Flatbread; fall back for file:// or alien hosts.
try {
const url = new URL(locationOrigin);
if (url.protocol === 'http:' || url.protocol === 'https:') {
return new URL(DEFAULT_GRAPHQL_PATH, locationOrigin).href;
}
} catch {
// fall through
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (consensus) — No-query/no-bootstrap fallback still requires typeof window !== 'undefined', so an injected http(s) locationOrigin (no window) is ignored and callers get localhost:5057. That undercuts the same injectable-origin contract just fixed for ?endpoint=.

Minimal fix: Honor http(s) locationOrigin without a window gate; add a no-window + injected-origin edge test.

Comment thread packages/explorer/src/web/core/endpoints.ts
Comment thread packages/flatbread/src/cli/openPath.test.ts Outdated
Comment on lines +10 to +16
/**
* Test-only: force `getExplorerStaticDir()` to `dir`.
* Pass `undefined` to clear. Not for production callers.
*/
export function setExplorerStaticDirOverride(dir: string | undefined): void {
staticDirOverride = dir;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (still open)setExplorerStaticDirOverride remains on the package’s public Node entry. JSDoc says tests-only; any production caller can still mutate this process-global seam.

Minimal fix: Drop it from the default export (test-only subpath or inject through mountExplorer deps), and keep remaining consumers test.serial.

Comment on lines +92 to +106

let openPath = GRAPHQL_ENDPOINT;
let explorer = false;
let contentForOpen: readonly ContentEntry[] | undefined;
try {
const loaded = await loadFlatbreadConfig(process.cwd());
contentForOpen = loaded.config?.content;
openPath = resolveOpenPath(contentForOpen);
explorer = openPath === EXPLORER_ENDPOINT;
} catch {
// Config may be missing during init; fall back to GraphQL sandbox.
}
settleOpenContent(contentForOpen);

welcome({ port, explorer });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (still open) — Welcome/resolveOpenPath still snapshot at parent config-load, while --open re-resolves after ready and the child mounts independently. An asset flip in that window desyncs the banner from the launched tab (and under --watch neither refreshes).

Minimal fix: Derive welcome URLs from the same post-ready resolution used by --open (or child-reported mount state).

Comment thread CONTRIBUTING.md
…state tests

Last round of review fixes. Two were worth taking on merit; two are cheap
guards against a bad first experience.

1. `?endpoint=/` pointed the client at the explorer's own HTML instead of
   the API. `normalizeGraphqlUrl` mapped an empty or root path to
   `/graphql`, but only for absolute URLs — the relative branch skipped
   it, so `/` and `http://host/` disagreed. Both now resolve to
   `/graphql`, and a real relative path such as `/alt/graphql` still
   passes through untouched.
2. `openPath.test.ts` mixed plain and serial tests over
   `setExplorerStaticDirOverride`, which is module-global. AVA runs the
   tests in one file at the same time, so the serial case pointing the
   override at an empty directory could race the cases that need real
   assets. Every case in that file is serial now. No other test file
   shares the hazard.
3. A throwing `spawnCorunner` escaped into an EventEmitter listener, so
   Node exited on an uncaught exception. It crashed rather than hung, but
   a raw stack trace is a poor way to say a dev script would not start.
   The spawn is wrapped now: log the command that failed, then exit 1.
4. The explorer README and both skill setup copies showed bare
   `flatbread start`, which is not on PATH. They use `npx` now so a reader
   can copy the line and run it. The monorepo section still uses
   `pnpm exec`, which is right for contributors working in this repo.

Verified with `pnpm verify`.

Change-Id: If1dc2a178035439a89579089a5f997c82a721357
@cursor

cursor Bot commented Jul 29, 2026

Copy link
Copy Markdown

@Mergifyio queue owner-bypass

@mergify

mergify Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

queue owner-bypass

☑️ Command disallowed due to command restrictions in the Mergify configuration.

Details
  • sender-permission >= write

@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.

Review verdict

COMMENTba1be3f closed the prior HIGH blockers (?endpoint=//graphql, sync spawnCorunner throw → exit 1, PATH for explorer README / skill §4). Remaining items are MED: whitespace ?endpoint= still hardcodes :5057, corunner async error without close, onReady before a failed spawn, public test-only override docs, and a few untested watch/teardown contracts. No consensus HIGH.

Tip fixes verified

  • Relative ?endpoint=/ maps to /graphql (absolute / path already did).
  • Sync spawn failure logs and onExit(1) instead of an uncaught exception.
  • openPath.test.ts is fully serial over setExplorerStaticDirOverride.
  • Skill/explorer “Try it locally” uses npx flatbread start.

Chunk-bound feedback (priority)

  1. MED endpoints.ts — whitespace-only ?endpoint= ignores locationOrigin (hardcoded :5057).
  2. MED (consensus) corunner — register error as well as close after spawn.
  3. MED ready.ts — defer onReady until after successful corunner spawn.
  4. MED public setExplorerStaticDirOverride — document as test-only in the README export table.
  5. MED (coverage) rejected replaceConfig must not flip server.explorer; EINVAL unsubscribe swallow needs a negative test.
  6. MED staticDir.test.ts — nest/concurrency: 1 + pin package-root path.

Coverage plan (short)

  • endpoints.test.ts — whitespace ?endpoint= with non-5057 locationOrigin; optional window-stubbed same-origin.
  • ready.test.ts — corunner error without close; onReady not called when spawn throws.
  • liveServer*.test.ts — rejected replaceConfig; unsubscribe EINVAL vs other errors.
  • explorerMount.test.ts — HTML without </head> still injects bootstrap.

Reviewer scoreboard

  • correctness-and-contracts: 0 findings, 3 coverage gaps, signal:LOW
  • test-coverage-robustness: 6 findings, 6 coverage gaps, signal:MED
  • cli-and-runtime: 4 findings, 4 coverage gaps, signal:MED
  • explorer-package: 3 findings, 4 coverage gaps, signal:MED
  • docs-and-positioning: 0 findings, 0 coverage gaps, signal:LOW

Perspectives: correctness, coverage, cli-runtime, explorer-package, docs. Models: grok-4.5 high / composer-2.5. Not dependency-only (79 non-dep files).

Open in Web View Automation 

Sent by Cursor Automation: Flatbread PR Review

const trimmed = value.trim();
if (!trimmed) {
return `http://localhost:${DEFAULT_PORT}${DEFAULT_GRAPHQL_PATH}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED — After trim(), empty/whitespace returns hardcoded http://localhost:5057/graphql, ignoring locationOrigin. So ?endpoint=%20%20%20 on :5173/:9999 skips bootstrap and never gets same-origin /graphql.

ba1be3f fixed relative ?endpoint=/. Treat whitespace-only ?endpoint= as absent (fall through), and for empty trim in normalizeGraphqlUrl return new URL(DEFAULT_GRAPHQL_PATH, locationOrigin).href. Re-assert in endpoints.test.ts with a non-5057 origin.


for (const script of [gqlChild, targetProcess]) {
script.on('close', (code) => {
deps.onExit(code ?? 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (consensus) — After a successful spawn, only close is registered on the corunner. Node may emit error without close, leaving GraphQL up and a silent parent.

Register error on the spawned corunner (mirror gql pre-ready handling) and exit non-zero. Cover in ready.test.ts: corunner emits error without close → parent exits non-zero.

handled = true;

deps.onReady?.();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDonReady (and thus --open) runs before spawnCorunner. A sync spawn failure still opens the browser, then exits 1.

Defer onReady until after a successful corunner spawn; keep the current order for server-only. Assert in ready.test.ts that onReady is not invoked when spawnCorunner throws.

export {
explorerAssetsPresent,
getExplorerStaticDir,
setExplorerStaticDirOverride,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (still open)setExplorerStaticDirOverride ships on the package’s only public "." entry. JSDoc says tests-only; the README export table omits it, so integrators can still mutate process-global static resolution.

Prefer an explicit “test-only” row in the README table over hiding the export (flatbread AVA files need this seam). Keep within-file serialization wherever the override is used.

reloader.replaceConfig = async (nextConfig) => {
const result = await replaceConfig(nextConfig);
if (result.status === 'committed') {
explorerHandle.update(nextConfig.content);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (coverage) — Explorer gate updates only when replaceConfig returns committed. Tests exercise successful reload only.

Add a rejected replaceConfig case in liveServerExplorerWatch.test.ts and assert server.explorer stays put.

} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes('Unable to remove watcher')) throw error;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (coverage)unsubscribe EINVAL (“Unable to remove watcher”) is swallowed with no test; non-matching errors must still propagate.

Stub unsubscribe to throw matching vs non-matching errors; assert close() resolves only for the former (liveServer.test.ts).


afterEach(() => {
setExplorerStaticDirOverride(undefined);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MED (consensus) — Module-global override + top-level afterEach can race if Node’s runner runs the two root describes concurrently; basename-only asserts do not pin package-root resolution.

Nest suites under one parent describe (or { concurrency: 1 }); pin the resolved path against package root / override, not basename shape alone.

@tonyketcham
tonyketcham disabled auto-merge July 29, 2026 10:00
@tonyketcham
tonyketcham merged commit c7a6c71 into main Jul 29, 2026
20 checks passed
@tonyketcham
tonyketcham deleted the toeknee/explorer-package-stack-merge-c899 branch July 29, 2026 10:00
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.

2 participants