@olierjs/fugue-cli 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +52 -2
  2. package/THIRD_PARTY_NOTICES +52 -1
  3. package/dist/darwin-arm64/fugue-cli +0 -0
  4. package/dist/darwin-x64/fugue-cli +0 -0
  5. package/dist/linux-arm64/fugue-cli +0 -0
  6. package/dist/linux-x64/fugue-cli +0 -0
  7. package/dist/win32-arm64/fugue-cli.exe +0 -0
  8. package/dist/win32-x64/fugue-cli.exe +0 -0
  9. package/package.json +2 -1
  10. package/skills/fugue-show-me/SKILL.md +16 -0
  11. package/skills/show-me/SKILL.md +128 -0
  12. package/skills/show-me/agents/openai.yaml +2 -0
  13. package/skills/statechart/SKILL.md +36 -0
  14. package/skills/statechart/scripts/bug-states.mjs +81 -0
  15. package/skills/statechart/scripts/chart-edges.mjs +68 -0
  16. package/skills/statechart/scripts/chart-parts.mjs +21 -0
  17. package/skills/statechart/scripts/chart-to-tla.mjs +205 -0
  18. package/skills/statechart/scripts/compare-charts.mjs +74 -0
  19. package/skills/statechart/scripts/lib.mjs +970 -0
  20. package/skills/statechart/scripts/map-view.mjs +92 -0
  21. package/skills/statechart/scripts/property-loop.mjs +60 -0
  22. package/skills/statechart/scripts/read-check.mjs +79 -0
  23. package/skills/statechart/scripts/regions-chart.mjs +307 -0
  24. package/skills/statechart/scripts/render.mjs +531 -0
  25. package/skills/statechart/scripts/rule-book.mjs +84 -0
  26. package/skills/statechart/scripts/second-replay.mjs +122 -0
  27. package/skills/statechart/scripts/snapshot.mjs +74 -0
  28. package/skills/statechart/scripts/spec-text.mjs +126 -0
  29. package/skills/statechart/scripts/statechart-help.mjs +350 -0
  30. package/skills/statechart/scripts/statechart.mjs +903 -0
  31. package/skills/statechart/scripts/test/band.mmd +15 -0
  32. package/skills/statechart/scripts/test/regions.mmd +62 -0
  33. package/skills/statechart/scripts/test/statechart.test.mjs +6854 -0
  34. package/skills/statechart/scripts/test/sync.mmd +19 -0
  35. package/skills/statechart/scripts/test/upload.mmd +16 -0
  36. package/skills/statechart/scripts/tlc-to-chart-args.mjs +52 -0
  37. package/skills/statechart/scripts/tlc-to-chart.mjs +802 -0
  38. package/skills/statechart/scripts/tlc-tools.mjs +69 -0
  39. package/skills/statechart/scripts/ways-in.mjs +69 -0
  40. package/upstream-skills.json +18 -0
package/README.md CHANGED
@@ -13,7 +13,9 @@ fugue-cli guide
13
13
 
14
14
  后续更新执行同一条安装命令。包内提供 macOS、Linux、Windows 的 x64 和 arm64 二进制。
15
15
 
16
- 安装脚本自动将包内 Skill 同步到 `~/.agents/skills`,包括薄入口 `fugue-stack` 和完整的 `plan-with-remote`。无需手动复制目录。替换同名旧 Skill 或清理本工具管理的过时 Skill 前,会先保存备份;其他 Skill 保持原样。备份放在 `~/.local/share/fugue-cli/backups`,位于技能发现目录之外。
16
+ 使用 nvm 管理 Node 时,先在当前终端执行 `nvm use 22.19.0` 或选择更高版本,再安装和运行。启动 Agent 的环境也需要能通过 `PATH` 找到 `fugue-cli`。
17
+
18
+ 安装脚本自动将包内 Skill 同步到 `~/.agents/skills`,包括薄入口 `fugue-stack`、远端规划 `plan-with-remote`、原版 `show-me`、原版 `statechart` 和可视化解释适配 `fugue-show-me`。无需手动复制目录。替换同名旧 Skill 或清理本工具管理的过时 Skill 前,会先保存备份;其他 Skill 保持原样。备份放在 `~/.local/share/fugue-cli/backups`,位于技能发现目录之外。
17
19
 
18
20
  自动同步通过 npm 的 `postinstall` 执行,需要允许安装脚本运行。清理发生在安装或升级时,针对新版已移除的受管理 Skill;`npm uninstall` 没有对应的自动清理钩子。
19
21
 
@@ -27,24 +29,72 @@ fugue-cli --help
27
29
  fugue-cli --version
28
30
  ```
29
31
 
32
+ 在目标项目中启动支持 Skill 的 Agent,描述要完成的工作,例如:
33
+
34
+ ```text
35
+ $fugue-stack 根据这份已确认需求实现功能,完成后审查差异并验证。
36
+ $fugue-stack 审查当前代码变更,列出有实际影响的问题。
37
+ $fugue-show-me 用最小的结构图解释这次改动。
38
+ $fugue-show-me 检查取消与重试交错时的状态变化,画出可能的缺陷路径。
39
+ ```
40
+
41
+ `fugue-cli guide` 输出工作指南,编码、审查和解释由当前 Agent 按指南执行。若安装后暂时看不到新 Skill,可重新启动 Agent 后再选择入口。
42
+
30
43
  需求理解、规划和需求或方案解释继续由 `plan-with-remote` 及远端规划服务负责。指南提供规划技能的绝对路径;该技能的客户端需要 Git、已配置的规划服务,以及已在服务中登记的目标仓库,配置说明见 [configuration.md](skills/plan-with-remote/references/configuration.md)。
31
44
 
32
45
  编码、代码审查与验证、面向人的实现解释由当前 Agent 按指南完成。读取指南本身不会发起规划、修改项目或调用模型。远端规划返回的正式文档继续按 Markdown 原文保存,批准版本由规划客户端和服务端管理。
33
46
 
47
+ ## 上游技能
48
+
49
+ `show-me` 完整目录原样引入自 [humanlayer/skills](https://github.com/humanlayer/skills/tree/653b6411c1f70c275a18e37673b042ff99f67ceb/plugins/show-me/skills/show-me),保留原始说明和 Agent 调用配置。来源、固定提交和目录记录在 [upstream-skills.json](upstream-skills.json),MIT 许可保留在 [THIRD_PARTY_NOTICES](THIRD_PARTY_NOTICES)。
50
+
51
+ `statechart` 完整目录原样引入自 [cursor/plugins 的 pstack](https://github.com/cursor/plugins/tree/99b609a72323b70204eab407c622f436e858e112/pstack/skills/statechart),包括原始说明、脚本和测试。同样在来源记录中固定提交,并保留 MIT 许可。
52
+
53
+ 需要可视化解释时,总览路由到 [fugue-show-me](skills/fugue-show-me/SKILL.md):由 `show-me` 选择通用表达形式,需要分析状态变化、检查行为规则或展示缺陷路径时配合 `statechart`。适配维护在独立目录中;更新上游时整体替换原版目录并更新来源记录。
54
+
55
+ ## 状态图依赖
56
+
57
+ `statechart` 使用 Node.js 和 npm;模型检查还需要 [mise](https://mise.jdx.dev/getting-started.html)。macOS 可先安装:
58
+
59
+ ```sh
60
+ brew install mise
61
+ node ~/.agents/skills/statechart/scripts/statechart.mjs --help
62
+ ```
63
+
64
+ 首次模型检查由原版脚本通过 mise 安装 Java Temurin 25 和 TLC 1.7.4,需要联网下载约 305 MB。若使用已有环境,可按上游帮助设置 `JAVA` 和 `TLA2TOOLS`。首次 PNG 出图会将 `pele@1.8.0` 和 `@resvg/resvg-js@2.6.2` 安装到用户缓存目录;仅渲染现成 Mermaid 图时不运行 TLC。
65
+
66
+ 该版 TLC 无法解析模型字符串中的中文。模型使用 ASCII 标识符和字符串值,中文标签放在映射文件中,从模型生成中文状态图。
67
+
68
+ 输入和产物放在目标仓库之外的临时目录,按上游要求交付 PNG、Mermaid 源码、缺陷说明和再生成命令。模型检查结果与真实代码复现结果分别说明。
69
+
34
70
  ## 开发与发布
35
71
 
36
72
  开发与打包需要 Go >=1.23 和 Node >=22.19.0。
37
73
 
74
+ 验证尚未发布到 npm 的仓库版本时,在仓库根目录打包,再安装生成的本地包:
75
+
76
+ ```sh
77
+ npm pack
78
+ npm install -g ./olierjs-fugue-cli-0.1.2.tgz
79
+ fugue-cli --version
80
+ fugue-cli guide
81
+ ```
82
+
83
+ 本地包安装同样会运行 Skill 自动同步。`@latest` 使用 npm 上的已发布版本,本地包使用本次构建的仓库版本。
84
+
38
85
  ```sh
39
86
  go test ./...
40
87
  node --test tests/*.test.mjs
88
+ node --test skills/statechart/scripts/test/statechart.test.mjs
41
89
  npm pack
42
90
  ```
43
91
 
92
+ 上游测试包含 Linux 的 `LD_PRELOAD` 故障注入,并对临时路径和英文 Java 诊断文本作精确断言;macOS 上需单独核对这些环境差异。
93
+
44
94
  `npm pack` 的 `prepack` 自动构建各平台二进制,并将版本写入命令。发布目标固定为 npmjs,包名为 `@olierjs/fugue-cli`。发布验收后的包:
45
95
 
46
96
  ```sh
47
- npm publish ./olierjs-fugue-cli-0.1.0.tgz --access public
97
+ npm publish ./olierjs-fugue-cli-0.1.2.tgz --access public
48
98
  ```
49
99
 
50
100
  Go 命令入口位于 [cmd/fugue-cli](cmd/fugue-cli/main.go),指南源码位于 [guide.md](internal/cli/guide.md),入口技能位于 [SKILL.md](skills/fugue-stack/SKILL.md)。
@@ -1,4 +1,4 @@
1
- Third-party dependencies included in the compiled fugue-cli binaries.
1
+ Third-party components distributed with fugue-cli.
2
2
 
3
3
 
4
4
  github.com/spf13/cobra v1.10.2
@@ -414,3 +414,54 @@ github.com/inconshreveable/mousetrap v1.1.0
414
414
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
415
415
  See the License for the specific language governing permissions and
416
416
  limitations under the License.
417
+
418
+ humanlayer/skills: show-me (653b6411c1f70c275a18e37673b042ff99f67ceb)
419
+ https://github.com/humanlayer/skills/tree/653b6411c1f70c275a18e37673b042ff99f67ceb/plugins/show-me/skills/show-me
420
+
421
+ MIT License
422
+
423
+ Copyright (c) 2026 HumanLayer
424
+
425
+ Permission is hereby granted, free of charge, to any person obtaining a copy
426
+ of this software and associated documentation files (the "Software"), to deal
427
+ in the Software without restriction, including without limitation the rights
428
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
429
+ copies of the Software, and to permit persons to whom the Software is
430
+ furnished to do so, subject to the following conditions:
431
+
432
+ The above copyright notice and this permission notice shall be included in all
433
+ copies or substantial portions of the Software.
434
+
435
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
436
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
437
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
438
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
439
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
440
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
441
+ SOFTWARE.
442
+
443
+
444
+ cursor/plugins: pstack statechart (99b609a72323b70204eab407c622f436e858e112)
445
+ https://github.com/cursor/plugins/tree/99b609a72323b70204eab407c622f436e858e112/pstack/skills/statechart
446
+
447
+ MIT License
448
+
449
+ Copyright (c) 2026 Lauren Tan
450
+
451
+ Permission is hereby granted, free of charge, to any person obtaining a copy
452
+ of this software and associated documentation files (the "Software"), to deal
453
+ in the Software without restriction, including without limitation the rights
454
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
455
+ copies of the Software, and to permit persons to whom the Software is
456
+ furnished to do so, subject to the following conditions:
457
+
458
+ The above copyright notice and this permission notice shall be included in all
459
+ copies or substantial portions of the Software.
460
+
461
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
462
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
463
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
464
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
465
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
466
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
467
+ SOFTWARE.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olierjs/fugue-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Fugue 需求交付工作指南与 Agent 技能",
5
5
  "type": "module",
6
6
  "bin": { "fugue-cli": "bin/fugue-cli.mjs" },
@@ -10,6 +10,7 @@
10
10
  "scripts/postinstall.mjs",
11
11
  "scripts/skills.mjs",
12
12
  "skills/",
13
+ "upstream-skills.json",
13
14
  "THIRD_PARTY_NOTICES"
14
15
  ],
15
16
  "engines": { "node": ">=22.19.0" },
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: fugue-show-me
3
+ description: 使用 show-me 和 statechart 可视化解释当前实现、代码变更、运行结果或状态变化;当用户需要图解、结构示意或缺陷路径时使用。
4
+ ---
5
+
6
+ # 可视化解释
7
+
8
+ 先阅读并遵循[上游 show-me](../show-me/SKILL.md),由它指导表达形式的选择。
9
+
10
+ 需要分析状态转换、并发、重试、取消、恢复或缺陷路径,并检查行为规则、对比修复时,阅读并遵循[上游 statechart](../statechart/SKILL.md)的建模、模型检查、出图与交付要求。用 `show-me` 解释整体结构,用 `statechart` 展示状态变化和缺陷路径,按当前问题需要组合。
11
+
12
+ 原版脚本安装的 TLC 使用 ASCII 模型标识符和字符串值,中文状态与步骤标签写在映射文件中,再从模型生成中文图示。
13
+
14
+ 说明使用用户的语言,依据当前源码、差异或已有材料,只展示本次问题需要的内容。
15
+
16
+ 将上游的 `Bash(open ...)` 对应为当前宿主可用的终端或文件预览工具,按实际操作系统打开 HTML 或图片。交付本地文件时提供绝对路径链接;仅在实际打开成功后说明已打开。
@@ -0,0 +1,128 @@
1
+ ---
2
+ name: show-me
3
+ description: Help the user understand the current topic visually with concise diagrams, code-shape sketches, and focused HTML artifacts.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ Help the user understand the current topic of conversation visually. Skip the preamble and keep prose brief. Pick the smallest view that makes the key point clear.
8
+
9
+ - Show logic or an algorithm as pseudocode:
10
+
11
+ ```text
12
+ on(save)
13
+ if content is unchanged
14
+ return cached result
15
+ write new content
16
+ return fresh result
17
+ ```
18
+
19
+ - Show runtime control flow as a call tree:
20
+
21
+ ```text
22
+ submitForm
23
+ createSession
24
+ persistPrompt
25
+ launchAgent
26
+ navigateToSession
27
+ ```
28
+
29
+ - Show UI structure as a component tree, including state and module boundaries that matter:
30
+
31
+ ```tsx
32
+ <SessionPage> (apps/example/src/routes/session.tsx)
33
+ useSessionEvents()
34
+ <SessionToolbar>
35
+ <RunSkillButton> (packages/ui)
36
+ ```
37
+
38
+ - Show file responsibility or a broad refactor as a shallow file tree:
39
+
40
+ ```text
41
+ src/
42
+ ├── commands/ # parses user actions
43
+ ├── sessions/ # owns session state
44
+ └── transport/ # sends API requests
45
+ ```
46
+
47
+ - Show component interaction, control flow, or data flow with Mermaid:
48
+
49
+ ```mermaid
50
+ sequenceDiagram
51
+ participant User
52
+ participant UI
53
+ participant Daemon
54
+ User->>UI: choose command
55
+ UI->>Daemon: send expanded prompt
56
+ Daemon-->>UI: stream result
57
+ ```
58
+
59
+ - Use `diff` when the point is what changes and the surrounding shape already exists. Match the diff shape to the topic.
60
+
61
+ For a component change:
62
+
63
+ ```diff
64
+ <SessionPage>
65
+ useSessionEvents()
66
+ <SessionToolbar>
67
+ + <RunSkillButton />
68
+ <SessionTimeline>
69
+ + <SkillResultCard />
70
+ ```
71
+
72
+ For a file-layout change:
73
+
74
+ ```diff
75
+ src/
76
+ ├── commands/
77
+ +│ └── show-me.ts # expands the slash command
78
+ ├── sessions/
79
+ -└── transport.ts
80
+ +└── transport/
81
+ + ├── client.ts
82
+ + └── stream.ts
83
+ ```
84
+
85
+ For a call-tree or call-stack change:
86
+
87
+ ```diff
88
+ submitForm
89
+ createSession
90
+ persistPrompt
91
+ + expandSkillMention
92
+ launchAgent
93
+ - navigateToSession
94
+ + navigateToSession
95
+ + subscribeToEvents
96
+ ```
97
+
98
+ For a state or control-flow change:
99
+
100
+ ```diff
101
+ on(save)
102
+ - write content
103
+ + if content is unchanged
104
+ + return cached result
105
+ + write new content
106
+ + invalidate cache
107
+ ```
108
+
109
+ - Show the whole block when most of it is new, when omitted context would hide ownership or order, or when the user needs a copyable target shape:
110
+
111
+ ```ts
112
+ function expandSkill(command: string): string {
113
+ const skillName = command.slice(1)
114
+ return `use the ${skillName} skill`
115
+ }
116
+ ```
117
+
118
+ - For a visual UI, layout, state comparison, or concept too dense for Mermaid, write one focused HTML file — a diagram, an infographic, or a short slide deck, whichever fits the point. Match the product's colors, type, spacing, and components; use real labels and data; support desktop and mobile. Then open it for the user:
119
+
120
+ ```
121
+ Bash(open path/to/show-me-{description}.html)
122
+ ```
123
+
124
+ ### guidance
125
+
126
+ Place each visual next to the short text it supports. Keep only the calls, files, props, states, and boundaries needed to answer the user's current question or the options to resolve the current discussion point.
127
+
128
+ You may use one of these, you may use several, it is unlikely you will use all of them. Use your judgement and don't overwhelm the user.
@@ -0,0 +1,2 @@
1
+ policy:
2
+ allow_implicit_invocation: false
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: statechart
3
+ description: "Draw how any system moves between states, such as a product flow, a protocol, or a service, as a Mermaid statechart from a TLA+ model. Each bug is a numbered path with blue borders to a state that breaks a rule. Use for /statechart, to show a bug as a path through states, or to turn an agreed chart into a TLA+ skeleton."
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Statechart
8
+
9
+ The chart shows how a system moves between states, in the words of the person who reads it. The scripts call that person the reader, as in `readerActions` and reader steps. The reader decides whether the behavior is right. The reader can be a PM who reads a product flow, an engineer who reads a protocol or a service, or a reviewer who reads a design. Label each state with what the reader can observe or cares about, such as "Paying" in an app or "Lease held, write pending" in a lock service. Label each step with the event that moves the system, such as "tap Pay" in an app, "the request reads the count" in a service, or "the lease expires" in a lock service. Keep variable and action names out of labels.
10
+
11
+ Run `node <skill dir>/scripts/statechart.mjs --help` for the commands and for how to read a chart. Work in a scratch folder outside the repo. The command writes its files next to its input. The first run installs TLC and Java with mise, which downloads about 305 MB and needs the network. The help's TLC section tells how to use a TLC and Java that you have.
12
+
13
+ ## Pick the direction
14
+
15
+ Use `from-spec` when a TLA+ model exists, or when the code or design is concrete enough to model. Use `from-chart` when there is no model, and the people who own the behavior should agree on it before anyone builds or changes it.
16
+
17
+ If the code already has a TLA+ spec, start from a copy of that spec. Add the variables that the labels and rules need, and keep the spec's existing invariants. To audit the code against that spec, make the copy model what the code does, and pass the spec unchanged as `--against`. Check it with the config that its authors say it passes, and pass that config as `--against-config` unless it has the spec's file name. One mapping labels both models, so label states and steps from variables that both have. A label that reads a variable that only the copy has must test `"name" in v` first, and label the spec's states another way. When a model describes code, tie every variable and action to code that a reviewer can open, and take its name from that code. Make each action one indivisible step in the code. Put the file and line of the code that checks each condition of an action in a comment on that condition, as in `\* pay.ts:120`.
18
+
19
+ ## Spec to chart
20
+
21
+ 1. Choose the chart's states. When the reader cares about something that a label would hide, put it in the label, such as "Paying, charged twice" when the screen still says "Paying" after a second charge. Start with few states. Split a state when the split separates two bugs, or when the reader must see a step that leaves the label the same, such as a request that reads a count, because the chart does not draw such a step.
22
+ 2. Write each rule that the reader agrees to as one invariant, with the reader's sentence as its comment. A rule can be about any state in the model: what a screen shows, what a request returns, a stored value, or a message between parts. When the model describes code, the rule also names the code that can change that state. If the model has no variable for what a rule is about, add the variable. Do not drop the rule. For a rule that applies once everything settles, define `Settled == ~ENABLED System`, where `System` is the disjunction of the actions that happen without the reader. For a rule that applies at a narrower point, such as after each request finishes, write a predicate for that point and make the rule hold where it is true. For a goal that the system must be able to reach, write a rule that says the goal never happens, and pass that rule with `--expect-broken`. The path that breaks it is then a way to the goal. The chart still draws the goal as a state that breaks a rule, and `stories.md` tells it as a bug, so name each goal rule in the hand-off. Report each other broken rule as a bug. Never weaken a rule to make the chart pass.
23
+ 3. Run `from-spec`, then fill in the mapping file it writes. Put the labels in `state` and `step`. When the chart must separate what the reader does from what happens around the reader, list the reader's actions in `readerActions` by action name. Leave the list empty when every action counts the same. When the overview is too large to read, add `--cover`. The overview then keeps only the shortest path to each settled state, plus every bug path, and the command lists what it leaves out. When two bugs end in the same state for different reasons, add `cause` and make it name each reason. To color the states by what the reader cares about, such as "Order shipped", return that group from `group`. The legend names the groups, and in the PNG a state that breaks a rule is red in place of its group color.
24
+ 4. If there is a proposed fix, model it and pass that model as `--fixed`.
25
+ 5. Render each chart with `--png` and look at every PNG. Check that each numbered path reads in order. If a label reads like code, fix `state` or `step` and run the command again.
26
+ 6. When the model describes code, replay each bug's story against the real code, as a test or a live repro, one step per model action. Drive only entry points that a user, a client, or another part of the system reaches, and assert only what they can observe. A test that asserts a flag that it sets, or that searches the source text, proves nothing. When a run proves part of the story and reading the code proves the rest, say which part each proves. When the test copies code that it cannot import, give the commit and line range of the copy, and list what the copy leaves out. If a bug does not reproduce, assume the model is wrong until you show otherwise. Fix the model, not the rule. For a design with no code yet, the TLC trace is the evidence, and the hand-off must say so.
27
+
28
+ ## Chart to spec
29
+
30
+ 1. Draw the chart that the reader agrees to. Use one region for each part that changes independently, such as a device's connection and the job that the device runs. Add one `%% never` line per rule, with the reader's sentence. Draw an event that stops or restarts a part, such as "the app closes" or "the process restarts", as an edge to the state that comes next. Run `render CHART.mmd` to get a PNG of the chart for the reader.
31
+ 2. Run `from-chart`. If a rule breaks, the agreed chart has a design bug. Take the bug to the people who agreed to the chart.
32
+ 3. Add the real code's guards and variables to the spec that `from-chart` wrote, and run `from-chart` again. For each difference it prints, ask the reader whether the code is wrong or the chart needs a correction.
33
+
34
+ ## Hand off
35
+
36
+ Give the PNG and the Mermaid source of each chart to the person who must decide. Add one line per bug with the rule it breaks, its steps as the chart numbers them, whether it reproduced, and whether the proposed fix fixes it. A broken PROPERTY has no path on the chart, so give the TLC counterexample that `stories.md` names for it. With `--against`, also say whether the spec already lists it: in the configs that `stories.md` names, in the spec's notes on what the model leaves out, or not at all. When the model describes code, do not report that a rule holds while the command lists conditions with no file and line. Take every count you quote from `summary.json`. Attach `stories.md` and the command that regenerates everything. Say what the model leaves out.
@@ -0,0 +1,81 @@
1
+ import fs from "node:fs";
2
+ import { BROKEN_MARK, parseValue, stateKey } from "./lib.mjs";
3
+ import { SEP } from "./chart-parts.mjs";
4
+
5
+ export function firstBad(raw) {
6
+ const badStates = new Map();
7
+ for (const t of raw) {
8
+ if (!badStates.has(t.invariant)) badStates.set(t.invariant, new Set());
9
+ badStates.get(t.invariant).add(stateKey(t.steps[t.steps.length - 1].vars));
10
+ }
11
+ const cuts = [];
12
+ const seen = new Set();
13
+ for (const t of raw) {
14
+ const first = t.steps.findIndex((s) => badStates.get(t.invariant).has(stateKey(s.vars)));
15
+ const cut = { invariant: t.invariant, steps: t.steps.slice(0, first + 1) };
16
+ const id = `${t.invariant}${SEP}${stateKey(cut.steps[cut.steps.length - 1].vars)}`;
17
+ if (seen.has(id)) continue;
18
+ seen.add(id);
19
+ cuts.push(cut);
20
+ }
21
+ return cuts;
22
+ }
23
+
24
+ export const byOrder = (a, b) =>
25
+ a.steps - b.steps ||
26
+ a.trace.steps.length - b.trace.steps.length ||
27
+ (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
28
+
29
+ const lastKey = (t) => stateKey(t.steps[t.steps.length - 1].vars);
30
+ export const badOf = (raw, rule) => new Set(raw.filter((t) => t.invariant === rule).map(lastKey));
31
+ export function reachableAvoiding({ graph: g, stateKeys }, bad) {
32
+ const ok = (id) => !bad.has(stateKeys.get(id));
33
+ const next = new Map();
34
+ for (const e of g.edges) {
35
+ if (!next.has(e.from)) next.set(e.from, []);
36
+ next.get(e.from).push(e.to);
37
+ }
38
+ const clean = new Set([...g.nodes.values()].filter((n) => n.init && ok(n.id)).map((n) => n.id));
39
+ for (const id of clean) for (const to of next.get(id) ?? []) if (ok(to)) clean.add(to);
40
+ return clean;
41
+ }
42
+
43
+ export const diverges = (c) => c.second?.key !== undefined;
44
+ export function readBroken(file) {
45
+ if (!file) return [];
46
+ const lines = fs.readFileSync(file, "utf8").split("\n");
47
+ const marked = new RegExp(`^<<\\s*"${BROKEN_MARK}",`);
48
+ const out = [];
49
+ for (let i = 0; i < lines.length; i++) {
50
+ if (!marked.test(lines[i])) continue;
51
+ let text = lines[i];
52
+ while (typeof parseValue(text) === "string" && i + 1 < lines.length) text += `\n${lines[++i]}`;
53
+ const [, vars, broken] = parseValue(text);
54
+ out.push({ key: stateKey(vars), rules: broken });
55
+ }
56
+ return out;
57
+ }
58
+ export const badStates = (raw, broken, counts = () => true) =>
59
+ new Set([
60
+ ...raw.filter((t) => counts(t.invariant)).map(lastKey),
61
+ ...broken.filter((b) => b.rules.some(counts)).map((b) => b.key),
62
+ ]);
63
+ export function labelStatus(m, keys, bad) {
64
+ const good = reachableAvoiding(m, bad);
65
+ const kinds = new Map();
66
+ for (const n of m.graph.nodes.values()) {
67
+ const k = keys.get(n.id);
68
+ if (!kinds.has(k)) kinds.set(k, new Set());
69
+ kinds.get(k).add(bad.has(m.stateKeys.get(n.id)) ? "bad" : good.has(n.id) ? "good" : "after");
70
+ }
71
+ return (k) => {
72
+ const has = kinds.get(k);
73
+ if (!has) return undefined;
74
+ if (has.has("bad")) return has.has("good") ? "mixed" : "wrong";
75
+ return has.has("good") ? "works" : "after";
76
+ };
77
+ }
78
+
79
+ export const RANK = ["works", "fixed", "after", "mixed", "wrong", "worse"];
80
+ export const worst = (...all) =>
81
+ all.filter(Boolean).reduce((a, b) => (RANK.indexOf(b) > RANK.indexOf(a) ? b : a), "works");
@@ -0,0 +1,68 @@
1
+ import { edgeId } from "./chart-parts.mjs";
2
+ import { diverges } from "./bug-states.mjs";
3
+
4
+ export const edgesAlong = (c) =>
5
+ c.path.slice(1).map((p, i) => ({
6
+ id: edgeId(c.path[i].key, p.key),
7
+ from: c.path[i].key,
8
+ to: p.key,
9
+ label: p.label,
10
+ guard: p.guard,
11
+ }));
12
+
13
+ export function chartEdges({ args, now, secondQ, modelEdges, classes, chosen, coverOff }) {
14
+ const edges = new Map(modelEdges);
15
+ for (const { second: s } of classes.filter(diverges)) {
16
+ if (modelEdges.has(s.edge)) continue;
17
+ if (!edges.has(s.edge))
18
+ edges.set(s.edge, {
19
+ from: s.from,
20
+ to: s.key,
21
+ labels: new Set(),
22
+ secondOnly: true,
23
+ apart: s.apart,
24
+ });
25
+ edges.get(s.edge).labels.add(s.overview);
26
+ }
27
+ const pathNumbers = new Map();
28
+ (chosen ? edgesAlong(chosen) : []).forEach((step, i) => {
29
+ if (!pathNumbers.has(step.id)) pathNumbers.set(step.id, []);
30
+ const shown = step.guard ? `${step.label} [${step.guard}]` : step.label;
31
+ pathNumbers.get(step.id).push({ n: i + 1, label: step.label, shown });
32
+ if (!edges.has(step.id))
33
+ edges.set(step.id, { from: step.from, to: step.to, labels: new Set([step.label]) });
34
+ });
35
+ const bugSteps = classes.flatMap(edgesAlong);
36
+ const pathEdges = new Map();
37
+ for (const step of bugSteps) {
38
+ if (edges.has(step.id)) continue;
39
+ if (!pathEdges.has(step.id))
40
+ pathEdges.set(step.id, { from: step.from, to: step.to, labels: new Set() });
41
+ pathEdges.get(step.id).labels.add(step.label);
42
+ }
43
+ for (const [id, e] of pathEdges) edges.set(id, e);
44
+ if (!args.cover || coverOff) return { edges, pathNumbers, coverEdges: null };
45
+
46
+ const coverEdges = new Set(bugSteps.map((step) => step.id));
47
+ const bfs = (q, targets) => {
48
+ const parent = new Map([...q.initial].map((k) => [k, null]));
49
+ const outEdges = new Map();
50
+ for (const [id, e] of q.edges) {
51
+ if (!outEdges.has(e.from)) outEdges.set(e.from, []);
52
+ outEdges.get(e.from).push([id, e]);
53
+ }
54
+ for (const k of parent.keys())
55
+ for (const [id, e] of outEdges.get(k) ?? []) if (!parent.has(e.to)) parent.set(e.to, id);
56
+ for (const t of targets) {
57
+ for (let id = parent.get(t); id; id = parent.get(q.edges.get(id).from)) coverEdges.add(id);
58
+ }
59
+ };
60
+ bfs(now, now.settled);
61
+ if (secondQ)
62
+ bfs(
63
+ secondQ,
64
+ [...secondQ.settled].filter((k) => !now.nodes.has(k)),
65
+ );
66
+ for (const c of classes.filter(diverges)) coverEdges.add(c.second.edge);
67
+ return { edges, pathNumbers, coverEdges };
68
+ }
@@ -0,0 +1,21 @@
1
+ export const SEP = "\u0000";
2
+ export const edgeId = (from, to) => `${from}${SEP}${SEP}${to}`;
3
+
4
+ export function guarded(guards, label) {
5
+ const words = guards?.get(label);
6
+ return words && !words.has("") ? `${label} [${[...words].sort().join(" or ")}]` : label;
7
+ }
8
+ export const addGuard = (guards, label, words) => {
9
+ if (!guards.has(label)) guards.set(label, new Set());
10
+ guards.get(label).add(words);
11
+ };
12
+
13
+ export const frontMatter = (title) => [
14
+ "---",
15
+ ...(title ? [`title: ${JSON.stringify(title)}`] : []),
16
+ "config:",
17
+ " markdownAutoWrap: false",
18
+ " state:",
19
+ " useMaxWidth: false",
20
+ "---",
21
+ ];