feat(explorer): ship Effort Graph visualizer with flatbread start - #230
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
There was a problem hiding this comment.
CI remediation
Status: fixed and pushed
Failing checks:
build (20.x, ubuntu-latest)/build (22.x, ubuntu-latest):Cannot find module '@flatbread/effort-graph'duringpnpm typecheck
Root cause:
packages/explorer/tsconfig.jsonset its ownpathswith only@/*. TypeScript replaces (does not merge) parentpaths, so the root monorepo aliases — including@flatbread/effort-graph— were gone. CI typechecks before build, so there is nodistyet and the path alias is required.
Fix (bc899ad):
- Re-declare the monorepo path map under
baseUrl: "../.." - Set
libtoESNextandskipLibCheck: trueso transitive source typechecks cleanly
Verified locally: pnpm typecheck (proof + explorer) passes.
Note: mergify stack push returned HTTP 401 here, so this landed as a follow-up commit on the stack branch via a normal push (not a force push).
Sent by Cursor Automation: Flatbread - Fix CI
| "@flatbread/resolver-svimg": ["./packages/resolver-svimg/src/index.ts"], | ||
| "@flatbread/utils": ["./packages/utils/src/index.ts"], | ||
| "@flatbread/explorer": ["./packages/explorer/src/node/index.ts"], | ||
| "@flatbread/effort-graph": ["./packages/effort-graph/src/index.ts"], |
There was a problem hiding this comment.
CI failed here: child paths replaced the root map, so @flatbread/effort-graph was missing at typecheck-before-build. Restored the monorepo aliases (plus @/*) under baseUrl: "../..".
|
Tick the box to add this pull request to the merge queue (same as
|
There was a problem hiding this comment.
Stale comment
Review verdict
REQUEST_CHANGES — Consensus HIGH: CLI/
--open/welcome treat Effort Graph preset match as “mounted,” whilemountExplorerIfMatchedcan soft-fail when explorer assets are missing, plus the SPA+/graphql+/events coexistence paths are under-tested.Blocking / high findings (priority)
resolveCliOpenPath/ welcome vs mount —--openand the banner can send users to/whileserver.explorerisfalseand Apollo still owns/.resolveOpenPathcontract drift — docs say “when mounted” but only check preset match; duplicates the CLI helper and skips the assets gate.explorerMounttests — title claims Apollo retention, but there are no behavioral hits on/graphql,/events, SPA fallback, or missing-index.html→null.Coverage plan (top items)
explorerMount.test.ts— missingindex.html→ warn +nullexplorerMount.test.ts— with assets,/graphqlis not SPA HTML;/eventscallsnext()openPath.test.ts— preset matches but no assets → open/graphql, not/liveServerEffortGraph.test.ts—server.explorer === truekeeps SSE; missing assets →explorer === falseendpoints.test.ts— relative?endpoint=, host-without-scheme, whitespace, customeventsPathReviewer scoreboard
- correctness-and-contracts: 6 findings, 5 coverage gaps, signal:HIGH
- test-coverage-robustness: 6 findings, 9 coverage gaps, signal:HIGH
- cli-and-runtime: 6 findings, 5 coverage gaps, signal:HIGH
- explorer-package: 6 findings, 6 coverage gaps, signal:MED
- dx-and-examples: 6 findings, 4 coverage gaps, signal:MED
Perspectives: correctness-and-contracts, test-coverage-robustness, cli-and-runtime, explorer-package, dx-and-examples → judge. Models: grok-4.5 (HIGH), composer-2.5 (MED/LOW).
Sent by Cursor Automation: Flatbread PR Review
There was a problem hiding this comment.
Stale comment
Review verdict
REQUEST_CHANGES — consensus HIGH: three AVA suites rename the shared
dist/static/index.htmlunderconcurrency: 4, and positive paths still soft-skip witht.pass('skipped…'), so mount/open proofs can go false-green. Rule: consensus HIGH / two+ independent HIGHs.Prior openPath/assets-gate findings at
bc899adare closed in4965f2f(resolveOpenPath/ mount shareexplorerAssetsPresent). Vitedevproxy andpreplay:effortsare also addressed.Blocking (do these first)
- Isolate explorer asset hide/restore across
openPath.test.ts,explorerMount.test.ts, andliveServerEffortGraph.test.ts(temp static dir, injectable FS, or cross-file mutex).test.serialonly serializes within one file.- Hard-fail (or require CI-built assets) on positive openPath /
/eventscases — drop soft-skipt.passthat masks races.Also worth fixing
- Vite
base: './'breaks asset URLs for extensionless deep SPA routes served by the mount fallback — preferbase: '/'(or inject<base href="/">).- Explorer mount is startup-only;
--watchconfig reload does not remount/clear SPA.- Skill setup still implies explorer always lands at
/after--openwithout the assets-gate caveat.Coverage plan (ordered)
- Cross-file isolation + hard-fail positives for the three suites above.
- Co-assert
resolveOpenPath(content) === (mountExplorerIfMatched(…)?.openPath ?? '/graphql')for assets present and missing.- From deep-route HTML, fetch script
srcand expect 200 (relativebaseedge).- Extensioned unknown path is not injected SPA HTML; non-GET extensionless still
next()s.endpoints.test.ts: same-origin/graphqlwhenwindow+ http origin, no bootstrap.- Watch reload that drops the preset: remount/clear or document startup-only.
Reviewer scoreboard
Perspective Signal correctness-and-contracts HIGH — race + SPA base; prior gate closed test-coverage-robustness HIGH — race + soft-skip + fallthrough/endpoints gaps explorer-package HIGH — soft-skip + vite base cli-and-runtime MED — watch stickiness; under-weighted race dx-and-examples MED — skill/play gaps; prior gate closed Perspectives: correctness, coverage, cli-runtime, explorer-package, dx. Models: grok-4.5 (HIGH), composer-2.5 (MED/LOW).
Sent by Cursor Automation: Flatbread PR Review
| export default defineConfig({ | ||
| plugins: [react()], | ||
| root: '.', | ||
| base: './', |
There was a problem hiding this comment.
MED (consensus) — base: './' emits relative ./assets/…. HTML served for extensionless nested routes (e.g. /effort-graph/view via SPA fallback) resolves assets under the wrong prefix → 404.
Minimal fix: Use base: '/' for the Flatbread-mounted SPA, or absolutize asset URLs / inject <base href="/"> at mount time.
| }); | ||
| // Explorer SPA first so `/` is the visualizer when a preset matches. It must | ||
| // `next()` for `/events` and `/graphql` (see explorerMount). | ||
| const explorerMount = mountExplorerIfMatched(app, config.content); |
There was a problem hiding this comment.
MED — Explorer mounts once at boot. --watch config reload replaces GraphQL config but does not remount/clear the SPA, so server.explorer can stick after the preset is removed.
Minimal fix: Remount (or clear) explorer on config reload, or document mount as startup-only.
|
|
||
| ## 4. Open the explorer (optional) | ||
|
|
||
| With a complete `effortGraphContent()` preset in config, Flatbread serves the | ||
| content-relation explorer automatically: | ||
|
|
||
| ```bash | ||
| flatbread start --watch --open | ||
| ``` | ||
|
|
||
| - Explorer UI: `http://localhost:5057/` | ||
| - Apollo GraphQL sandbox: `http://localhost:5057/graphql` | ||
|
|
||
| No separate app install is required; `@flatbread/explorer` ships with |
There was a problem hiding this comment.
MED — Skill implies explorer is always at http://localhost:5057/ after flatbread start --watch --open, omitting that missing dist/static soft-fails mount and --open falls back to /graphql.
Minimal fix: One sentence that built assets are required (pnpm play:efforts or explorer build) and that --open shares the mount assets gate. Mirror in the packaged skill under packages/effort-graph/skills/.
4965f2f to
20bfa76
Compare
|
This pull request is part of a Mergify stack:
|
Revision history
|
There was a problem hiding this comment.
Stale comment
Review verdict
REQUEST_CHANGES — CLI welcome/
--openadvertise explorer from preset match alone whilemountExplorerIfMatchedsoft-fails when assets are missing, so public open-path UX drifts from the real Express mount (consensus HIGH).Prior threads closed on this HEAD: shared
dist/static/index.htmlrename race and soft-skip false greens (mount suite now fails closed). Still open: vitebase:'./'vs deep SPA routes,--watchmount stickiness, skill/docs assets caveat.Blocking / high
- Unify open-path with mountability —
resolveCliOpenPath/resolveOpenPath/ welcome must require preset anddist/static/index.html(same predicate asmountExplorerIfMatched), else/graphql.- Prove
/graphql+/eventscoexistence — mount tests claim Apollo retention but never fetch those paths; add missing-assets negative without renaming shared assets.--watchexplorer stickiness — mount is once-at-boot; remount/tear-down on config reload or document + test.Coverage plan (priority)
- Missing assets → mount returns
null(isolated temp dir).- With mount:
GET /graphqlandGET /eventsnot stolen as SPA HTML.- Preset + missing assets → open path
/graphql; welcome omits explorer lines.- Watch/
replaceConfigdocuments or reconciles explorer mount.staticDir.test.tsfail closed whenindex.htmlabsent under rootpnpm test.Reviewer scoreboard
correctness / coverage / cli-runtime / explorer-package / dx — all HIGH signal on the open-path contract; DX uniquely flagged
play:effortswithout build guard.Sent by Cursor Automation: Flatbread PR Review
| export function resolveCliOpenPath( | ||
| content: readonly ContentEntry[] | undefined | ||
| ): string { | ||
| if (content && matchExplorerPreset(content)) return EXPLORER_ENDPOINT; |
There was a problem hiding this comment.
HIGH (consensus) — resolveCliOpenPath returns / on matchExplorerPreset only. That diverges from mountExplorerIfMatched, which soft-fails when dist/static/index.html is missing. Welcome/--open then claim explorer while Express never mounted it.
Minimal fix: Share one helper with the mount layer: return / only when preset matches and assets exist; else /graphql.
| let explorer = false; | ||
| try { | ||
| const loaded = await loadFlatbreadConfig(process.cwd()); | ||
| openPath = resolveCliOpenPath(loaded.config?.content); |
There was a problem hiding this comment.
HIGH (consensus) — Welcome/--open set openPath/explorer from preset-only resolveCliOpenPath before/without knowing mount succeeded. Fresh clones that skip pnpm build hit the soft-fail path while CLI still opens / and prints explorer URLs.
Minimal fix: Derive both flags from the same mountability predicate as mountExplorerIfMatched (preset + assets).
|
|
||
| if (details.internal) { | ||
| // Always show HTTP endpoint | ||
| if (explorer) { |
There was a problem hiding this comment.
HIGH — HTTP welcome prints explorer: whenever open-path resolved to /, even after soft-fail left Apollo owning /.
Minimal fix: Print explorer lines only when assets+preset would mount; otherwise GraphQL-only banner. Same for the HTTPS block below.
| export function resolveOpenPath( | ||
| content: readonly ContentEntry[] | undefined | ||
| ): string { | ||
| if (content && matchExplorerPreset(content)) return '/'; |
There was a problem hiding this comment.
HIGH (consensus) — JSDoc says “when mounted,” but this only calls matchExplorerPreset and ignores the assets gate above. It also duplicates resolveCliOpenPath, so the two can (and do) drift.
Minimal fix: Export canMountExplorer / reuse mountability; make both resolvers and welcome consume it (or fail hard for matched presets without assets).
| async (t) => { | ||
| const staticDir = getExplorerStaticDir(); | ||
| const indexPath = join(staticDir, 'index.html'); | ||
| // Build must have produced assets; skip soft-fail would hide regressions. |
There was a problem hiding this comment.
HIGH (coverage) — Suite claims Apollo retention / SPA mount but never hits /graphql or /events, never exercises extensionless SPA fallback, and never covers missing-assets → null. Prior rename-race is fixed (t.fail instead of hiding assets); coexistence is still unproven.
Minimal fix: Stack Apollo//events handlers after mount and assert they win; add an isolated-temp missing-assets negative (do not rename shared dist/static/index.html).
| }); | ||
| // Explorer SPA first so `/` is the visualizer when a preset matches. It must | ||
| // `next()` for `/events` and `/graphql` (see explorerMount). | ||
| const explorerMount = mountExplorerIfMatched(app, config.content); |
There was a problem hiding this comment.
HIGH (consensus) — Explorer mounts once at boot. --watch replaceConfig never remounts/clears the SPA, so server.explorer can stick after the Effort Graph preset is removed (and stay false after it is added).
Minimal fix: Remount/tear-down on config reload, or document boot-sticky explorer and add a negative test that reload does not change mount.
| "typecheck": "pnpm --filter @flatbread/proof --filter @flatbread/explorer typecheck", | ||
| "play": "cd examples/nextjs && pnpm dev", | ||
| "play:efforts": "cd examples/effort-viz && pnpm dev", | ||
| "play:efforts": "pnpm exec flatbread start --watch --open", |
There was a problem hiding this comment.
HIGH (DX) — play:efforts runs flatbread start --watch --open with no build precondition. Fresh pnpm install && pnpm play:efforts hits the assets soft-fail while CLI still claims/opens explorer. CONTRIBUTING already says “after pnpm build.”
Minimal fix: preplay:efforts (pnpm build or explorer build) or inline pnpm build && ….
| export default defineConfig({ | ||
| plugins: [react()], | ||
| root: '.', | ||
| base: './', |
There was a problem hiding this comment.
MED (latent; still open) — base: './' emits relative ./assets/…. HTML served for extensionless nested routes (SPA fallback in explorerMount) resolves assets under the wrong prefix. Contested for v1 (no client router yet; / is fine).
Minimal fix: Use base: '/' for Flatbread-served same-origin, or stop advertising deep-path fallback until client routing exists.
|
|
||
| ```bash | ||
| flatbread start --watch --open | ||
| ``` |
There was a problem hiding this comment.
MED (still open) — Skill implies explorer is always at http://localhost:5057/ after flatbread start --watch --open, omitting that missing dist/static soft-fails mount and --open can still open /. Keep .agents copy in sync.
Minimal fix: Add a short monorepo-from-source pnpm build / assets caveat (mirror CONTRIBUTING).
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: Ic08896dac672cad66a12ee566014c568af870a7a
There was a problem hiding this comment.
Review verdict
REQUEST_CHANGES — consensus HIGH (≥2 perspectives) on open-path/assets mismatch, duplicate assets-unaware resolveOpenPath, sticky --watch explorer mount/flag, under-tested mount contracts, and play:efforts build gap.
Sync @ d1cff0a only restores monorepo path aliases. Prior HIGH items #1–#5 remain STILL OPEN. Rename-race soft-skip and tsconfig alias fixes stay FIXED.
Blocking themes (priority)
- Preset ≠ mount readiness —
resolveCliOpenPath/ welcome /--openkey offmatchExplorerPresetalone;mountExplorerIfMatchedsoft-fails withoutdist/static/index.html→ banner can claim explorer while Apollo owns/. - Duplicate open helpers —
resolveOpenPathJSDoc says “when mounted” but only checks preset; drifts from mount. - Mount suite gaps — no isolated missing-assets→
null, no/graphql+/eventscoexistence, no extensionless SPA fallback asserts. - Sticky explorer under
--watch— one-shot mount + frozenserver.explorer;replaceConfignever remounts/clears. play:efforts/ skills — no build gate; skill setup claims:5057/unconditionally.
Coverage plan (HIGH first)
explorerMount.test.ts— missingindex.htmlin isolated temp →null, no/handler (no shared rename).explorerMount.test.ts— after mount,/graphqlPOST +/eventsSSE still win.explorerMount.test.ts— extensionless nested path serves SPA HTML.openPath.test.ts— preset+missing assets →/graphql; assets present →/.liveServer*.test.ts—replaceConfigclear/remount flipsserver.explorer;/eventswhile explorer true; missing assets →explorer === false.
Consensus MED (still open)
vitebase: './'breaks nested SPA fallback asset URLs.- Both effort-graph skill
setup.mdcopies omit the assets/build caveat.
Reviewer scoreboard
| Reviewer | Verdict | Signal |
|---|---|---|
| correctness-and-contracts | REQUEST_CHANGES | Strong |
| test-coverage-robustness | REQUEST_CHANGES | Strong |
| cli-and-runtime | REQUEST_CHANGES | Strong |
| explorer-package | REQUEST_CHANGES | Strong |
| dx-and-examples | REQUEST_CHANGES | Solid |
Perspectives: correctness, coverage, cli-runtime, explorer-package, dx. Models: grok-4.5 (HIGH), composer-2.5 (MED/LOW).
Sent by Cursor Automation: Flatbread PR Review
| export function resolveCliOpenPath( | ||
| content: readonly ContentEntry[] | undefined | ||
| ): string { | ||
| if (content && matchExplorerPreset(content)) return EXPLORER_ENDPOINT; |
There was a problem hiding this comment.
HIGH (consensus) — resolveCliOpenPath returns / on matchExplorerPreset only. That diverges from mountExplorerIfMatched, which soft-fails when dist/static/index.html is missing. Welcome/--open then claim explorer while Express never mounted it.
Minimal fix: Share one helper with the mount layer: return / only when preset matches and assets exist; else /graphql.
| let openPath = GRAPHQL_ENDPOINT; | ||
| let explorer = false; | ||
| try { | ||
| const loaded = await loadFlatbreadConfig(process.cwd()); | ||
| openPath = resolveCliOpenPath(loaded.config?.content); |
There was a problem hiding this comment.
HIGH (consensus) — Welcome/--open set openPath/explorer from preset-only resolveCliOpenPath before/without knowing mount succeeded. Fresh clones that skip pnpm build hit the soft-fail path while CLI still opens / and prints explorer URLs.
Minimal fix: Derive both flags from the same mountability predicate as mountExplorerIfMatched (preset + assets).
| if (explorer) { | ||
| console.log( | ||
| ` ${colors.gray('explorer:')} http://${colors.bold( | ||
| `localhost:${port}${EXPLORER_ENDPOINT}` | ||
| )}` | ||
| ); | ||
| } |
There was a problem hiding this comment.
HIGH — HTTP welcome prints explorer: whenever open-path resolved to /, even after soft-fail left Apollo owning /.
Minimal fix: Print explorer lines only when assets+preset would mount; otherwise GraphQL-only banner. Same for the HTTPS block below.
| /** Open path for `--open`: explorer root when mounted, else Apollo sandbox. */ | ||
| export function resolveOpenPath( | ||
| content: readonly ContentEntry[] | undefined | ||
| ): string { | ||
| if (content && matchExplorerPreset(content)) return '/'; | ||
| return GRAPHQL_PATH; | ||
| } |
There was a problem hiding this comment.
HIGH (consensus) — JSDoc says “when mounted,” but this only calls matchExplorerPreset and ignores the assets gate above. It also duplicates resolveCliOpenPath, so the two can (and do) drift.
Minimal fix: Export canMountExplorer / reuse mountability; make both resolvers and welcome consume it (or fail hard for matched presets without assets).
| }); | ||
|
|
||
| test.serial( | ||
| 'mounts SPA at / and leaves /graphql for Apollo when assets exist', |
There was a problem hiding this comment.
HIGH (coverage) — Suite claims Apollo retention / SPA mount but never hits /graphql or /events, never exercises extensionless SPA fallback, and never covers missing-assets → null. Prior rename-race is fixed (t.fail instead of hiding assets); coexistence is still unproven.
Minimal fix: Stack Apollo//events handlers after mount and assert they win; add an isolated-temp missing-assets negative (do not rename shared dist/static/index.html).
| // Explorer SPA first so `/` is the visualizer when a preset matches. It must | ||
| // `next()` for `/events` and `/graphql` (see explorerMount). | ||
| const explorerMount = mountExplorerIfMatched(app, config.content); |
There was a problem hiding this comment.
HIGH (consensus) — Explorer mounts once at boot. --watch replaceConfig never remounts/clears the SPA, so server.explorer can stick after the Effort Graph preset is removed (and stay false after it is added).
Minimal fix: Remount/tear-down on config reload, or document boot-sticky explorer and add a negative test that reload does not change mount.
| port, | ||
| reloader, | ||
| effortGraph, | ||
| explorer: explorerMount !== null, |
There was a problem hiding this comment.
HIGH (consensus) — explorer: explorerMount !== null is freeze-dried at listen. Under --watch config swap this flag stays wrong even if routes were remounted later.
Minimal fix: Recompute/flip server.explorer when remounting on reload (or expose a live getter).
| "typecheck": "pnpm --filter @flatbread/proof --filter @flatbread/explorer typecheck", | ||
| "play": "cd examples/nextjs && pnpm dev", | ||
| "play:efforts": "cd examples/effort-viz && pnpm dev", | ||
| "play:efforts": "pnpm exec flatbread start --watch --open", |
There was a problem hiding this comment.
HIGH (DX) — play:efforts runs flatbread start --watch --open with no build precondition. Fresh pnpm install && pnpm play:efforts hits the assets soft-fail while CLI still claims/opens explorer. CONTRIBUTING already says “after pnpm build.”
Minimal fix: preplay:efforts (pnpm build or explorer build) or inline pnpm build && ….
| export default defineConfig({ | ||
| plugins: [react()], | ||
| root: '.', | ||
| base: './', |
There was a problem hiding this comment.
MED (latent; still open) — base: './' emits relative ./assets/…. HTML served for extensionless nested routes (SPA fallback in explorerMount) resolves assets under the wrong prefix. Contested for v1 (no client router yet; / is fine).
Minimal fix: Use base: '/' for Flatbread-served same-origin, or stop advertising deep-path fallback until client routing exists.


Distribute the content-relation explorer as
@flatbread/explorersoconsumers no longer need a separate Next app. When config uses
effortGraphContent(),flatbread startserves the SPA at/and--openlaunches it; Apollo stays at/graphql.Summary
@flatbread/explorer(Vite SPA + Node helpers) with a genericgraph core and an Effort Graph preset migrated from
examples/effort-vizflatbread; auto-mount on known presets with bootstrapinjection for same-origin GraphQL/SSE
flatbread start(no corunner) and static deploysvia
?endpoint=pnpm play:effortsatflatbread start --watch --open; retireexamples/effort-viz; update CONTRIBUTING and Effort Graph setup skillTest plan
pnpm --filter @flatbread/explorer test(79 tests)pnpm --filter @flatbread/explorer typecheckflatbread startserves explorer HTML at/and GraphQL at/graphqlwith bootstrap injectionCo-authored-by: Cursor cursoragent@cursor.com