feat(explorer): ship Effort Graph visualizer with flatbread start - #233
Conversation
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
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
|
@Mergifyio queue |
☑️ Command disallowed due to command restrictions in the Mergify configuration.Details
|
|
Tick the box to add this pull request to the merge queue (same as
|
|
@Mergifyio refresh |
☑️ Command disallowed due to command restrictions in the Mergify configuration.Details
|
There was a problem hiding this comment.
Stale comment
Review verdict
REQUEST_CHANGES — consensus HIGH on production
--watchignore globs (liveServer.ts:270-282) plus two further independent HIGHs (watcher error/pushsoft-paths;mountExplorer.updatemissing-assets). Prior #230 blockers are closed at9fde89a/58a9a82/75fc81a.What still blocks
- 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.- Watcher soft-fail untested — error callback and
coordinator.pushthrows are swallowed with no tests; a regression that re-rejects the server or drops all pushes would stay green.updatemissing-assets untested — initial mount gate is covered; reload-timereplaceConfig/updatewhen assets vanish is not.Prior #230 status
Closed: openPath↔mount assets gate, mount
/graphql+/events/missing-assets/extensionless, sticky watch + liveserver.explorer, explorerpreplay:efforts, vitebase.Coverage plan (highest priority)
- Watch ignore:
.tmp-live-server-test-*still commits;.tmp-effort-*/.tmp-explorer-*do not;.journalignored.- Watcher error /
pushthrow keep server alive; normal event still reindexes.- After active mount,
updatewith empty static override warns once, deactivates, then restore re-enables.- Assert bootstrap
eventsPath === '/events'; treat/events/like GraphQL prefix exclusion.- Server-only
runnerkeep-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.
Sent by Cursor Automation: Flatbread PR Review
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
|
All eight review findings are resolved in Real bugs
Watch ignore contract Production no longer knows any test-fixture name.
The runner now fires an Two places I took a different approach than suggested
Also covered: watcher error and Docs were rewritten to this repo's plain-English rules:
|
There was a problem hiding this comment.
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/, bootstrapeventsPath, server-only keep-alive,-H/launch honesty). No BLOCKER and no consensus HIGH remain.Remaining signal is MED coverage stickiness plus a few residuals:
- DEFAULT_WATCH_IGNORE is production-correct, but tests do not lock that
.tmp-effort-*/.tmp-explorer-*stay out of the default list.--openready∧path sequencing is fixed in CLI glue, but untested as a composed unit; path can still be snapshotted before listen-time asset gate.- 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)
- Lock exact
DEFAULT_WATCH_IGNORE+ negative matches for.tmp-effort-*/.tmp-explorer-*.- Extract/cover
--opensequencer (ready-first, path-first, no double-launch).- Parent exits when GraphQL child dies before ready.
- Live
replaceConfigwhen assets vanish with preset unchanged.- Harden/
try/catchasset gate beyond bareindex.html.Perspectives: correctness-and-contracts, test-coverage-robustness, cli-and-runtime, explorer-package, dx-and-examples. Models: grok-4.5 high / composer-2.5.
Sent by Cursor Automation: Flatbread PR Review
| 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 }); |
There was a problem hiding this comment.
MED — resolveOpenPath / 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.
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
|
Residuals from the second pass are closed in Taken — the two that were real defects
Taken — three smaller ones
Declined, with reasons
Docs The stale HTTPS wording in Sweeping the rest of the repo turned up three more false claims that were not reported, and one was worse than the flagged pair:
|
There was a problem hiding this comment.
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_IGNOREstickiness tests- Child
exit/errorbefore ready (handlers present; exit covered)readFileSyncrace on explorerupdate- HTTPS / plain-HTTP docs drift
- Watcher callback error log assert
Still open (priority)
- MED —
explorerAssetsPresent()is index-only; partial Vite builds mount/--opena broken SPA- MED —
setExplorerStaticDirOverrideis on the package main export surface- MED —
--openready∧content→launch sequencer has no composed regression test- MED —
welcome({ explorer })still uses the config-load snapshot before ready- MED —
ready.test.tsnever callsemitError(handler exists; coverage hole)- MED —
CONTRIBUTING.mdHMR step uses bareflatbread(not on PATH in the monorepo)Coverage plan
- HTML-only dir →
explorerAssetsPresent()false; HTML + hashedassets/*.js→ true- Extracted ready∧path sequencer: ready-first / path-first / no double-launch
emitErrorbefore ready → exit once, log, no spawn- Welcome vs ready: banner matches post-ready
resolveOpenPath(or provisional label)- 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.
Sent by Cursor Automation: Flatbread PR Review
| export { | ||
| explorerAssetsPresent, | ||
| getExplorerStaticDir, | ||
| setExplorerStaticDirOverride, | ||
| EXPLORER_BOOTSTRAP_PATH, |
There was a problem hiding this comment.
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.
| onReady: open | ||
| ? () => { | ||
| void loadedOpenContent.then((content) => { | ||
| void launch(port, resolveOpenPath(content)); | ||
| }); | ||
| } | ||
| : undefined, |
There was a problem hiding this comment.
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.
| 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 }); |
There was a problem hiding this comment.
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).
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
There was a problem hiding this comment.
Stale comment
Review verdict: REQUEST_CHANGES
Adversarial DAG review of PR #233 @
0c6b581(merge-basebbcaf42b). 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 injectedlocationOrigin.normalizeGraphqlUrlfalls back towindow.location.origin/localhost:5057, andendpoints.test.tslocks9999→5057. Vite-on-5173 with?endpoint=/alt/graphqlwill talk to the wrong origin.Still open (MED), carried from prior syncs:
- Public
setExplorerStaticDirOverrideon@flatbread/explorer’s only entryexplorerAssetsPresent()= bareexistsSync('index.html')(dir-as-index can lie to--open/ welcome)--openready∧path sequencer untested (runtime looks correct; no binding test)- Welcome snapshots explorer at config-settle;
--openre-resolves after readygqlChilderror-before-ready wired but onlyexitis tested- CONTRIBUTING HMR still teaches bare
flatbread start --watchbuildWatchIgnoreexpands…/**→ bare+contents, but bare extras never gain/**Closed since
51a61f1/ new in0c6b581: HTTPS docs drift; DEFAULT watch-ignore stickiness; inotify directory-pair ignores + injectable subscribe retries (good hardening — narrow permanentinotify_add_watchfailures later).Coverage plan (priority)
- HIGH —
endpoints.test.ts: relative?endpoint=against injected origin (flip the9999→5057lock)- MED —
ready.test.ts:emitErrorbefore ready- MED — extract
--opensequencer; ready-first / path-first / no double-launch- MED —
staticDir+openPath: dir-as-index.html→ absent //graphql- MED —
watchIgnore: bare extra → pair, or lock intentional asymmetryInline comments bind the highest-signal chunks.
Sent by Cursor Automation: Flatbread PR Review
| export { | ||
| explorerAssetsPresent, | ||
| getExplorerStaticDir, | ||
| setExplorerStaticDirOverride, |
There was a problem hiding this comment.
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.
| onReady: open | ||
| ? () => { | ||
| void loadedOpenContent.then((content) => { | ||
| void launch(port, resolveOpenPath(content)); | ||
| }); | ||
| } | ||
| : undefined, |
There was a problem hiding this comment.
MED (consensus) — --open correctly waits on onReady ∧ loadedOpenContent, 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.
| openPath = resolveOpenPath(contentForOpen); | ||
| explorer = openPath === EXPLORER_ENDPOINT; | ||
| } catch { | ||
| // Config may be missing during init; fall back to GraphQL sandbox. | ||
| } | ||
| settleOpenContent(contentForOpen); | ||
|
|
||
| welcome({ port, explorer }); |
There was a problem hiding this comment.
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.
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
|
Third pass resolved in On the blocking finding — it is valid, but not for the stated reason. The claim was that Vite on 5173 with The real defect is narrower and worth fixing anyway: Also taken
One honest consequence worth flagging. The asset-gate fix changed what an earlier test proves. A directory named Declined, with reasons
|
There was a problem hiding this comment.
Stale comment
Review verdict
REQUEST_CHANGES — two or more independent HIGHs (ready-handler post-ready hang; explorer onboarding docs using bare
flatbread startvs AGENTS/pnpm execcontract).Prior HIGH on relative
?endpoint=ignoringlocationOriginand directory-as-index.htmlare closed at71d9f4d. New blockers below.Closed since last pass
- Relative
?endpoint=now honors injectablelocationOrigin(tests lock 9999).explorerAssetsPresent()requiresindex.htmlto be a regular file.emitErrorbefore ready is covered inready.test.ts.- Bare watchIgnore directory extras expand via
directoryIgnore.- CONTRIBUTING HMR step uses
pnpm exec flatbread.Still open / new
- HIGH — After first
flatbread-gql-ready,exitis ignored whileclose→onExitis registered only afteronReady/spawnCorunner; spawn throw or earlyclosehangs the parent.- HIGH —
packages/explorer/README.md“Try it locally” still shows bareflatbread start.- MED — Endpoint fallback still gates same-origin
/graphqlonwindow; relative?endpoint=/stays SPA/while absolute origin/becomes/graphql.- MED — Public
setExplorerStaticDirOverride; non-serialopenPathtests can race it; welcome/--openstill snapshot open path at parent config-load;--opensequencer still untested end-to-end.Coverage plan (priority)
ready.test.ts— spawnCorunner throws after ready → parent still exits; childclosebefore post-ready listener.endpoints.test.ts— no-window + injected origin;normalizeGraphqlUrl('/')vs absolute-origin empty path agree; window same-origin happy path.liveServer.test.ts—ENOENTretry; 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.
Sent by Cursor Automation: Flatbread PR Review
| // 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| /** | ||
| * Test-only: force `getExplorerStaticDir()` to `dir`. | ||
| * Pass `undefined` to clear. Not for production callers. | ||
| */ | ||
| export function setExplorerStaticDirOverride(dir: string | undefined): void { | ||
| staticDirOverride = dir; | ||
| } |
There was a problem hiding this comment.
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.
|
|
||
| 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 }); |
There was a problem hiding this comment.
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).
…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
|
@Mergifyio queue owner-bypass |
☑️ Command disallowed due to command restrictions in the Mergify configuration.Details
|
There was a problem hiding this comment.
Review verdict
COMMENT — ba1be3f 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.tsis fully serial oversetExplorerStaticDirOverride.- Skill/explorer “Try it locally” uses
npx flatbread start.
Chunk-bound feedback (priority)
- MED
endpoints.ts— whitespace-only?endpoint=ignoreslocationOrigin(hardcoded:5057). - MED (consensus) corunner — register
erroras well ascloseafter spawn. - MED
ready.ts— deferonReadyuntil after successful corunner spawn. - MED public
setExplorerStaticDirOverride— document as test-only in the README export table. - MED (coverage) rejected
replaceConfigmust not flipserver.explorer; EINVAL unsubscribe swallow needs a negative test. - MED
staticDir.test.ts— nest/concurrency: 1+ pin package-root path.
Coverage plan (short)
endpoints.test.ts— whitespace?endpoint=with non-5057locationOrigin; optional window-stubbed same-origin.ready.test.ts— corunnererrorwithoutclose;onReadynot 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).
Sent by Cursor Automation: Flatbread PR Review
| const trimmed = value.trim(); | ||
| if (!trimmed) { | ||
| return `http://localhost:${DEFAULT_PORT}${DEFAULT_GRAPHQL_PATH}`; | ||
| } |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?.(); | ||
|
|
There was a problem hiding this comment.
MED — onReady (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, |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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; | ||
| } |
There was a problem hiding this comment.
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); | ||
| }); |
There was a problem hiding this comment.
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.


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 useseffortGraphContent(),flatbread startserves it at/, with the Apollo sandbox at/graphqland the event stream at/events. Replaces the deletedexamples/effort-vizNext.js app.What lands
@flatbread/explorerwithflatbread startmountingEINVALReview fixes
Real bugs found and fixed
GET /events/returned the explorer's HTML instead of the event stream. The SPA fallback excluded/graphqlby exact match and prefix but/eventsby exact match only, and it runs before the SSE handler.-H, --httpsadvertised a server that does not exist. Nothing readFLATBREAD_HTTPS, so the flag never changed how Flatbread listens, yet the banner printedhttps://localhost:<port+1>and--openopened an HTTPS URL. Dead plumbing and false banner lines removed; the flag still parses so existing scripts run.flatbread starthung 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()readindex.htmlright 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()usedexistsSync, which is true for a directory. A directory namedindex.htmlmade--openopen a broken explorer.inotify_add_watchrace 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
watchIgnore.tsholds the production list; tests pass their fixture globs through a new option, and a deep-equal pin stops them being reintroduced.--openwaits forflatbread-gql-readyinstead of racing startup, and resolves the path at ready time so the assets check happens as late as possible.ready.tsbehind injectedspawnandexit, unit-tested rather than forked.eventsPathcontract, and static assets with and without file extensions.openPath.test.tstests 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 DXand## Package surfaceheadings, spelled out SPA and HMR on first use, and replaced abstract "assets gate" wording with what Flatbread does. Corrected five false HTTPS claims acrossAGENTS.md,CONTRIBUTING.md,examples/nextjs/README.md,packages/flatbread/README.md,docs/local-dev-loop.md, andflatbread-flow-agentic-workflows.md— including a cited port5058HTTPS listener that never existed. CLI examples usenpxfor package consumers andpnpm execfor 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--opensequencer (it would test a two-line promise chain whose guarantee is already covered), and requiring a hashedassets/*.jsin the asset gate (it would couple the gate to Vite's output naming).Test plan
pnpm verify— lint, typecheck, build, AVA, Vitest, and 102 explorer testspnpm skills:check; bothsetup.mdcopies byte-identicaltest (22.x, ubuntu-latest)after the inotify fix@mergifyio queueor tick Mergify's queue checkboxWhy this is still blocked
.mergify.ymldefines anowner-bypassqueue rule that this PR fully satisfies —author = tonyketchamand all 19 required checks green, withbranch_protection_injection_mode: nonewaiving the review requirement. But the config has nopull_request_rulesentry that queues anything, so the queue only ever starts from the@mergifyio queuecommand, and that command is restricted tosender-permission >= write. Agent accounts are refused. One comment from a maintainer merges this.