Sfoglia il codice sorgente

docs(steps): the in-order reading, and what validating it found

Spec §3.13.1 describes the rail as built: what it is made of (records the same
pass makes as the links), what makes the fold possible (a guard naming the
decision it belongs to), the items, and the words. `CLAUDE.md` names
`api/program.ts` and what the guard reader now returns. `CHANGELOG.md` gets the
user-facing feature and the three fixes under it.

Both plans now say what happened: the 2026-08-29 plan carries a BUILT header
with where the build differs from it (a guard's `branch`, reading a function
once per rail, blocks as one kind carrying facts, loops needing a reading of
their own), the answers to its open questions, and a §8 recording the six
endpoints read against their source — plus the two gaps left open on purpose,
a mongoose `product.save()` the effects table does not know and the nested
`const handleX = async () => …` that is still not a node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
Colby McHenry 1 settimana fa
parent
commit
22f92a828b

+ 8 - 0
CHANGELOG.md

@@ -14,6 +14,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ### New Features
 
+- **The Steps tab reads a handler in the code's own order.** A picture of what an endpoint sets in motion put the lookup, the token signing, the 200 and the 401 side by side, because each is one step from the anchor — true, and not how the code reads. Now a handler, an endpoint or any function opens as a rail instead: the calls top to bottom in the order they are written, a fork where the code forks — an `if`, a `switch`, a `try`, an early exit — with its arms side by side under the condition, an arm that answers the request, returns or throws ending right there, a helper drawn where it is called, and a body that repeats saying so (`for each item of items`). A call written inside another call's arguments comes first, so the token is signed before the reply that carries it. Work registered to run later (`later · then`) and calls started together (`together · Promise.all`) say so rather than pretending to be a sequence. A screen still opens as before — its handlers fire on events and have no order between them — and either reading is one click, or one `&view=order` / `&view=tree` in the link, away. Nothing to re-index: it is read from the source at request time, and where the conditions cannot be read the rail is a plain sequence rather than an invented structure.
+
 - **A Next.js app lands on the Screens tab like a mobile app.** App Router pages (`app/(group)/blog/[slug]/page.tsx` → `/blog/:slug`) and Pages Router pages are screens bound to the component they export; `<Link href>`, an internal `<a href>`, `router.push` / `router.replace` (`next/navigation` and `next/router`), `redirect()` / `permanentRedirect()` in a server action or a page, and the middleware's `NextResponse.redirect(new URL('/login', req.url))` are the transitions between them — each attributed back to the page it starts on with the plumbing folded and the condition on the arrow, a link written in markup drawn dashed as an inferred hop. `app/api/**/route.ts` exports (`GET`, `POST`, …) are endpoints bound to their functions, `pages/api/*` handlers are `ANY /api/…`, and a page's Steps picture fires from its load (`FIRES FROM page load · /users`), draws the data it reads, the handlers it wires, the server actions it crosses to and the pages it leads to as boundaries. A response's status written as `{ status: 201 }` is read too. Re-index after upgrading.
 
 - **Double-click a box to go there.** On the Steps tab a double-click on any step starts the picture from it — the same as the panel's *Start here* — so an endpoint the page calls, or another screen drawn as a boundary, opens as its own chapter in one gesture; on the Screens tab a double-click on a screen opens what happens from it. A boundary's panel now says it is not entered rather than that nothing leaves it.
@@ -32,6 +34,12 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ### Fixes
 
+- **A server action, or any handler written inside a wrapper, draws what it really does.** `const signIn = validatedAction(schema, async (data) => { … })` showed one call out of nine, because the constant held a reference to its schema and that counted as having a body of its own. Its picture is now whole — the lookup, the early returns, the `Promise.all`, the redirect.
+
+- **A call is no longer followed to a same-named method of its own class.** `crypto.createHash('sha256').update(…)` inside a service that happens to have an `update` method was followed into that method, so a login endpoint read as though it updated the user, extra replies and all. A method of your own class is written `this.update(…)`; a receiver that is not `this` now ends the walk instead of guessing.
+
+- **A step is ordered by the call it was actually reached at.** An endpoint whose handler is written inline at the registration (`router.post('/users/login', async (req, res) => { … })`) had every call in the handler read as part of the registration itself, which put its reply before the work that produces it.
+
 - **Express routes behind `app.use('/api', router)` are named by the path a request takes.** A router mounted at a prefix — through as many `router.use('/users', usersRouter)` levels as the app nests, by import or `require` — now names its routes `POST /api/users` instead of `POST /`, and the chained form `router.route('/:id').get(getProduct).put(protect, updateProduct)` (split across lines or not) registers one route per method. Entry points, the Steps tab and the client-to-route pairing all read the real paths.
 
 - **An Express handler written through a wrapper is the endpoint's handler.** `const authUser = asyncHandler(async (req, res) => { … })` — the `express-async-handler` idiom — now names the handler in Entry points and starts the Steps walk at it, with the database reads, the token check and the `401` row read from the arrow's body.

File diff suppressed because it is too large
+ 0 - 0
CLAUDE.md


+ 60 - 0
docs/design/codegraph-ui-design-spec.md

@@ -571,6 +571,66 @@ one is fitted to the whole stage. `Picture` (`screens-model.ts`) is the structur
 the shared machinery works over; `steps-model.ts` builds one. Pure model tests: `ui-steps-model.test.ts`; the
 endpoint against a real RN + Expo fixture: `ui-steps-api.test.ts`.
 
+#### 3.13.1 In order — the same walk as the code reads (`&view=order`)
+Rows-by-distance is the right picture for a screen, where handlers fire on events and nothing orders them. It is the
+wrong one for a handler: on proshop's `POST /api/users/login` the tree puts `User.findOne`, `jwt.sign`, `200` and `401`
+side by side — each is one step from the anchor — when the code says *look the user up, then IF the password matches
+sign a token and answer 200, ELSE answer 401*, and the signing happens INSIDE the reply that carries it. So the same
+walk has a second reading: the anchor, then its body top to bottom.
+
+```
+● POST /api/users/login · authUser        FIRES FROM POST /api/users/login
+│ User.findOne({ email })                 database · User · read · authUser
+│ user AND (await user.matchPassword(password))
+│   ┌ WHEN ───────────────────────┐  ┌ WHEN NOT ──────────┐
+│   │ via generateToken · inside res.json(…)              │
+│   │   jwt.sign({ id }, …)  auth │  │ 401  response      │
+│   │ 200  response               │  │ answers here       │
+│   │ answers here                │  └────────────────────┘
+```
+
+**What it is made of.** Every hop the walk makes is recorded where the code writes it — the step it reached (or the
+helper it folded into), the call's position and span, the branch guards, the loops, what fires it — by the SAME pass
+that makes the links, so the two readings can never hold different steps (`WireStepsPayload.program`, built by
+`src/ui-server/api/program.ts` from the records `steps.ts` keeps; `ProgramSite` is one such record). `buildProgram` is
+pure over them: no graph, no source, no control-flow graph.
+
+**What makes the fold possible** is that a guard names the DECISION it belongs to and not only its own words
+(`BranchGuard.branch` — where the branching construct starts): the `if` and the `else` of one statement carry the same
+branch with `negated` flipped, an early exit carries the branch of the `if` that returned, every case of a `switch`
+carries the branch of the switch, and two `try`/`catch` blocks in one function stay apart. Two sites are arms of ONE
+fork when they agree on the branch and disagree on the arm — which a joined condition string can never say. A guard
+also carries how the arm it is in leaves (`armExit`) and, for an early exit, how the arm that was not taken leaves
+(`exit`), so an arm ends with the word the code uses.
+
+**The items** (`WireItem`): a **step**, where the code writes it — with `inside res.json(…)` when it is written in
+another call's arguments, its own body under it when the walk entered it, and `again` when the same function has
+already been read (a function is read once in a rail, however many times it is called); a **fork** — `if` / `switch` /
+`ternary` / `try` / an early exit — carrying its condition once, with an arm per side, each ending `reply` / `return` /
+`throw` / `exit` when it stops there; a bracketed **block** — a helper drawn in place (`via generateToken`), a body that
+runs per item (`for each item of items`, read by `loopsForFile`, which loops and forks nest by which construct BEGINS
+first), work registered to run later (`later · then`), calls started together (`together · Promise.all`); and a **cut**
+where the reading stopped. Source order is execution order for straight-line code and for arguments before their call
+(so the token is signed before the reply); where it is not — a callback, concurrency — the block says so rather than
+pretending.
+
+**Honest by construction.** A fork exists only where a guard was READ: a language without rules, or a file that changed
+since the index sync, reads as a plain sequence rather than an invented structure. Every cap is announced
+(`program.truncated`).
+
+**The view.** `ui/src/lib/program-model.ts` decides the words, `StepsRail.svelte` + `RailBlock.svelte` draw them — no
+layout engine, a column of boxes with a hairline down its left and a fork as a row of arm columns. The box is
+`StepBox.svelte`, the canvas's box exactly (the canvas wraps it in handles; the rail lets it size to its words), and so
+are the click, the double-click-to-start-here and the panel. The fork's head says the decision once and its arms say
+only which side they are — **WHEN** / **WHEN NOT** — except a `switch`, whose arms each have a case to say, and a
+`try`, which says `on error` once. `StepsKey.svelte` is the key: floating over the canvas, last in the document on the
+rail, which scrolls and cannot have things sitting on it.
+
+**Which reading opens** travels in the URL (`&view=order` / `&view=tree`) and the summary offers both; without one the
+answer's own `defaultView` decides — the code's order for a handler, an endpoint or any function, the tree for a
+screen. Tests: `ui-steps-program.test.ts` (the fold, over hand-made records), `ui-program-model.test.ts` (the words),
+and one `in order` reading per framework in `ui-steps-api-servers.test.ts`.
+
 ## 4. Libraries and versions
 - Svelte 5 (≥ 5.25) + Vite (workspace `ui/`), Svelte Flow `@xyflow/svelte` ^1.6 for the Map and Flow canvases only (custom nodes/edges,
   hidden handles for port spreading, local selection state — the pattern in docker-app's `StackGraph.svelte`); `@dagrejs/dagre` only as a

+ 5 - 2
docs/plans/2026-08-28-steps-and-screens-for-apis-and-web.md

@@ -4,8 +4,11 @@
 readings it rests on (Expo + React Native app, `amniservices-mobile-app`). **Updated the same day, later
 sessions: P0–P6 are built** (see the per-item notes marked *Built*); P7 has its first agent A/B (proshop, small) and
 the medium / large rows are open.
-**Next:** `docs/plans/2026-08-29-steps-in-code-order.md` — the Steps tab reading a handler in the code's order (a rail with
-forks) instead of the tree by distance; a derivation from this walk, no new walk. Every claim about what a
+**Next: BUILT (2026-08-29).** `docs/plans/2026-08-29-steps-in-code-order.md` — the Steps tab reading a handler in the
+code's order (a rail with forks) instead of the tree by distance; a derivation from this walk, no new walk. P0–P6 are
+in (`src/ui-server/api/program.ts`, `ui/src/lib/program-model.ts`, spec §3.13.1); validating it against four real
+servers also fixed three defects in THIS walk — a hop's span read from the wrong call, a name-match the call as
+written disproves, and a value with one `references` edge going unlent the file's calls. Every claim about what a
 resolver emits *today* was verified against the source on this date — re-verify before building on it,
 the resolvers move. What was learned building it, beyond the plan: the index keeps only the LAST
 segment of a deep member call (`create` for `prisma.user.create`) and name-matches it — often to the

+ 57 - 3
docs/plans/2026-08-29-steps-in-code-order.md

@@ -1,8 +1,29 @@
 # Steps, in the code's order — plan
 
-**Status:** plan, written 2026-08-29 on `feature/steps-servers` (tip `5797d7e`), for a fresh session. Nothing
-below is built. The Steps tab as it stands (spec §3.13, `src/ui-server/api/steps.ts`, `ui/src/views/StepsView.svelte`)
-is the base; everything here is a second *reading* of the same walk, not a new walk.
+**Status: BUILT** 2026-08-29 on `feature/steps-servers`, P0–P6. Written the same day (at tip `5797d7e`) as a plan for a
+fresh session; what follows is that plan, kept as written, with the notes below on where the build differs from it.
+The reading is `&view=order` on the Steps tab, `src/ui-server/api/program.ts` + `ui/src/lib/program-model.ts`, and
+spec §3.13.1 is the description of what was built.
+
+**Where the build differs from the plan:**
+- **A guard carries the decision it belongs to** (`BranchGuard.branch`), not just its text and line — §4.1's
+  "same `line`, same `text`" pairing does not tell one `switch` case from another, nor two `try`/`catch` blocks apart.
+  It also carries how the arm it is in leaves (`armExit`) and, for an early exit, how the arm not taken leaves (`exit`),
+  which is where `WireArm.ends` comes from.
+- **A function is read ONCE per rail** (`again` on the item), not redrawn at every call: expanding per path turned an
+  87-step screen into 3,849 items and 618 KB. Once-only is 476 items and 94 KB (+3% wall clock on that picture).
+- **A step the walk entered reads on into its own body** under its box, so the rail holds the same steps the tree does
+  (§4.1.7 only said this for boundaries under `through`).
+- **`WireItem`'s blocks are one kind with a discriminator** (`block: 'inline' | 'loop' | 'later' | 'together'`) rather
+  than four item kinds, and they carry facts (`by`, `via`, `loop`) rather than words — the viewer says them.
+- **Loops needed their own reading** (`loopsForFile`), and loops and forks nest by which construct BEGINS first, since
+  neither reading knows about the other.
+- The open questions of §7 were answered: order for functions/endpoints and the tree for screens (1); read each
+  function once rather than capping the fold depth (2); a loop's body once, marked (3); the fork carries its condition
+  and the arms say WHEN / WHEN NOT (4).
+
+The Steps tab as it stood (spec §3.13, `src/ui-server/api/steps.ts`, `ui/src/views/StepsView.svelte`) is the base;
+everything here is a second *reading* of the same walk, not a new walk.
 
 **The ask, in the maintainer's words:** on proshop's `POST /api/users/login` the picture draws `User.findOne · jwt.sign ·
 200 · 401` in one row under the handler. That is true — all four are one step from `authUser` — and it is not what a
@@ -272,3 +293,36 @@ early exit, and the tree mode is byte-for-byte the picture it is today.
 3. **Loops.** Body once with `for each …`, or unrolled never. Proposal: once, marked.
 4. **Should the rail replace the pills' "→ …x" placement rule?** The rail's forks carry the condition once, on the
    fork; the arm's `WHEN` / `WHEN NOT` is the pill. The scenario rows in the panel stay as they are.
+
+---
+
+## 8. What the validation pictures found (2026-08-29, P5)
+
+Read against the source, endpoint by endpoint. Every reading below is the one the rail draws today.
+
+| Repo | Anchor | Reads as | Verdict |
+|---|---|---|---|
+| `bradtraversy/proshop_mern` | `POST /api/users/login` | `User.findOne` · fork on the password check · [`via generateToken` → `jwt.sign`, then `200`] \| [`401`], both arms answering | §1 exactly |
+| " | `POST /api/products/:id/reviews` | `Product.findById` · fork on `product` · [fork on `alreadyReviewed` → `400` \| `201`] \| [`404`] | three outcomes, right |
+| `gothinkster/node-express-realworld-example-app` | `POST /users/login` | `via login` → two `422` guards, `prisma.user.findUnique`, `if user` → `bcrypt.compare` → `if match` → `via generateToken`, then `403`; then the handler's own `200` | right, after the span fix |
+| `brocoders/nestjs-boilerplate` | `POST /auth/email/login` | `via validateLogin` → `findByEmail`, `!user` → `422`, two throwing guards, `bcrypt.compare`, then `sessionRepository.create` and `via getTokensData` → **`together Promise.all`** of two `jwtService.signAsync` | right, after dropping the `update` name-match |
+| `leerob/next-saas-starter` | `signIn` (server action) | drizzle `select`, `length === 0` → return, `!isPasswordValid` → return, **`together Promise.all`** of `setSession` (→ `signToken` → `SignJWT`) and `logActivity` (→ `db.insert`), then `redirectTo === 'checkout'` → the whole checkout session \| `/dashboard` | right, after the lending fix |
+| `fastapi/full-stack-fastapi-template` | `POST /login/access-token` | `via authenticate` → `session.exec`, `not db_user` → return, `not verified` → return, `updated_password_hash` → `session.add`/`commit`/`refresh`; then `not user` → `400`, `elif not user.is_active` → `400`; then `via create_access_token` → `jwt.encode` | right, after the `elif` fix |
+| `amniservices-mobile-app` | `/capture/review` | unchanged: 87 steps, 160 links, the tree, `defaultView: 'tree'` | the regression check |
+
+**Three defects the pictures caught, all in the walk and both readings** — a hop's span taken from a call that was not
+the one asked for (an inline Express handler's edges carry the route's line, so the registration's span swallowed the
+body); a name-match the call as written disproves (`crypto.createHash('sha256').update(…)` followed into the caller's
+own `AuthService.update`); and a value lent nothing because one plain `references` edge counted as a body of its own
+(`const signIn = validatedAction(schema, async (data) => { … })` drew one call out of nine). Plus an `elif` whose body
+raises being read as ending the arm it is written in.
+
+**Left open, deliberately:**
+- **A mongoose document's `product.save()` is not in the effects table** (`api/effects.ts`), so proshop's review
+  endpoint draws its `201` but not the write before it. A JS rule for `<lowercase receiver>.save` would catch it and
+  would also catch `canvas.save()` / `ctx.save()` / `sharp(...).toFile`-adjacent idioms in any web app — a call for
+  the maintainer, not a silent widening.
+- **A nested `const handleX = async () => …` is still not a node** (the plan's §6): its sites belong to the enclosing
+  component. Fixing it is an extractor change with a Rust kernel twin.
+- The **`via` name of an inlined helper is its bare name** (`via create`), which is ambiguous when two classes have a
+  `create`. The panel disambiguates; the rail could say the class.

Some files were not shown because too many files changed in this diff