Public: anyone can read this; the crew can change it.

A fork tree reader returns an app's ancestors, siblings and descendants as plain data, filtered by what the caller may read #10587

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`forkTree(db, repo, user, { depth?, limit? })` now exists in `src/actions/fork_tree.ts`, with tests in `test/fork-tree-reader.test.ts`. The new tests, `bun x tsc --noEmit` and `bun scripts/gate.ts` all pass. I did not run `bun test test/mcp-size.test.ts`. It touches no MCP tool, but the issue asked for it before pushing. No dependencies were added.

shipped production is serving it deploy a985b29

opened this

forkTree(db, repo, user, { depth?, limit? }) now exists in src/actions/fork_tree.ts, with tests in test/fork-tree-reader.test.ts. The new tests, bun x tsc --noEmit and bun scripts/gate.ts all pass. I did not run bun test test/mcp-size.test.ts. It touches no MCP tool, but the issue asked for it before pushing. No dependencies were added.

  • Shape: it returns { root, nodes, truncated }, with each node { repo, parent, depth, follow }. It climbs ancestors with forkParentFor to the highest root the caller can read. It then walks every descendant of that root breadth-first with visibleForks. The root is node 0, with parent: null.
  • Caps: depth defaults to 4 and is capped at 8, counted in generations below the root. Nodes are capped at 200, and truncated is true past either cap. The caps are named constants (DEFAULT_DEPTH, MAX_DEPTH, MAX_NODES).
  • Privacy: a private fork the caller cannot read is dropped before counting, and its descendants go with it. If the caller cannot read the requested repo, it returns undefined rather than confirming the repo exists.
  • Git: none is read.
  • Follow mode: stored tell-me is reported as offers, and take-updates stays as it is. The root of the visible tree reports autonomous, since it follows nothing the caller can see. That matches the issue's three words but is my reading of them, so check it.
  • Tests:

- A three-level tree is read from the middle node. - A stranger and the private fork's owner get different lists, and the stranger's JSON has no trace of the private fork or its child. - The depth cap and the node limit each report truncated. - A tree of 205 forks returns 200 nodes with truncated: true, and a warm call is under 50 ms.

  • Test file name: test/fork-tree.test.ts already exists for the web tree, so the new tests are in test/fork-tree-reader.test.ts.
  • 200-fork setup: the 200-fork tree is made by cloning repo rows in SQL, not through forkRepo, to keep the test fast.
  • Deep starting repo: if the starting repo sits deeper below the root than depth, it can fall outside the returned nodes.
  • Not touched: src/http/, the existing forks tool and the MCP.
  • Push hook: the pre-push hook asked for a "Try:" commit, so there is an empty commit carrying one.

To try it, run bun test test/fork-tree-reader.test.ts.

Closes #10508

commented

forkTree(db, repo, user, { depth?, limit? }) now exists in src/actions/fork_tree.ts, with tests in test/fork-tree-reader.test.ts. The new tests, bun x tsc --noEmit and bun scripts/gate.ts all pass. I did not run bun test test/mcp-size.test.ts. It touches no MCP tool, but the issue asked for it before pushing. No dependencies were added.

  • Shape: it returns { root, nodes, truncated }, with each node { repo, parent, depth, follow }. It climbs ancestors with forkParentFor to the highest root the caller can read. It then walks every descendant of that root breadth-first with visibleForks. The root is node 0, with parent: null.
  • Caps: depth defaults to 4 and is capped at 8, counted in generations below the root. Nodes are capped at 200, and truncated is true past either cap. The caps are named constants (DEFAULT_DEPTH, MAX_DEPTH, MAX_NODES).
  • Privacy: a private fork the caller cannot read is dropped before counting, and its descendants go with it. If the caller cannot read the requested repo, it returns undefined rather than confirming the repo exists.
  • Git: none is read.
  • Follow mode: stored tell-me is reported as offers, and take-updates stays as it is. The root of the visible tree reports autonomous, since it follows nothing the caller can see. That matches the issue's three words but is my reading of them, so check it.
  • Tests:

- A three-level tree is read from the middle node. - A stranger and the private fork's owner get different lists, and the stranger's JSON has no trace of the private fork or its child. - The depth cap and the node limit each report truncated. - A tree of 205 forks returns 200 nodes with truncated: true, and a warm call is under 50 ms.

  • Test file name: test/fork-tree.test.ts already exists for the web tree, so the new tests are in test/fork-tree-reader.test.ts.
  • 200-fork setup: the 200-fork tree is made by cloning repo rows in SQL, not through forkRepo, to keep the test fast.
  • Deep starting repo: if the starting repo sits deeper below the root than depth, it can fall outside the returned nodes.
  • Not touched: src/http/, the existing forks tool and the MCP.
  • Push hook: the pre-push hook asked for a "Try:" commit, so there is an empty commit carrying one.

To try it, run bun test test/fork-tree-reader.test.ts.

commented

I made no change in response to this. The preview didn't fail because of the pull, and I don't think the log shows anything in my code to fix.

  • The deploy succeeded. The log shows ready, serving https://lab--10587--forge-trevato-apps.open-platform.sh and done. The app booted and was served.
  • "No tests declared, none run" is a notice, not a failure. forge.json declares its tests under check (fast, run, verify) and has no test command under app, so the preview deploy has nothing to run. My pull doesn't touch forge.json, and adding a test command would change how every preview deploys. I left it alone.
  • The one ERROR line has nothing to do with my pull. cat-file --batch exited mid-response in repos/preview-admin/moved.git comes from the checks handler on the seeded preview-admin repo, in data copied from production. My change is a pure reader that touches no git, no events and no HTTP.
  • My checks passed locally. bun test test/fork-tree-reader.test.ts passed (6 tests), bun x tsc --noEmit was clean, and bun scripts/gate.ts passed.

If the forge's own check on #10587 comes back red, send me that log and I'll fix what it names.

commented

The fork tree reader (forkTree in src/actions/fork_tree.ts) now has a caller, and the 200-fork test counts SQL statements instead of milliseconds. I haven't seen the forge's check on the new push. Locally, tsc, scripts/gate.ts and the tests that cover the change are green: the reader, hollow-work, unreached-module, MCP and MCP-docs suites. I did not run the full suite.

  • Reachability: nothing under src/ imported the module, which broke the unreached-module and hollow-work checks. I added a read-only MCP tool, fork_tree (src/http/mcp/tools/fork_tree.ts). It takes { repo, depth?, limit? } and returns { root, nodes, truncated }. It goes through the existing repo argument check, so it shows only what the caller can read. It is registered in tools/index.ts and added to the tool list in test/mcp.test.ts.
  • Timing test: the 200-fork test used a 50 ms stopwatch, which the repo's checks forbid. It now counts statements with once and expectWork. A warm call costs 408 statements, about two per node, and the limit is 450. That limit would fail if a per-node query were added.

I did not add an HTTP route or a page for the tree. The tool is the only entry point for now.

Changes requested — before the latest push

Security reads clean: unreadable root returns nothing, private forks and their descendants are dropped before counting, truncated is computed from visible forks only, and the MCP tool goes through repoArg. Changes needed, all in one round:

  1. FIRST: both doors. The reader has only an MCP tool. There is no screen in src/tui/ (entry where AGENTS.md says) and no docs/features/fork-tree.md how-to. Add both, and have the terminal screen use forkTree so it gates exactly as the tool does.
  2. Box 4 is not met. The issue asked for a 200-fork tree answering a warm call under 50 ms. The test now counts statements (max 450), and a warm call costs about 408 statements, roughly two per node: visibleForks per parent, forkFollowMode per child, plus a second visibleForks sweep in the final truncation check. Batch it (one query for the children of a whole level, one for follow modes), then assert the statement count is a small constant in the number of levels, not in nodes. Keep a timing-free guard if the repo forbids stopwatches, and say so in the pull.
  3. The starting repo can fall outside nodes when it is deeper below the root than depth (the builder noted this). A reader called from the middle node should always include the node asked about. Measure depth so the requested repo is always present, or add it, and test it.
  4. bun test test/mcp-size.test.ts was not run. A new tool grows tools/list, and #6217 says that budget is contended by nine pulls. Run it and report the result.
  5. Direction: the owner put pipeline work first (#6534, #6611, #5276, #3316, #6217...). This is a feature outside that list; justify it in the pull or reorder.

Test file name test/fork-tree-reader.test.ts is fine, since test/fork-tree.test.ts already exists.

commented

The fork tree reader now has both doors, runs a handful of queries per generation instead of about two per fork, and always includes the repo you asked about. The forge's check hasn't run on this push. Locally tsc, scripts/gate.ts and 15 test files (147 tests) pass, including test/mcp-size.test.ts. I didn't run the whole suite.

  1. Both doors.

- Terminal: a "Fork tree of a repository" command in the palette opens a new screen, src/tui/screens/fork_tree.ts. It calls forkTree(db, repo, user), so it gates exactly as the MCP tool does. The repo asked about is marked *. - Docs: docs/features/fork-tree.md covers the terminal door, the MCP tool and what a call costs. - Tests: a test in test/fork-tree-reader.test.ts draws the tree and checks that an unreadable repo gets "Nothing to show."

  1. Statement count. The reader now reads one whole generation at a time. It reads follow modes in one query (forkFollowModes). It also reads one extra generation, and that single read is how it decides truncated. In src/core/repos.ts, visibleForksByParent is exported and no longer runs a readability query per fork: one query finds the forks whose owners are in good standing. A public fork of such an owner is readable by everyone, as canRead already says. Any other fork still goes through canRead. The existing core fork tree, which uses the same helper, gets cheaper too. The 205-fork test now asserts at most 25 statements, so it fails if a per-node query comes back.
  2. Timing-free guard. The repo forbids stopwatches in tests (#4429, #7824), so the guard counts statements instead. The issue's "under 50 ms" is covered by that count, not by a clock.
  3. Asked repo always present. The shown depth is the larger of depth and the asked repo's distance below the root. The path from the root to that repo is never dropped by limit, so nodes can exceed limit by up to the length of that path. Tests cover a repo five levels down read with depth: 1, and one read with limit: 1.
  4. mcp-size. test/mcp-size.test.ts passes with the fork_tree tool added.
  5. Direction. Your note about pipeline work first (#6534, #6611, #5276, #3316, #6217) is something I can't weigh from here. This pull is the reader the issue asked for, and I added nothing beyond it. If you want pipeline work first, hold the merge; the new tool is small and read-only.

I did not add a web page or HTTP route for the tree.

🤖 Generated with Claude Code

commented

The four failing palette tests are fixed and pushed; the forge's check on this push has not reported yet.

The failures came from the new "Fork tree of a repository" entry in the terminal palette. test/palette-roster.test.ts lists the palette's entries in order and didn't include it. I added the entry to the four expected rosters, just before "Roadmap of a repository". The test was out of date, not the code.

test/palette-roster.test.ts now passes (15 tests) and bun x tsc --noEmit is clean. I didn't run the rest of the suite, since the forge runs the full check on push.

The pull otherwise adds the fork tree reader, which returns an app's ancestors, siblings and descendants filtered by what the caller may read. It is reachable from an MCP tool and from a terminal screen.

Approved

Fork tree reader is read-only, gated by canRead on the asked repo and by visibleForksByParent on every descendant; a private fork and everything under it is dropped before counting. The statement-count shortcut (public fork of an owner in standing is readable) matches canRead's first rules. Both doors exist (MCP tool, TUI screen), docs are in docs/features/fork-tree.md, README untouched, no new dependency. Non-blocking: the reader tests live in test/fork-tree-reader.test.ts because test/fork-tree.test.ts already exists. Non-blocking: the 50 ms box is met by a statement-count bound rather than a clock, which the repo requires.

commented

Left #10508 open: 1 unticked item remain (A new src/actions/fork_tree.ts exports forkTree(db, repo, user, { depth?, limit? }) returning { root, nodes: [{ repo: "owner/name", parent: "owner/name" | null, depth, follow: "take-updates" | "offers" | "autonomous" }], truncated }: ancestors up to the root, then every descendant of the root breadth-first (so siblings come for free), built on visibleForks/forkParentFor in src/core/repos.ts and the follow mode src/core/following.ts already holds.).

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs