| name | flow-scetch |
| description | Use it when user ask describe the flow or write pseudocode.
|
spec — flow
TS pseudocode is the canonical way to describe what a TODO must do.
Purpose: surface corner cases and decisions — not implementation.
Shape
- Wrap the component in a
namespace named after it (the unit being changed).
- Every flow's entrypoint is
function flow(...). Sub-steps are helper functions in the same namespace.
- Types declare boundaries; bodies show branches, guards, side effects.
namespace Auth {
type RefreshReq = { token: string }
type Pair = { access: string; refresh: string }
function flow(req: RefreshReq): Pair | 401 | 409 {
const s = redis.get(`auth:${req.token}`)
if (!s) return 401
if (s.expiresAt < now()) return 401
if (s.rotating) return 409
const pair = mint(s.userId)
redis.del(`auth:${req.token}`)
redis.set(`auth:${pair.refresh}`, s, TTL)
return pair
}
function mint(userId: string): Pair { return { access: "", refresh: "" } }
}
Rules
- Real TS, fake bodies. Must parse; bodies may be
/* ... */.
- One
namespace per TODO, one flow(...) entrypoint, ≤ 40 lines.
- Named types, no
any, no magic strings — use unions.
- Every side effect visible (
redis.*, db.*, emit, log, fs.*).
- Every error path explicit —
return <sentinel> or throw. Each distinct failure cause gets its own arm; don't collapse them. If two causes must look identical to the caller (e.g. unknown-token vs wrong-password, to avoid enumeration or a timing leak), say so in a trailing comment — that sameness is a decision, not an accident.
- Every branch terminal — no silent fall-through.
- No imports, no real paths in the snippet (paths go in the TODO's Files section).
- Decisions surface as branches + a
// see spec.md → Decisions: <name> anchor. If a decision is hidden inside /* ... */, lift it out.
- Follow the data.
flow reads as one value's lifecycle — born from the input, guarded, transformed, returned. Order the body by what happens to that value, not by which helper is convenient to call next.
- Open with a
// trace: one-liner. The first line of flow is a one-sentence trace of the whole path. If it can't be written in one sentence, the flow is too big — split the TODO. The trace must match the TODO's Outcome.
Variants
State machine — flow is the transition function:
namespace Job {
type State = "Pending" | "Active" | "Suspended" | "Done"
type Event = "activate" | "suspend" | "complete"
function flow(s: State, e: Event): State {
if (s === "Pending" && e === "activate") { assert(ready()); return "Active" }
if (s === "Active" && e === "suspend") { assert(inFlight() === 0); return "Suspended" }
if (s === "Active" && e === "complete") { emit("done"); return "Done" }
throw new Error(`invalid: ${s} + ${e}`)
}
}
Component (interface + wiring) — flow is the constructor / wire-up:
namespace SessionRepo {
interface Repo<T> { get(id: string): T | null; set(id: string, v: T): void }
function flow(client: RedisClient): Repo<Session> {
return {
get: (id) => { return null },
set: (id, v) => { },
}
}
}
Data shape change — flow is omitted; show before/after types only:
namespace Job {
type _Job = { id: string; status: string }
type Job = { id: string; status: "queued" | "running" | "done" | "failed"; retries: number }
}
Anti-patterns
- Prose inside the code block
- Hidden algorithm in
/* ... */ (lift to spec.md Decisions)