← Prototype · 技能图谱

Logic Prototype

逻辑原型

A tiny interactive terminal app that lets the user drive a state model by hand. Use this when the question is about business logic, state transitions, or data shape — the kind of thing that looks reasonable on paper but only feels wrong once you push it through real cases.

一个小型交互式终端应用,让用户亲手驱动状态模型。当问题涉及业务逻辑、状态转换或数据结构时使用——那些在纸上看起来合理,只有通过真实案例推动才会觉得不对劲的事情。

When this is the right shape

何时适用

  • "I'm not sure if this state machine handles the edge case where X then Y."
  • "Does this data model actually let me represent the case where…"
  • "I want to feel out what the API should look like before writing it."
  • Anything where the user wants to press buttons and watch state change.
  • "我不确定这个状态机能否处理 X 然后 Y 的边缘情况。"
  • "这个数据模型真的能让我表达……的情况吗?"
  • "我想在编写之前先感受一下 API 应该长什么样。"
  • 任何用户想按按钮并观察状态变化的场景。

If the question is "what should this look like" — wrong branch. Use UI.md.

如果问题是"这应该长什么样"——选错分支了。使用 UI.md

Process

流程

1. State the question

1. 陈述问题

Before writing code, write down what state model and what question you're prototyping. One paragraph, in the prototype's README or a comment at the top of the file. A logic prototype that answers the wrong question is pure waste — make the question explicit so it can be checked later, whether the user is watching now or returning to it AFK.

在写代码之前,写下你要原型化的状态模型和问题。一段话,放在原型的 README 或文件顶部的注释中。回答错误问题的逻辑原型纯粹是浪费——把问题明确写出来,这样以后可以检查,无论用户现在在看,还是离开后回来查看。

2. Pick the language

2. 选择语言

Use whatever the host project uses. If the project has no obvious runtime (e.g. a docs repo), ask.

使用宿主项目所用的语言。如果项目没有明显的运行时(如文档仓库),询问。

Match the project's existing conventions for tooling — don't add a new package manager or runtime just for the prototype.

匹配项目已有的工具链约定——不要仅为原型引入新的包管理器或运行时。

3. Isolate the logic in a portable module

3. 将逻辑隔离到可移植模块中

Put the actual logic — the bit that's answering the question — behind a small, pure interface that could be lifted out and dropped into the real codebase later. The TUI around it is throwaway; the logic module shouldn't be.

将实际逻辑——回答问题的部分——放在一个小的纯接口后面,以后可以提取出来放到真实的代码库中。周围的 TUI 是可抛弃的;逻辑模块不应该是。

The right shape depends on the question:

正确的形态取决于问题:

  • A pure reducer(state, action) => state. Good when actions are discrete events and state is a single value.
  • A state machine — explicit states and transitions. Good when "which actions are even legal right now" is part of the question.
  • A small set of pure functions over a plain data type. Good when there's no implicit current state — just transformations.
  • A class or module with a clear method surface when the logic genuinely owns ongoing internal state.
  • 纯 reducer(state, action) => state。适用于操作是离散事件且状态是单一值的情况。
  • 状态机——显式状态和转换。适用于"当前哪些操作是合法的"本身就是问题一部分的情况。
  • 一组纯函数——操作普通数据类型。适用于没有隐式当前状态——只有转换的情况。
  • 具有清晰方法面的类或模块——当逻辑真正拥有持续的内部状态时。

Pick whichever shape best fits the question being asked, not whichever is easiest to wire to a TUI. Keep it pure: no I/O, no terminal code, no console.log for control flow. The TUI imports it and calls into it; nothing flows the other direction.

选择最适合问题的形态,而不是最容易连接到 TUI 的。保持纯粹:无 I/O,无终端代码,不用 console.log 做控制流。TUI 导入它并调用它;没有反向流动。

This is what makes the prototype useful past its own lifetime: when the question's been answered, the validated reducer / machine / function set can be lifted into the real module on its own.

这正是让原型在其自身生命周期之外仍然有用的原因:当问题得到回答后,经过验证的 reducer / 状态机 / 函数集可以独立提升到真实模块中。

4. Build the smallest TUI that exposes the state

4. 构建展示状态的最小 TUI

Build it as a lightweight TUI — on every tick, clear the screen (console.clear() / print("\033[2J\033[H") / equivalent) and re-render the whole frame. The user should always see one stable view, not an ever-growing scrollback.

轻量级 TUI 的方式构建——每次 tick 时清屏(console.clear() / print("\033[2J\033[H") / 等效方式)并重新渲染整个画面。用户应该始终看到一个稳定的视图,而不是不断增长的滚动回退。

Each frame has two parts, in this order:

每帧按顺序包含两部分:

  1. Current state, pretty-printed and diff-friendly (one field per line, or formatted JSON). Use bold for field names or section headers and dim for less important context (timestamps, IDs, derived values). Native ANSI escape codes are fine — \x1b[1m bold, \x1b[2m dim, \x1b[0m reset. No need to pull in a styling library unless one is already in the project.
  2. Keyboard shortcuts, listed at the bottom: [a] add user [d] delete user [t] tick clock [q] quit. Bold the key, dim the description, or vice-versa — whatever reads cleanly.
  1. 当前状态,美观打印且便于比较(每行一个字段,或格式化的 JSON)。字段名或章节标题用粗体,次要上下文(时间戳、ID、派生值)用暗淡。原生 ANSI 转义码就很好——\x1b[1m 粗体、\x1b[2m 暗淡、\x1b[0m 重置。除非项目中已有,否则无需引入样式库。
  2. 键盘快捷键,列在底部:[a] add user [d] delete user [t] tick clock [q] quit。键用粗体、描述用暗淡,或反之——只要清晰可读。

Behaviour:

行为:

  1. Initialise state — a single in-memory object/struct. Render the first frame on start.
  2. Read one keystroke (or one line) at a time, dispatch to a handler that mutates state.
  3. Re-render the full frame after every action — don't append, replace.
  4. Loop until quit.
  1. 初始化状态——一个内存中的对象/结构。启动时渲染第一帧。
  2. 每次读取一个按键(或一行),分发到修改状态的处理函数。
  3. 重新渲染每次操作后的完整画面——不要追加,要替换。
  4. 循环直到退出。

The whole frame should fit on one screen.

整个画面应适合一个屏幕。

5. Make it runnable in one command

5. 一条命令可运行

Add a script to the project's existing task runner (package.json scripts, Makefile, justfile, pyproject.toml). The user should run pnpm run <prototype-name> or equivalent — never need to remember a path.

在项目已有的任务运行器中添加脚本(package.json scripts、Makefilejustfilepyproject.toml)。用户应该运行 pnpm run <prototype-name> 或等效命令——永远不需要记住路径。

If the host project has no task runner, just put the command at the top of the prototype's README.

如果宿主项目没有任务运行器,只需把命令放在原型 README 顶部。

6. Hand it over

6. 交出去

Give the user the run command. They'll drive it themselves; the interesting moments are when they say "wait, that shouldn't be possible" or "huh, I assumed X would be different" — those are the bugs in the idea, which is the whole point. If they want new actions added, add them. Prototypes evolve.

把运行命令交给用户。他们会自己操作;有趣的是当他们说"等等,那不应该可能"或"呃,我原以为 X 会不一样"的时候——那些是想法中的 bug,而这正是全部意义所在。如果他们想要添加新的操作,就添加。原型会进化。

7. Capture the answer and the prototype

7. 记录答案与原型

Once the prototype has answered its question, capture the answer, then capture the prototype the way the [SKILL](SKILL.html) describes. The logic-specific mapping: the validated reducer / machine / function set lifts into the real module (the decision, absorbed); the TUI shell rides along to the throwaway branch that keeps the prototype as a primary source.

一旦原型回答了它的问题,先捕获答案,然后按 [SKILL](SKILL.html) 所述方式捕获原型。逻辑特有的映射是:经过验证的 reducer / 状态机 / 函数集提升到真实模块中(决策被吸收);TUI 外壳随行进入保留原型作为一手来源的一次性分支。

Anti-patterns

反模式

  • Don't add tests. A prototype that needs tests is no longer a prototype.
  • Don't wire it to the real database. Use an in-memory store unless the question is specifically about persistence.
  • Don't generalise. No "what if we wanted to support X later." The prototype answers one question.
  • Don't blur the logic and the TUI together. If the reducer / state machine references console.log, prompts, or terminal escape codes, it's no longer portable. Keep the TUI as a thin shell over a pure module.
  • Don't ship the TUI shell into production. The shell is optimised for being driven by hand from a terminal. The logic module behind it is the bit worth keeping.
  • 不要添加测试。 需要测试的原型不再是原型。
  • 不要连接到真实数据库。 使用内存存储,除非问题特别关于持久化。
  • 不要泛化。 不要说"如果我们以后想支持 X 呢"。原型只回答一个问题。
  • 不要将逻辑和 TUI 混在一起。 如果 reducer / 状态机引用了 console.log、提示或终端转义码,它就不再可移植了。保持 TUI 作为纯模块上的薄壳。
  • 不要将 TUI 外壳发布到生产环境。 外壳是为从终端手动操作而优化的。它背后的逻辑模块才是值得保留的部分。